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

Merge branch 'master' into pr/1353

This commit is contained in:
Trevor Buckner
2021-06-20 14:19:51 -04:00
22 changed files with 11177 additions and 809 deletions

View File

@@ -1,4 +1,4 @@
FROM node:8 FROM node:14.15
ENV NODE_ENV=docker ENV NODE_ENV=docker

View File

@@ -6,6 +6,11 @@ h5 {
# changelog # changelog
### Thursday, 10/6/2021 - v2.12.0
- New "style" tab to better organize custom CSS in preparation for new themes and sharable styles.
- Your own Google brews will no longer show up in the list when viewing someone else's profile.
### Saturday, 02/5/2021 - v2.11.2 ### Saturday, 02/5/2021 - v2.11.2
- Fix for edge case where brews could accidentally transfer from Google Drive back to Homebrewery. - Fix for edge case where brews could accidentally transfer from Google Drive back to Homebrewery.

View File

@@ -20,6 +20,7 @@ const BrewRenderer = createClass({
getDefaultProps : function() { getDefaultProps : function() {
return { return {
text : '', text : '',
style : '',
renderer : 'legacy', renderer : 'legacy',
errors : [] errors : []
}; };
@@ -123,9 +124,9 @@ const BrewRenderer = createClass({
renderPage : function(pageText, index){ renderPage : function(pageText, index){
if(this.props.renderer == 'legacy') if(this.props.renderer == 'legacy')
return <div className='phb' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }} key={index} />; return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }} key={index} />;
else else
return <div className='phb3' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} key={index} />; return <div className='phb3 page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} key={index} />;
}, },
renderPages : function(){ renderPages : function(){
@@ -187,6 +188,10 @@ const BrewRenderer = createClass({
</div> </div>
<div className='pages' ref='pages'> <div className='pages' ref='pages'>
{/* Apply CSS from Style tab */}
<div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${this.props.style} </style>` }} />
{/* Render pages from Markdown tab */}
{this.state.isMounted {this.state.isMounted
? this.renderPages() ? this.renderPages()
: null} : null}

View File

@@ -19,57 +19,62 @@ const Editor = createClass({
getDefaultProps : function() { getDefaultProps : function() {
return { return {
brew : { brew : {
text : '' text : '',
style : ''
}, },
onChange : ()=>{},
onMetadataChange : ()=>{}, onTextChange : ()=>{},
showMetaButton : true, onStyleChange : ()=>{},
renderer : 'legacy' onMetaChange : ()=>{},
renderer : 'legacy'
}; };
}, },
getInitialState : function() { getInitialState : function() {
return { return {
showMetadataEditor : false view : 'text' //'text', 'style', 'meta'
}; };
}, },
cursorPosition : {
line : 0, isText : function() {return this.state.view == 'text';},
ch : 0 isStyle : function() {return this.state.view == 'style';},
}, isMeta : function() {return this.state.view == 'meta';},
componentDidMount : function() { componentDidMount : function() {
this.updateEditorSize(); this.updateEditorSize();
this.highlightCustomMarkdown(); this.highlightCustomMarkdown();
window.addEventListener('resize', this.updateEditorSize); window.addEventListener('resize', this.updateEditorSize);
}, },
componentWillUnmount : function() { componentWillUnmount : function() {
window.removeEventListener('resize', this.updateEditorSize); window.removeEventListener('resize', this.updateEditorSize);
}, },
updateEditorSize : function() { updateEditorSize : function() {
let paneHeight = this.refs.main.parentNode.clientHeight; if(this.refs.codeEditor) {
paneHeight -= SNIPPETBAR_HEIGHT + 1; let paneHeight = this.refs.main.parentNode.clientHeight;
this.refs.codeEditor.codeMirror.setSize(null, paneHeight); paneHeight -= SNIPPETBAR_HEIGHT + 1;
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
}
}, },
handleTextChange : function(text){
this.props.onChange(text);
},
handleCursorActivty : function(curpos){
this.cursorPosition = curpos;
},
handleInject : function(injectText){ handleInject : function(injectText){
const lines = this.props.brew.text.split('\n'); const text = (this.isText() ? this.props.brew.text : this.props.brew.style);
lines[this.cursorPosition.line] = splice(lines[this.cursorPosition.line], this.cursorPosition.ch, injectText);
this.handleTextChange(lines.join('\n')); const lines = text.split('\n');
this.refs.codeEditor.setCursorPosition(this.cursorPosition.line + injectText.split('\n').length, this.cursorPosition.ch + injectText.length); const cursorPos = this.refs.codeEditor.getCursorPosition();
lines[cursorPos.line] = splice(lines[cursorPos.line], cursorPos.ch, injectText);
this.refs.codeEditor.setCursorPosition(cursorPos.line + injectText.split('\n').length, cursorPos.ch + injectText.length);
if(this.isText()) this.props.onTextChange(lines.join('\n'));
if(this.isStyle()) this.props.onStyleChange(lines.join('\n'));
}, },
handgleToggle : function(){
handleViewChange : function(newView){
this.setState({ this.setState({
showMetadataEditor : !this.state.showMetadataEditor view : newView
}); }, this.updateEditorSize); //TODO: not sure if updateeditorsize needed
}, },
getCurrentPage : function(){ getCurrentPage : function(){
@@ -82,72 +87,73 @@ const Editor = createClass({
highlightCustomMarkdown : function(){ highlightCustomMarkdown : function(){
if(!this.refs.codeEditor) return; if(!this.refs.codeEditor) return;
const codeMirror = this.refs.codeEditor.codeMirror; if(this.state.view === 'text') {
const codeMirror = this.refs.codeEditor.codeMirror;
//reset custom text styles //reset custom text styles
const customHighlights = codeMirror.getAllMarks(); const customHighlights = codeMirror.getAllMarks();
for (let i=0;i<customHighlights.length;i++) customHighlights[i].clear(); for (let i=0;i<customHighlights.length;i++) customHighlights[i].clear();
const lineNumbers = _.reduce(this.props.brew.text.split('\n'), (r, line, lineNumber)=>{ const lineNumbers = _.reduce(this.props.brew.text.split('\n'), (r, line, lineNumber)=>{
//reset custom line styles //reset custom line styles
codeMirror.removeLineClass(lineNumber, 'background'); codeMirror.removeLineClass(lineNumber, 'background');
codeMirror.removeLineClass(lineNumber, 'text'); codeMirror.removeLineClass(lineNumber, 'text');
// Legacy Codemirror styling // Legacy Codemirror styling
if(this.props.renderer == 'legacy') { if(this.props.renderer == 'legacy') {
if(line.includes('\\page')){ if(line.includes('\\page')){
codeMirror.addLineClass(lineNumber, 'background', 'pageLine'); codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
r.push(lineNumber); r.push(lineNumber);
}
}
// New Codemirror styling for V3 renderer
if(this.props.renderer == 'V3') {
if(line.startsWith('\\page')){
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
r.push(lineNumber);
}
if(line.match(/^\\column$/)){
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
r.push(lineNumber);
}
// Highlight inline spans {{content}}
if(line.includes('{{') && line.includes('}}')){
const regex = /{{(?:="[\w,\-. ]*"|[^"'\s])*\s*|}}/g;
let match;
let blockCount = 0;
while ((match = regex.exec(line)) != null) {
if(match[0].startsWith('{')) {
blockCount += 1;
} else {
blockCount -= 1;
}
if(blockCount < 0) {
blockCount = 0;
continue;
}
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'inline-block' });
} }
} else if(line.trimLeft().startsWith('{{') || line.trimLeft().startsWith('}}')){
// Highlight block divs {{\n Content \n}}
let endCh = line.length+1;
const match = line.match(/^ *{{(?:="[\w,\-. ]*"|[^"'\s])*$|^ *}}$/);
if(match)
endCh = match.index+match[0].length;
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
} }
}
return r; // New Codemirror styling for V3 renderer
}, []); if(this.props.renderer == 'V3') {
return lineNumbers; if(line.startsWith('\\page')){
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
r.push(lineNumber);
}
if(line.match(/^\\column$/)){
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
r.push(lineNumber);
}
// Highlight inline spans {{content}}
if(line.includes('{{') && line.includes('}}')){
const regex = /{{(?:="[\w,\-. ]*"|[^"'\s])*\s*|}}/g;
let match;
let blockCount = 0;
while ((match = regex.exec(line)) != null) {
if(match[0].startsWith('{')) {
blockCount += 1;
} else {
blockCount -= 1;
}
if(blockCount < 0) {
blockCount = 0;
continue;
}
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'inline-block' });
}
} else if(line.trimLeft().startsWith('{{') || line.trimLeft().startsWith('}}')){
// Highlight block divs {{\n Content \n}}
let endCh = line.length+1;
const match = line.match(/^ *{{(?:="[\w,\-. ]*"|[^"'\s])*$|^ *}}$/);
if(match)
endCh = match.index+match[0].length;
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
}
}
return r;
}, []);
return lineNumbers;
}
}, },
brewJump : function(){ brewJump : function(){
const currentPage = this.getCurrentPage(); const currentPage = this.getCurrentPage();
window.location.hash = `p${currentPage}`; window.location.hash = `p${currentPage}`;
@@ -158,12 +164,26 @@ const Editor = createClass({
this.refs.codeEditor.updateSize(); this.refs.codeEditor.updateSize();
}, },
renderMetadataEditor : function(){ renderEditor : function(){
if(!this.state.showMetadataEditor) return; if(this.isText()){
return <MetadataEditor return <CodeEditor key='text'
metadata={this.props.brew} ref='codeEditor'
onChange={this.props.onMetadataChange} language='gfm'
/>; value={this.props.brew.text}
onChange={this.props.onTextChange} />;
}
if(this.isStyle()){
return <CodeEditor key='style'
ref='codeEditor'
language='css'
value={this.props.brew.style}
onChange={this.props.onStyleChange} />;
}
if(this.isMeta()){
return <MetadataEditor
metadata={this.props.brew}
onChange={this.props.onMetaChange} />;
}
}, },
render : function(){ render : function(){
@@ -172,25 +192,13 @@ const Editor = createClass({
<div className='editor' ref='main'> <div className='editor' ref='main'>
<SnippetBar <SnippetBar
brew={this.props.brew} brew={this.props.brew}
view={this.state.view}
onViewChange={this.handleViewChange}
onInject={this.handleInject} onInject={this.handleInject}
onToggle={this.handgleToggle} showEditButtons={this.props.showEditButtons}
showmeta={this.state.showMetadataEditor}
showMetaButton={this.props.showMetaButton}
renderer={this.props.renderer} /> renderer={this.props.renderer} />
{this.renderMetadataEditor()}
<CodeEditor
ref='codeEditor'
wrap={true}
language='gfm'
value={this.props.brew.text}
onChange={this.handleTextChange}
onCursorActivity={this.handleCursorActivty} />
{/* {this.renderEditor()}
<div className='brewJump' onClick={this.brewJump}>
<i className='fas fa-arrow-right' />
</div>
*/}
</div> </div>
); );
} }

View File

@@ -16,12 +16,13 @@ const execute = function(val, brew){
const Snippetbar = createClass({ const Snippetbar = createClass({
getDefaultProps : function() { getDefaultProps : function() {
return { return {
brew : {}, brew : {},
onInject : ()=>{}, view : 'text',
onToggle : ()=>{}, onViewChange : ()=>{},
showmeta : false, onInject : ()=>{},
showMetaButton : true, onToggle : ()=>{},
renderer : '' showEditButtons : true,
renderer : 'legacy'
}; };
}, },
@@ -36,12 +37,16 @@ const Snippetbar = createClass({
}, },
renderSnippetGroups : function(){ renderSnippetGroups : function(){
if(this.props.renderer == 'V3') let snippets = [];
Snippets = SnippetsV3;
else
Snippets = SnippetsLegacy;
return _.map(Snippets, (snippetGroup)=>{ if(this.props.view === 'text') {
if(this.props.renderer === 'V3')
snippets = SnippetsV3;
else
snippets = SnippetsLegacy;
}
return _.map(snippets, (snippetGroup)=>{
return <SnippetGroup return <SnippetGroup
brew={this.props.brew} brew={this.props.brew}
groupName={snippetGroup.groupName} groupName={snippetGroup.groupName}
@@ -53,19 +58,29 @@ const Snippetbar = createClass({
}); });
}, },
renderMetadataButton : function(){ renderEditorButtons : function(){
if(!this.props.showMetaButton) return; if(!this.props.showEditButtons) return;
return <div className={cx('snippetBarButton', 'toggleMeta', { selected: this.props.showmeta })}
onClick={this.props.onToggle}> return <div className='editors'>
<i className='fas fa-info-circle' /> <div className={cx('text', { selected: this.props.view === 'text' })}
<span className='groupName'>Properties</span> onClick={()=>this.props.onViewChange('text')}>
<i className='fa fa-beer' />
</div>
<div className={cx('style', { selected: this.props.view === 'style' })}
onClick={()=>this.props.onViewChange('style')}>
<i className='fa fa-paint-brush' />
</div>
<div className={cx('meta', { selected: this.props.view === 'meta' })}
onClick={()=>this.props.onViewChange('meta')}>
<i className='fas fa-info-circle' />
</div>
</div>; </div>;
}, },
render : function(){ render : function(){
return <div className='snippetBar'> return <div className='snippetBar'>
{this.renderSnippetGroups()} {this.renderSnippetGroups()}
{this.renderMetadataButton()} {this.renderEditorButtons()}
</div>; </div>;
} }
}); });

View File

@@ -1,12 +1,40 @@
.snippetBar{ .snippetBar{
@height : 25px; @menuHeight : 25px;
position : relative; position : relative;
height : @height; height : @menuHeight;
background-color : #ddd; background-color : #ddd;
.editors{
position : absolute;
display : flex;
top : 0px;
right : 0px;
height : @menuHeight;
width : 90px;
justify-content : space-between;
&>div{
height : @menuHeight;
width : @menuHeight;
cursor : pointer;
line-height : @menuHeight;
text-align : center;
&:hover,&.selected{
background-color : #999;
}
&.text{
.tooltipLeft('Brew Editor');
}
&.style{
.tooltipLeft('Style Editor');
}
&.meta{
.tooltipLeft('Properties');
}
}
}
.snippetBarButton{ .snippetBarButton{
height : @height; height : @menuHeight;
line-height : @height; line-height : @menuHeight;
display : inline-block; display : inline-block;
padding : 0px 5px; padding : 0px 5px;
font-weight : 800; font-weight : 800;

View File

@@ -66,7 +66,7 @@ module.exports = [
{ {
name : 'Auto-incrementing Page Number', name : 'Auto-incrementing Page Number',
icon : 'fas fa-sort-numeric-down', icon : 'fas fa-sort-numeric-down',
gen : '{{\npageNumber,auto\n}}\n\n' gen : '{{pageNumber,auto\n}}\n\n'
}, },
{ {
name : 'Link to page', name : 'Link to page',

View File

@@ -35,7 +35,6 @@ const Homebrew = createClass({
global.account = this.props.account; global.account = this.props.account;
global.version = this.props.version; global.version = this.props.version;
global.enable_v3 = this.props.enable_v3; global.enable_v3 = this.props.enable_v3;
}, },
render : function (){ render : function (){
return ( return (
@@ -45,7 +44,7 @@ const Homebrew = createClass({
<Route path='/edit/:id' component={(routeProps)=><EditPage id={routeProps.match.params.id} brew={this.props.brew} />}/> <Route path='/edit/:id' component={(routeProps)=><EditPage id={routeProps.match.params.id} brew={this.props.brew} />}/>
<Route path='/share/:id' component={(routeProps)=><SharePage id={routeProps.match.params.id} brew={this.props.brew} />}/> <Route path='/share/:id' component={(routeProps)=><SharePage id={routeProps.match.params.id} brew={this.props.brew} />}/>
<Route path='/new/:id' component={(routeProps)=><NewPage id={routeProps.match.params.id} brew={this.props.brew} />}/> <Route path='/new/:id' component={(routeProps)=><NewPage id={routeProps.match.params.id} brew={this.props.brew} />}/>
<Route path='/new' exact component={NewPage}/> <Route path='/new' exact component={(routeProps)=><NewPage/>}/>
<Route path='/user/:username' component={(routeProps)=><UserPage username={routeProps.match.params.username} brews={this.props.brews} />}/> <Route path='/user/:username' component={(routeProps)=><UserPage username={routeProps.match.params.username} brews={this.props.brews} />}/>
<Route path='/print/:id' component={(routeProps)=><PrintPage brew={this.props.brew} query={queryString.parse(routeProps.location.search)} /> } /> <Route path='/print/:id' component={(routeProps)=><PrintPage brew={this.props.brew} query={queryString.parse(routeProps.location.search)} /> } />
<Route path='/print' exact component={(routeProps)=><PrintPage query={queryString.parse(routeProps.location.search)} /> } /> <Route path='/print' exact component={(routeProps)=><PrintPage query={queryString.parse(routeProps.location.search)} /> } />

View File

@@ -31,6 +31,7 @@ const EditPage = createClass({
return { return {
brew : { brew : {
text : '', text : '',
style : '',
shareId : null, shareId : null,
editId : null, editId : null,
createdAt : null, createdAt : null,
@@ -106,17 +107,8 @@ const EditPage = createClass({
this.refs.editor.update(); this.refs.editor.update();
}, },
handleMetadataChange : function(metadata){
this.setState((prevState)=>({
brew : _.merge({}, prevState.brew, metadata),
isPending : true,
}), ()=>this.trySave());
},
handleTextChange : function(text){ handleTextChange : function(text){
//If there are errors, run the validator on every change to give quick feedback
//If there are errors, run the validator on everychange to give quick feedback
let htmlErrors = this.state.htmlErrors; let htmlErrors = this.state.htmlErrors;
if(htmlErrors.length) htmlErrors = Markdown.validate(text); if(htmlErrors.length) htmlErrors = Markdown.validate(text);
@@ -127,6 +119,21 @@ const EditPage = createClass({
}), ()=>this.trySave()); }), ()=>this.trySave());
}, },
handleStyleChange : function(style){
this.setState((prevState)=>({
brew : _.merge({}, prevState.brew, { style: style }),
isPending : true
}), ()=>this.trySave());
},
handleMetaChange : function(metadata){
this.setState((prevState)=>({
brew : _.merge({}, prevState.brew, metadata),
isPending : true,
}), ()=>this.trySave());
},
hasChanges : function(){ hasChanges : function(){
return !_.isEqual(this.state.brew, this.savedBrew); return !_.isEqual(this.state.brew, this.savedBrew);
}, },
@@ -141,7 +148,6 @@ const EditPage = createClass({
}, },
handleGoogleClick : function(){ handleGoogleClick : function(){
console.log('handlegoogleclick');
if(!global.account?.googleId) { if(!global.account?.googleId) {
this.setState({ this.setState({
alertLoginToTransfer : true alertLoginToTransfer : true
@@ -409,11 +415,12 @@ const EditPage = createClass({
<Editor <Editor
ref='editor' ref='editor'
brew={this.state.brew} brew={this.state.brew}
onChange={this.handleTextChange} onTextChange={this.handleTextChange}
onMetadataChange={this.handleMetadataChange} onStyleChange={this.handleStyleChange}
onMetaChange={this.handleMetaChange}
renderer={this.state.brew.renderer} renderer={this.state.brew.renderer}
/> />
<BrewRenderer text={this.state.brew.text} errors={this.state.htmlErrors} renderer={this.state.brew.renderer} /> <BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} errors={this.state.htmlErrors} />
</SplitPane> </SplitPane>
</div> </div>
</div>; </div>;

View File

@@ -78,7 +78,13 @@ const HomePage = createClass({
<div className='content'> <div className='content'>
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'> <SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
<Editor brew={this.state.brew} onChange={this.handleTextChange} showMetaButton={false} ref='editor'/> <Editor
ref='editor'
brew={this.state.brew}
onTextChange={this.handleTextChange}
renderer={this.state.brew.renderer}
showEditButtons={false}
/>
<BrewRenderer text={this.state.brew.text} /> <BrewRenderer text={this.state.brew.text} />
</SplitPane> </SplitPane>
</div> </div>

View File

@@ -3,6 +3,7 @@ const React = require('react');
const createClass = require('create-react-class'); const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const request = require('superagent'); const request = require('superagent');
const dedent = require('dedent-tabs').default;
const Markdown = require('naturalcrit/markdown.js'); const Markdown = require('naturalcrit/markdown.js');
@@ -16,14 +17,20 @@ const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx'); const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const KEY = 'homebrewery-new'; const KEY = 'homebrewery-new';
const NewPage = createClass({ const NewPage = createClass({
getDefaultProps : function() { getDefaultProps : function() {
return { return {
brew : { brew : {
text : '', text : '',
style : dedent`
/*=======--- Example CSS styling ---=======*/
/* Any CSS here will apply to your document! */
.myExampleClass {
color: black;
}`,
shareId : null, shareId : null,
editId : null, editId : null,
createdAt : null, createdAt : null,
@@ -44,18 +51,21 @@ const NewPage = createClass({
return { return {
brew : { brew : {
text : this.props.brew.text || '', text : this.props.brew.text || '',
style : this.props.brew.style || '',
gDrive : false, gDrive : false,
title : this.props.brew.title || '', title : this.props.brew.title || '',
description : this.props.brew.description || '', description : this.props.brew.description || '',
tags : this.props.brew.tags || '', tags : this.props.brew.tags || '',
published : false, published : false,
authors : [], authors : [],
systems : this.props.brew.systems || [] systems : this.props.brew.systems || [],
renderer : this.props.brew.renderer || 'legacy'
}, },
isSaving : false, isSaving : false,
saveGoogle : (global.account && global.account.googleId ? true : false), saveGoogle : (global.account && global.account.googleId ? true : false),
errors : [] errors : [],
htmlErrors : Markdown.validate(this.props.brew.text)
}; };
}, },
@@ -66,6 +76,11 @@ const NewPage = createClass({
brew : { text: storage } brew : { text: storage }
}); });
} }
this.setState((prevState)=>({
htmlErrors : Markdown.validate(prevState.brew.text)
}));
document.addEventListener('keydown', this.handleControlKeys); document.addEventListener('keydown', this.handleControlKeys);
}, },
componentWillUnmount : function() { componentWillUnmount : function() {
@@ -88,18 +103,29 @@ const NewPage = createClass({
this.refs.editor.update(); this.refs.editor.update();
}, },
handleMetadataChange : function(metadata){ handleTextChange : function(text){
this.setState({ //If there are errors, run the validator on every change to give quick feedback
brew : _.merge({}, this.state.brew, metadata) let htmlErrors = this.state.htmlErrors;
}); if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState((prevState)=>({
brew : _.merge({}, prevState.brew, { text: text }),
htmlErrors : htmlErrors
}));
localStorage.setItem(KEY, text);
}, },
handleTextChange : function(text){ handleStyleChange : function(style){
this.setState({ this.setState((prevState)=>({
brew : { text: text }, brew : _.merge({}, prevState.brew, { style: style }),
errors : Markdown.validate(text) }));
}); },
localStorage.setItem(KEY, text);
handleMetaChange : function(metadata){
this.setState((prevState)=>({
brew : _.merge({}, prevState.brew, metadata),
}));
}, },
save : async function(){ save : async function(){
@@ -190,10 +216,12 @@ const NewPage = createClass({
<Editor <Editor
ref='editor' ref='editor'
brew={this.state.brew} brew={this.state.brew}
onChange={this.handleTextChange} onTextChange={this.handleTextChange}
onMetadataChange={this.handleMetadataChange} onStyleChange={this.handleStyleChange}
onMetaChange={this.handleMetaChange}
renderer={this.state.brew.renderer}
/> />
<BrewRenderer text={this.state.brew.text} errors={this.state.errors} /> <BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} errors={this.state.htmlErrors}/>
</SplitPane> </SplitPane>
</div> </div>
</div>; </div>;

View File

@@ -13,6 +13,7 @@ const PrintPage = createClass({
query : {}, query : {},
brew : { brew : {
text : '', text : '',
style : '',
renderer : 'legacy' renderer : 'legacy'
} }
}; };
@@ -58,6 +59,8 @@ const PrintPage = createClass({
render : function(){ render : function(){
return <div> return <div>
<Meta name='robots' content='noindex, nofollow' /> <Meta name='robots' content='noindex, nofollow' />
{/* Apply CSS from Style tab */}
<div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${this.props.brew.style} </style>` }} />
{this.renderPages()} {this.renderPages()}
</div>; </div>;
} }

View File

@@ -19,6 +19,7 @@ const SharePage = createClass({
brew : { brew : {
title : '', title : '',
text : '', text : '',
style : '',
shareId : null, shareId : null,
createdAt : null, createdAt : null,
updatedAt : null, updatedAt : null,
@@ -72,7 +73,7 @@ const SharePage = createClass({
</Navbar> </Navbar>
<div className='content'> <div className='content'>
<BrewRenderer text={this.props.brew.text} renderer={this.props.brew.renderer} /> <BrewRenderer text={this.props.brew.text} style={this.props.brew.style} renderer={this.props.brew.renderer} />
</div> </div>
</div>; </div>;
} }

11320
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": "2.11.2", "version": "2.12.0",
"engines": { "engines": {
"node": "14.15.x" "node": "14.15.x"
}, },
@@ -35,17 +35,18 @@
}, },
"babel": { "babel": {
"presets": [ "presets": [
"env", "@babel/preset-env",
"react" "@babel/preset-react"
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.14.0", "@babel/core": "^7.14.3",
"@babel/preset-env": "^7.13.15", "@babel/plugin-transform-runtime": "^7.14.5",
"@babel/preset-env": "^7.14.4",
"@babel/preset-react": "^7.13.13", "@babel/preset-react": "^7.13.13",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"codemirror": "^5.61.0", "codemirror": "^5.61.1",
"cookie-parser": "^1.4.5", "cookie-parser": "^1.4.5",
"create-react-class": "^15.7.0", "create-react-class": "^15.7.0",
"dedent-tabs": "^0.9.0", "dedent-tabs": "^0.9.0",
@@ -53,15 +54,15 @@
"express-async-handler": "^1.1.4", "express-async-handler": "^1.1.4",
"express-static-gzip": "2.1.1", "express-static-gzip": "2.1.1",
"fs-extra": "9.1.0", "fs-extra": "9.1.0",
"googleapis": "73.0.0", "googleapis": "75.0.0",
"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": "2.0.3", "marked": "2.0.6",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.29.1", "moment": "^2.29.1",
"mongoose": "^5.12.7", "mongoose": "^5.12.12",
"nanoid": "3.1.22", "nanoid": "3.1.23",
"nconf": "^0.11.2", "nconf": "^0.11.2",
"prop-types": "15.7.2", "prop-types": "15.7.2",
"query-string": "7.0.0", "query-string": "7.0.0",
@@ -71,11 +72,11 @@
"react-router-dom": "5.2.0", "react-router-dom": "5.2.0",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"superagent": "^6.1.0", "superagent": "^6.1.0",
"vitreum": "git+https://github.com/calculuschild/vitreum.git" "vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^7.25.0", "eslint": "^7.27.0",
"eslint-plugin-react": "^7.23.2", "eslint-plugin-react": "^7.23.2",
"pico-check": "^2.0.3" "pico-check": "^2.1.3"
} }
} }

View File

@@ -7,9 +7,13 @@ const isDev = !!process.argv.find((arg)=>arg=='--dev');
const lessTransform = require('vitreum/transforms/less.js'); const lessTransform = require('vitreum/transforms/less.js');
const assetTransform = require('vitreum/transforms/asset.js'); const assetTransform = require('vitreum/transforms/asset.js');
//const Meta = require('vitreum/headtags'); const babel = require('@babel/core');
const babelify = async (code)=>(await babel.transformAsync(code, { presets: ['@babel/preset-env', '@babel/preset-react'], plugins: ['@babel/plugin-transform-runtime'] })).code;
const transforms = { const transforms = {
'.js' : (code, filename, opts)=>babelify(code),
'.jsx' : (code, filename, opts)=>babelify(code),
'.less' : lessTransform, '.less' : lessTransform,
'*' : assetTransform('./build') '*' : assetTransform('./build')
}; };

View File

@@ -10,6 +10,7 @@
"classnames", "classnames",
"codemirror", "codemirror",
"codemirror/mode/gfm/gfm.js", "codemirror/mode/gfm/gfm.js",
"codemirror/mode/css/css.js",
"codemirror/mode/javascript/javascript.js", "codemirror/mode/javascript/javascript.js",
"moment", "moment",
"superagent", "superagent",

View File

@@ -9,6 +9,7 @@ const GoogleActions = require('./server/googleActions.js');
const serveCompressedStaticAssets = require('./server/static-assets.mv.js'); const serveCompressedStaticAssets = require('./server/static-assets.mv.js');
const sanitizeFilename = require('sanitize-filename'); const sanitizeFilename = require('sanitize-filename');
const asyncHandler = require('express-async-handler'); const asyncHandler = require('express-async-handler');
const dedent = require('dedent-tabs').default;
//Get the brew object from the HB database or Google Drive //Get the brew object from the HB database or Google Drive
const getBrewFromId = asyncHandler(async (id, accessType)=>{ const getBrewFromId = asyncHandler(async (id, accessType)=>{
@@ -21,11 +22,35 @@ const getBrewFromId = asyncHandler(async (id, accessType)=>{
brew = await GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, id, accessType); brew = await GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, id, accessType);
} else { } else {
brew = await HomebrewModel.get(accessType == 'edit' ? { editId: id } : { shareId: id }); brew = await HomebrewModel.get(accessType == 'edit' ? { editId: id } : { shareId: id });
brew.sanatize(true); }
brew = sanitizeBrew(brew, accessType === 'edit' ? false : true);
//Split brew.text into text and style
if(brew.text.startsWith('```css')) {
const index = brew.text.indexOf('```\n\n');
brew.style = brew.text.slice(7, index - 1);
brew.text = brew.text.slice(index + 5);
} else {
brew.style = dedent`
/*=======--- Example CSS styling ---=======*/
/* Any CSS here will apply to your document! */
.myExampleClass {
color: black;
}`;
} }
return brew; return brew;
}); });
const sanitizeBrew = (brew, full=false)=>{
delete brew._id;
delete brew.__v;
if(full){
delete brew.editId;
}
return brew;
};
app.use('/', serveCompressedStaticAssets(`${__dirname}/build`)); app.use('/', serveCompressedStaticAssets(`${__dirname}/build`));
process.chdir(__dirname); process.chdir(__dirname);
@@ -112,25 +137,26 @@ app.get('/download/:id', asyncHandler(async (req, res)=>{
//User Page //User Page
app.get('/user/:username', async (req, res, next)=>{ app.get('/user/:username', async (req, res, next)=>{
const fullAccess = req.account && (req.account.username == req.params.username); const ownAccount = req.account && (req.account.username == req.params.username);
let googleBrews = []; let brews = await HomebrewModel.getByUser(req.params.username, ownAccount)
if(req.account && req.account.googleId){
googleBrews = await GoogleActions.listGoogleBrews(req, res)
.catch((err)=>{
console.error(err);
});
}
const brews = await HomebrewModel.getByUser(req.params.username, fullAccess)
.catch((err)=>{ .catch((err)=>{
console.log(err); console.log(err);
}); });
if(googleBrews) { if(ownAccount && req?.account?.googleId){
req.brews = _.concat(brews, googleBrews); const googleBrews = await GoogleActions.listGoogleBrews(req, res)
} else {req.brews = brews;} .catch((err)=>{
console.error(err);
});
if(googleBrews)
brews = _.concat(brews, googleBrews);
}
req.brews = _.map(brews, (brew)=>{
return sanitizeBrew(brew, !ownAccount);
});
return next(); return next();
}); });
@@ -160,7 +186,7 @@ app.get('/share/:id', asyncHandler(async (req, res, next)=>{
await GoogleActions.increaseView(googleId, shareId, 'share', brew) await GoogleActions.increaseView(googleId, shareId, 'share', brew)
.catch((err)=>{next(err);}); .catch((err)=>{next(err);});
} else { } else {
await brew.increaseView(); await HomebrewModel.increaseView({ shareId: brew.shareId });
} }
req.brew = brew; req.brew = brew;
@@ -191,7 +217,6 @@ app.use((req, res)=>{
templateFn('homebrew', title = req.brew ? req.brew.title : '', props) templateFn('homebrew', title = req.brew ? req.brew.title : '', props)
.then((page)=>{ res.send(page); }) .then((page)=>{ res.send(page); })
.catch((err)=>{ .catch((err)=>{
console.log('TEMPLATE ERROR');
console.log(err); console.log(err);
return res.sendStatus(500); return res.sendStatus(500);
}); });

View File

@@ -100,6 +100,7 @@ GoogleActions = {
}) })
.catch((err)=>{ .catch((err)=>{
return console.error(`Error Listing Google Brews: ${err}`); return console.error(`Error Listing Google Brews: ${err}`);
//TODO: Should break out here, but continues on for some reason.
}); });
if(!obj.data.files.length) { if(!obj.data.files.length) {
@@ -152,17 +153,17 @@ GoogleActions = {
fileId : brew.googleId, fileId : brew.googleId,
resource : { name : `${brew.title}.txt`, resource : { name : `${brew.title}.txt`,
description : `${brew.description}`, description : `${brew.description}`,
properties : { title : brew.title, properties : { title : brew.title,
published : brew.published, published : brew.published,
lastViewed : brew.lastViewed, lastViewed : brew.lastViewed,
views : brew.views, views : brew.views,
version : brew.version, version : brew.version,
renderer : brew.renderer, renderer : brew.renderer,
tags : brew.tags, tags : brew.tags,
systems : brew.systems.join() } systems : brew.systems.join() }
}, },
media : { mimeType : 'text/plain', media : { mimeType : 'text/plain',
body : brew.text } body : brew.text }
}) })
.catch((err)=>{ .catch((err)=>{
console.log('Error saving to google'); console.log('Error saving to google');

View File

@@ -19,14 +19,24 @@ const getGoodBrewTitle = (text)=>{
.slice(0, MAX_TITLE_LENGTH); .slice(0, MAX_TITLE_LENGTH);
}; };
const mergeBrewText = (text, style)=>{
text = `\`\`\`css\n` +
`${style}\n` +
`\`\`\`\n\n` +
`${text}`;
return text;
};
const newBrew = (req, res)=>{ const newBrew = (req, res)=>{
const brew = req.body; const brew = req.body;
brew.authors = (req.account) ? [req.account.username] : [];
if(!brew.title) { if(!brew.title) {
brew.title = getGoodBrewTitle(brew.text); brew.title = getGoodBrewTitle(brew.text);
} }
brew.authors = (req.account) ? [req.account.username] : [];
brew.text = mergeBrewText(brew.text, brew.style);
delete brew.editId; delete brew.editId;
delete brew.shareId; delete brew.shareId;
delete brew.googleId; delete brew.googleId;
@@ -53,8 +63,10 @@ const updateBrew = (req, res)=>{
HomebrewModel.get({ editId: req.params.id }) HomebrewModel.get({ editId: req.params.id })
.then((brew)=>{ .then((brew)=>{
brew = _.merge(brew, req.body); brew = _.merge(brew, req.body);
brew.text = mergeBrewText(brew.text, brew.style);
// Compress brew text to binary before saving // Compress brew text to binary before saving
brew.textBin = zlib.deflateRawSync(req.body.text); brew.textBin = zlib.deflateRawSync(brew.text);
// Delete the non-binary text field since it's not needed anymore // Delete the non-binary text field since it's not needed anymore
brew.text = undefined; brew.text = undefined;
brew.updatedAt = new Date(); brew.updatedAt = new Date();
@@ -113,12 +125,14 @@ const newGoogleBrew = async (req, res, next)=>{
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); } try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
const brew = req.body; const brew = req.body;
brew.authors = (req.account) ? [req.account.username] : [];
if(!brew.title) { if(!brew.title) {
brew.title = getGoodBrewTitle(brew.text); brew.title = getGoodBrewTitle(brew.text);
} }
brew.authors = (req.account) ? [req.account.username] : [];
brew.text = mergeBrewText(brew.text, brew.style);
delete brew.editId; delete brew.editId;
delete brew.shareId; delete brew.shareId;
delete brew.googleId; delete brew.googleId;
@@ -135,7 +149,10 @@ const updateGoogleBrew = async (req, res, next)=>{
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); } try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
const updatedBrew = await GoogleActions.updateGoogleBrew(oAuth2Client, req.body); const brew = req.body;
brew.text = mergeBrewText(brew.text, brew.style);
const updatedBrew = await GoogleActions.updateGoogleBrew(oAuth2Client, brew);
return res.status(200).send(updatedBrew); return res.status(200).send(updatedBrew);
}; };

View File

@@ -24,28 +24,15 @@ const HomebrewSchema = mongoose.Schema({
version : { type: Number, default: 1 } version : { type: Number, default: 1 }
}, { versionKey: false }); }, { versionKey: false });
HomebrewSchema.statics.increaseView = async function(query) {
HomebrewSchema.methods.sanatize = function(full=false){ const brew = await Homebrew.findOne(query).exec();
const brew = this.toJSON(); brew.lastViewed = new Date();
delete brew._id; brew.views = brew.views + 1;
delete brew.__v; await brew.save()
if(full){
delete brew.editId;
}
return brew;
};
HomebrewSchema.methods.increaseView = async function(){
this.lastViewed = new Date();
this.views = this.views + 1;
const text = this.text;
this.text = undefined;
await this.save()
.catch((err)=>{ .catch((err)=>{
return err; return err;
}); });
this.text = text; return brew;
return this;
}; };
HomebrewSchema.statics.get = function(query){ HomebrewSchema.statics.get = function(query){
@@ -58,7 +45,7 @@ HomebrewSchema.statics.get = function(query){
} }
if(!brews[0].renderer) if(!brews[0].renderer)
brews[0].renderer = 'legacy'; brews[0].renderer = 'legacy';
return resolve(brews[0]); return resolve(brews[0].toObject()); //Convert Mongo Object to JSObject
}); });
}); });
}; };
@@ -69,11 +56,9 @@ HomebrewSchema.statics.getByUser = function(username, allowAccess=false){
if(allowAccess){ if(allowAccess){
delete query.published; delete query.published;
} }
Homebrew.find(query, (err, brews)=>{ Homebrew.find(query).lean().exec((err, brews)=>{ //lean() converts results to JSObjects
if(err) return reject('Can not find brew'); if(err) return reject('Can not find brew');
return resolve(_.map(brews, (brew)=>{ return resolve(brews);
return brew.sanatize(!allowAccess);
}));
}); });
}); });
}; };

View File

@@ -11,28 +11,42 @@ if(typeof navigator !== 'undefined'){
//Language Modes //Language Modes
require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown
require('codemirror/mode/css/css.js');
require('codemirror/mode/javascript/javascript.js'); require('codemirror/mode/javascript/javascript.js');
} }
const CodeEditor = createClass({ const CodeEditor = createClass({
getDefaultProps : function() { getDefaultProps : function() {
return { return {
language : '', language : '',
value : '', value : '',
wrap : false, wrap : true,
onChange : function(){}, onChange : ()=>{}
onCursorActivity : function(){},
}; };
}, },
componentDidMount : function() { componentDidMount : function() {
this.buildEditor();
},
componentDidUpdate : function(prevProps) {
if(prevProps.language !== this.props.language){ //rebuild editor when switching tabs
this.buildEditor();
}
if(this.codeMirror && this.codeMirror.getValue() != this.props.value) { //update editor contents if brew.text is changed from outside
this.codeMirror.setValue(this.props.value);
}
},
buildEditor : function() {
this.codeMirror = CodeMirror(this.refs.editor, { this.codeMirror = CodeMirror(this.refs.editor, {
value : this.props.value, value : this.props.value,
lineNumbers : true, lineNumbers : true,
lineWrapping : this.props.wrap, lineWrapping : this.props.wrap,
mode : this.props.language, mode : this.props.language, //TODO: CSS MODE DOESN'T SEEM TO LOAD PROPERLY
extraKeys : { indentWithTabs : true,
tabSize : 2,
extraKeys : {
'Ctrl-B' : this.makeBold, 'Ctrl-B' : this.makeBold,
'Cmd-B' : this.makeBold, 'Cmd-B' : this.makeBold,
'Ctrl-I' : this.makeItalic, 'Ctrl-I' : this.makeItalic,
@@ -42,8 +56,8 @@ const CodeEditor = createClass({
} }
}); });
this.codeMirror.on('change', this.handleChange); // Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works.
this.codeMirror.on('cursorActivity', this.handleCursorActivity); this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue());});
this.updateSize(); this.updateSize();
}, },
@@ -74,29 +88,20 @@ const CodeEditor = createClass({
} }
}, },
componentDidUpdate : function(prevProps) { //=-- Externally used -==//
if(this.codeMirror && this.codeMirror.getValue() != this.props.value) {
this.codeMirror.setValue(this.props.value);
}
},
setCursorPosition : function(line, char){ setCursorPosition : function(line, char){
setTimeout(()=>{ setTimeout(()=>{
this.codeMirror.focus(); this.codeMirror.focus();
this.codeMirror.doc.setCursor(line, char); this.codeMirror.doc.setCursor(line, char);
}, 10); }, 10);
}, },
getCursorPosition : function(){
return this.codeMirror.getCursor();
},
updateSize : function(){ updateSize : function(){
this.codeMirror.refresh(); this.codeMirror.refresh();
}, },
//----------------------//
handleChange : function(editor){
this.props.onChange(editor.getValue());
},
handleCursorActivity : function(){
this.props.onCursorActivity(this.codeMirror.doc.getCursor());
},
render : function(){ render : function(){
return <div className='codeEditor' ref='editor' />; return <div className='codeEditor' ref='editor' />;