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

Update Production (#1249)

* Legacy renderer (#1184)

* Include two versions of Marked.js

* Include two versions of Marked.js

* Working two different render pipelines

Adds stylesheet "styleLegacy.less"
Adds markdownHandler "markdownLegacy.js"
The BrewRenderer will switch between these and the new pipeline dependent on the "version" prop passed in.

* Mustache-style div blocks

* Legacy snippets & columnbreak

* Codemirror styling for Div Blocks

* Lint

* Codemirror highlights for inline Divs as well

These will turn red `{{class Content}}`

Multi-line divs will turn purple

```
{{class,class2
content
}}
```

No real need for these to be different colors. Just for testing.

* More lint

* Update dependencies.

* Adding Button to switch render pipelines

* Update Marked.js

* Popup alert to refresh page when renderer changed

* Don't compress files in Development (very slow)

* Block DIV or inline Span depending on {{ placement

* \column emits a Div instead of Span

* Allow share page to use new renderer

* {{ divs no longer need empty lines. Spans work in lists.

* Typo

* Typo

* Enforce \page must be at start of line. Code cleanup.

* Inject newlines after/before {{/}} to avoid needing blank lines

* Fixes issues with tables.

* Remove console.log

* Fix spacing issue for Spans

* Move things from Brewrenderer to Markdown

Try to keep all custom text fiddling in one spot.

* Rename variables

* Update Font-Awesome to v5.15. Fix style issues on popups.

* Update {{ Divs/Spans, Fix nested hilighting

* Fixed Spans/divs with no tags or just commas

* Use blacklist for {{ to allow more characters

* Update package-lock.json

* Update all icons to Font-awesome 5

* V3 hidden behind config variable

Add "globalThis.enable_v3 = true" in the console to enable.

* lint

* Legacy renderer (#1229)

* Include two versions of Marked.js

* Include two versions of Marked.js

* Working two different render pipelines

Adds stylesheet "styleLegacy.less"
Adds markdownHandler "markdownLegacy.js"
The BrewRenderer will switch between these and the new pipeline dependent on the "version" prop passed in.

* Mustache-style div blocks

* Legacy snippets & columnbreak

* Codemirror styling for Div Blocks

* Lint

* Codemirror highlights for inline Divs as well

These will turn red `{{class Content}}`

Multi-line divs will turn purple

```
{{class,class2
content
}}
```

No real need for these to be different colors. Just for testing.

* More lint

* Update dependencies.

* Adding Button to switch render pipelines

* Update Marked.js

* Popup alert to refresh page when renderer changed

* Don't compress files in Development (very slow)

* Block DIV or inline Span depending on {{ placement

* \column emits a Div instead of Span

* Allow share page to use new renderer

* {{ divs no longer need empty lines. Spans work in lists.

* Typo

* Typo

* Enforce \page must be at start of line. Code cleanup.

* Inject newlines after/before {{/}} to avoid needing blank lines

* Fixes issues with tables.

* Remove console.log

* Fix spacing issue for Spans

* Move things from Brewrenderer to Markdown

Try to keep all custom text fiddling in one spot.

* Rename variables

* Update Font-Awesome to v5.15. Fix style issues on popups.

* Update {{ Divs/Spans, Fix nested hilighting

* Fixed Spans/divs with no tags or just commas

* Use blacklist for {{ to allow more characters

* Update package-lock.json

* Update all icons to Font-awesome 5

* V3 hidden behind config variable

Add "globalThis.enable_v3 = true" in the console to enable.

* lint

* Give user styles higher priority to still allow overrides

* Apply style priority to *all* user styles

* Change .legacy .v3 to .phb, .phb3

* Revert accidental color change

* Fix brew styles overwriting each other. (#1230)

* Fix /page not working in legacy mode. (#1233)

* Fix brew styles overwriting each other.

* Word wrapping, start fixing spacing on Title letter

* Fix \page in legacy brews when not at line start

* Default 'legacy' if not set. Auto-change styles.

* Fix brew styles overwriting each other.

* Word wrapping, start fixing spacing on Title letter

* Fix \page in legacy brews when not at line start

* Fix Page Padding

* Set 'legacy' as default value if not set in brew saved file.

* Apply Legacy\v3 renderer to print page (#1235)
This commit is contained in:
Trevor Buckner
2021-02-19 17:47:56 -05:00
committed by GitHub
parent 6a2e39355c
commit d57df84a59
53 changed files with 2068 additions and 190 deletions

View File

@@ -18,7 +18,7 @@ const Admin = createClass({
<header> <header>
<div className='container'> <div className='container'>
<i className='fa fa-rocket' /> <i className='fas fa-rocket' />
homebrewery admin homebrewery admin
</div> </div>
</header> </header>

View File

@@ -45,8 +45,8 @@ const BrewCleanup = createClass({
return <div className='removeBox'> return <div className='removeBox'>
<button onClick={this.cleanup} className='remove'> <button onClick={this.cleanup} className='remove'>
{this.state.pending {this.state.pending
? <i className='fa fa-spin fa-spinner' /> ? <i className='fas fa-spin fa-spinner' />
: <span><i className='fa fa-times' /> Remove</span> : <span><i className='fas fa-times' /> Remove</span>
} }
</button> </button>
<span>Found {this.state.count} Brews that could be removed. </span> <span>Found {this.state.count} Brews that could be removed. </span>
@@ -59,7 +59,7 @@ const BrewCleanup = createClass({
<button onClick={this.prime} className='query'> <button onClick={this.prime} className='query'>
{this.state.pending {this.state.pending
? <i className='fa fa-spin fa-spinner' /> ? <i className='fas fa-spin fa-spinner' />
: 'Query Brews' : 'Query Brews'
} }
</button> </button>

View File

@@ -59,8 +59,8 @@ const BrewCompress = createClass({
return <div className='removeBox'> return <div className='removeBox'>
<button onClick={this.cleanup} className='remove'> <button onClick={this.cleanup} className='remove'>
{this.state.pending {this.state.pending
? <i className='fa fa-spin fa-spinner' /> ? <i className='fas fa-spin fa-spinner' />
: <span><i className='fa fa-compress' /> compress </span> : <span><i className='fas fa-compress' /> compress </span>
} }
</button> </button>
{this.state.pending {this.state.pending
@@ -76,7 +76,7 @@ const BrewCompress = createClass({
<button onClick={this.prime} className='query'> <button onClick={this.prime} className='query'>
{this.state.pending {this.state.pending
? <i className='fa fa-spin fa-spinner' /> ? <i className='fas fa-spin fa-spinner' />
: 'Query Brews' : 'Query Brews'
} }
</button> </button>

View File

@@ -61,7 +61,7 @@ const BrewLookup = createClass({
<h2>Brew Lookup</h2> <h2>Brew Lookup</h2>
<input type='text' value={this.state.query} onChange={this.handleChange} placeholder='edit or share id' /> <input type='text' value={this.state.query} onChange={this.handleChange} placeholder='edit or share id' />
<button onClick={this.lookup}> <button onClick={this.lookup}>
<i className={cx('fa', { <i className={cx('fas', {
'fa-search' : !this.state.searching, 'fa-search' : !this.state.searching,
'fa-spin fa-spinner' : this.state.searching, 'fa-spin fa-spinner' : this.state.searching,
})} /> })} />

View File

@@ -37,7 +37,7 @@ const Stats = createClass({
</dl> </dl>
{this.state.fetching {this.state.fetching
&& <div className='pending'><i className='fa fa-spin fa-spinner' /></div> && <div className='pending'><i className='fas fa-spin fa-spinner' /></div>
} }
</div>; </div>;
} }

View File

@@ -4,6 +4,7 @@ const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
const Markdown = require('naturalcrit/markdown.js'); const Markdown = require('naturalcrit/markdown.js');
const ErrorBar = require('./errorBar/errorBar.jsx'); const ErrorBar = require('./errorBar/errorBar.jsx');
@@ -18,12 +19,18 @@ const PPR_THRESHOLD = 50;
const BrewRenderer = createClass({ const BrewRenderer = createClass({
getDefaultProps : function() { getDefaultProps : function() {
return { return {
text : '', text : '',
errors : [] renderer : 'legacy',
errors : []
}; };
}, },
getInitialState : function() { getInitialState : function() {
const pages = this.props.text.split('\\page'); let pages;
if(this.props.renderer == 'legacy') {
pages = this.props.text.split('\\page');
} else {
pages = this.props.text.split(/^\\page/gm);
}
return { return {
viewablePageNumber : 0, viewablePageNumber : 0,
@@ -34,7 +41,7 @@ const BrewRenderer = createClass({
usePPR : pages.length >= PPR_THRESHOLD, usePPR : pages.length >= PPR_THRESHOLD,
visibility : 'hidden', visibility : 'hidden',
initialContent : `<!DOCTYPE html><html><head> initialContent : `<!DOCTYPE html><html><head>
<link href="//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.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='/homebrew/bundle.css' rel='stylesheet' /> <link href='/homebrew/bundle.css' rel='stylesheet' />
<base target=_blank> <base target=_blank>
@@ -48,12 +55,19 @@ const BrewRenderer = createClass({
window.removeEventListener('resize', this.updateSize); window.removeEventListener('resize', this.updateSize);
}, },
componentWillReceiveProps : function(nextProps) { componentDidUpdate : function(prevProps) {
const pages = nextProps.text.split('\\page'); if(prevProps.text !== this.props.text) {
this.setState({ let pages;
pages : pages, if(this.props.renderer == 'legacy') {
usePPR : pages.length >= PPR_THRESHOLD pages = this.props.text.split('\\page');
}); } else {
pages = this.props.text.split(/^\\page/gm);
}
this.setState({
pages : pages,
usePPR : pages.length >= PPR_THRESHOLD
});
}
}, },
updateSize : function() { updateSize : function() {
@@ -103,12 +117,15 @@ const BrewRenderer = createClass({
renderDummyPage : function(index){ renderDummyPage : function(index){
return <div className='phb' id={`p${index + 1}`} key={index}> return <div className='phb' id={`p${index + 1}`} key={index}>
<i className='fa fa-spinner fa-spin' /> <i className='fas fa-spinner fa-spin' />
</div>; </div>;
}, },
renderPage : function(pageText, index){ renderPage : function(pageText, index){
return <div className='phb' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} key={index} />; if(this.props.renderer == 'legacy')
return <div className='phb' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }} key={index} />;
else
return <div className='phb3' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} key={index} />;
}, },
renderPages : function(){ renderPages : function(){
@@ -159,7 +176,7 @@ const BrewRenderer = createClass({
: null} : null}
<Frame initialContent={this.state.initialContent} style={{ width: '100%', height: '100%', visibility: this.state.visibility }} contentDidMount={this.frameDidMount}> <Frame initialContent={this.state.initialContent} style={{ width: '100%', height: '100%', visibility: this.state.visibility }} contentDidMount={this.frameDidMount}>
<div className='brewRenderer' <div className={'brewRenderer'}
onScroll={this.handleScroll} onScroll={this.handleScroll}
style={{ height: this.state.height }}> style={{ height: this.state.height }}>

View File

@@ -1,8 +1,7 @@
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
& {@import (multiple, less) './client/homebrew/phbStyle/phb.styleLegacy.less';} //&{} keeps internal variables locally-scoped
& {@import (multiple, less) './client/homebrew/phbStyle/phb.style.less';}
@import (less) './client/homebrew/phbStyle/phb.style.less';
.pane{
position : relative;
}
.brewRenderer{ .brewRenderer{
will-change : transform; will-change : transform;
overflow-y : scroll; overflow-y : scroll;
@@ -14,8 +13,17 @@
margin-left : auto; margin-left : auto;
box-shadow : 1px 4px 14px #000; box-shadow : 1px 4px 14px #000;
} }
&>.phb3{
margin-right : auto;
margin-bottom : 30px;
margin-left : auto;
box-shadow : 1px 4px 14px #000;
}
} }
} }
.pane{
position : relative;
}
.pageInfo{ .pageInfo{
position : absolute; position : absolute;
right : 17px; right : 17px;
@@ -37,4 +45,4 @@
font-size : 10px; font-size : 10px;
font-weight : 800; font-weight : 800;
color : white; color : white;
} }

View File

@@ -62,7 +62,7 @@ const ErrorBar = createClass({
if(!this.props.errors.length) return null; if(!this.props.errors.length) return null;
return <div className='errorBar'> return <div className='errorBar'>
<i className='fa fa-exclamation-triangle' /> <i className='fas fa-exclamation-triangle' />
<h3> There are HTML errors in your markup</h3> <h3> There are HTML errors in your markup</h3>
<small>If these aren't fixed your brew will not render properly when you print it to PDF or share it</small> <small>If these aren't fixed your brew will not render properly when you print it to PDF or share it</small>
{this.renderErrors()} {this.renderErrors()}

View File

@@ -60,8 +60,8 @@ const NotificationPopup = createClass({
if(_.isEmpty(this.state.notifications)) return null; if(_.isEmpty(this.state.notifications)) return null;
return <div className='notificationPopup'> return <div className='notificationPopup'>
<i className='fa fa-times dismiss' onClick={this.dismiss}/> <i className='fas fa-times dismiss' onClick={this.dismiss}/>
<i className='fa fa-info-circle info' /> <i className='fas fa-info-circle info' />
<h3>Notice</h3> <h3>Notice</h3>
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small> <small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
<ul>{_.values(this.state.notifications)}</ul> <ul>{_.values(this.state.notifications)}</ul>

View File

@@ -23,7 +23,8 @@ const Editor = createClass({
metadata : {}, metadata : {},
onMetadataChange : ()=>{}, onMetadataChange : ()=>{},
showMetaButton : true showMetaButton : true,
renderer : 'legacy'
}; };
}, },
getInitialState : function() { getInitialState : function() {
@@ -38,7 +39,7 @@ const Editor = createClass({
componentDidMount : function() { componentDidMount : function() {
this.updateEditorSize(); this.updateEditorSize();
this.highlightPageLines(); this.highlightCustomMarkdown();
window.addEventListener('resize', this.updateEditorSize); window.addEventListener('resize', this.updateEditorSize);
}, },
componentWillUnmount : function() { componentWillUnmount : function() {
@@ -78,15 +79,67 @@ const Editor = createClass({
}, 1); }, 1);
}, },
highlightPageLines : function(){ highlightCustomMarkdown : function(){
if(!this.refs.codeEditor) return; if(!this.refs.codeEditor) return;
const codeMirror = this.refs.codeEditor.codeMirror; const codeMirror = this.refs.codeEditor.codeMirror;
//reset custom text styles
const customHighlights = codeMirror.getAllMarks();
for (let i=0;i<customHighlights.length;i++) customHighlights[i].clear();
const lineNumbers = _.reduce(this.props.value.split('\n'), (r, line, lineNumber)=>{ const lineNumbers = _.reduce(this.props.value.split('\n'), (r, line, lineNumber)=>{
if(line.indexOf('\\page') !== -1){
codeMirror.addLineClass(lineNumber, 'background', 'pageLine'); //reset custom line styles
r.push(lineNumber); codeMirror.removeLineClass(lineNumber, 'background');
codeMirror.removeLineClass(lineNumber, 'text');
// Legacy Codemirror styling
if(this.props.renderer == 'legacy') {
if(line.includes('\\page')){
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
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.startsWith('\\column')){
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
r.push(lineNumber);
}
if(line.startsWith('{{') || line.startsWith('}}')){
let endCh = line.length+1;
const match = line.match(/{{(?:[\w,#-]|="[\w, ]*")*\s*|}}/);
if(match)
endCh = match.index+match[0].length;
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
}
if(line.includes('{{') && line.includes('}}')){
const regex = /{{(?:[\w,#-]|="[\w, ]*")*\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' });
}
}
}
return r; return r;
}, []); }, []);
return lineNumbers; return lineNumbers;
@@ -112,7 +165,7 @@ const Editor = createClass({
}, },
render : function(){ render : function(){
this.highlightPageLines(); this.highlightCustomMarkdown();
return ( return (
<div className='editor' ref='main'> <div className='editor' ref='main'>
<SnippetBar <SnippetBar
@@ -120,7 +173,8 @@ const Editor = createClass({
onInject={this.handleInject} onInject={this.handleInject}
onToggle={this.handgleToggle} onToggle={this.handgleToggle}
showmeta={this.state.showMetadataEditor} showmeta={this.state.showMetadataEditor}
showMetaButton={this.props.showMetaButton} /> showMetaButton={this.props.showMetaButton}
renderer={this.props.renderer} />
{this.renderMetadataEditor()} {this.renderMetadataEditor()}
<CodeEditor <CodeEditor
ref='codeEditor' ref='codeEditor'
@@ -132,7 +186,7 @@ const Editor = createClass({
{/* {/*
<div className='brewJump' onClick={this.brewJump}> <div className='brewJump' onClick={this.brewJump}>
<i className='fa fa-arrow-right' /> <i className='fas fa-arrow-right' />
</div> </div>
*/} */}
</div> </div>

View File

@@ -9,6 +9,22 @@
background-color : fade(#333, 15%); background-color : fade(#333, 15%);
border-bottom : #333 solid 1px; border-bottom : #333 solid 1px;
} }
.columnSplit{
font-style : italic;
color : grey;
background-color : fade(#299, 15%);
border-bottom : #299 solid 1px;
}
.block{
color : purple;
font-weight : bold;
//font-style: italic;
}
.inline-block{
color : red;
font-weight : bold;
//font-style: italic;
}
} }
.brewJump{ .brewJump{
@@ -26,4 +42,4 @@
.tooltipLeft("Jump to brew page"); .tooltipLeft("Jump to brew page");
} }
} }

View File

@@ -17,7 +17,8 @@ const MetadataEditor = createClass({
tags : '', tags : '',
published : false, published : false,
authors : [], authors : [],
systems : [] systems : [],
renderer : 'legacy'
}, },
onChange : ()=>{} onChange : ()=>{}
}; };
@@ -36,6 +37,12 @@ const MetadataEditor = createClass({
} }
this.props.onChange(this.props.metadata); this.props.onChange(this.props.metadata);
}, },
handleRenderer : function(renderer, e){
if(e.target.checked){
this.props.metadata.renderer = renderer;
}
this.props.onChange(this.props.metadata);
},
handlePublish : function(val){ handlePublish : function(val){
this.props.onChange(_.merge({}, this.props.metadata, { this.props.onChange(_.merge({}, this.props.metadata, {
published : val published : val
@@ -83,11 +90,11 @@ const MetadataEditor = createClass({
renderPublish : function(){ renderPublish : function(){
if(this.props.metadata.published){ if(this.props.metadata.published){
return <button className='unpublish' onClick={()=>this.handlePublish(false)}> return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
<i className='fa fa-ban' /> unpublish <i className='fas fa-ban' /> unpublish
</button>; </button>;
} else { } else {
return <button className='publish' onClick={()=>this.handlePublish(true)}> return <button className='publish' onClick={()=>this.handlePublish(true)}>
<i className='fa fa-globe' /> publish <i className='fas fa-globe' /> publish
</button>; </button>;
} }
}, },
@@ -99,7 +106,7 @@ const MetadataEditor = createClass({
<label>delete</label> <label>delete</label>
<div className='value'> <div className='value'>
<button className='publish' onClick={this.handleDelete}> <button className='publish' onClick={this.handleDelete}>
<i className='fa fa-trash' /> delete brew <i className='fas fa-trash-alt' /> delete brew
</button> </button>
</div> </div>
</div>; </div>;
@@ -126,13 +133,42 @@ const MetadataEditor = createClass({
<div className='value'> <div className='value'>
<a href={this.getRedditLink()} target='_blank' rel='noopener noreferrer'> <a href={this.getRedditLink()} target='_blank' rel='noopener noreferrer'>
<button className='publish'> <button className='publish'>
<i className='fa fa-reddit-alien' /> share to reddit <i className='fab fa-reddit-alien' /> share to reddit
</button> </button>
</a> </a>
</div> </div>
</div>; </div>;
}, },
renderRenderOptions : function(){
if(!global.enable_v3) return;
return <div className='field systems'>
<label>Renderer</label>
<div className='value'>
<label key='legacy'>
<input
type='radio'
value = 'legacy'
name = 'renderer'
checked={this.props.metadata.renderer === 'legacy'}
onChange={(e)=>this.handleRenderer('legacy', e)} />
Legacy
</label>
<label key='V3'>
<input
type='radio'
value = 'V3'
name = 'renderer'
checked={this.props.metadata.renderer === 'V3'}
onChange={(e)=>this.handleRenderer('V3', e)} />
V3
</label>
</div>
</div>;
},
render : function(){ render : function(){
return <div className='metadataEditor'> return <div className='metadataEditor'>
<div className='field title'> <div className='field title'>
@@ -154,6 +190,8 @@ const MetadataEditor = createClass({
</div> </div>
*/} */}
{this.renderAuthors()}
<div className='field systems'> <div className='field systems'>
<label>systems</label> <label>systems</label>
<div className='value'> <div className='value'>
@@ -161,7 +199,7 @@ const MetadataEditor = createClass({
</div> </div>
</div> </div>
{this.renderAuthors()} {this.renderRenderOptions()}
<div className='field publish'> <div className='field publish'>
<label>publish</label> <label>publish</label>

View File

@@ -5,7 +5,8 @@ const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const Snippets = require('./snippets/snippets.js'); const SnippetsLegacy = require('./snippetsLegacy/snippets.js');
const SnippetsV3 = require('./snippets/snippets.js');
const execute = function(val, brew){ const execute = function(val, brew){
if(_.isFunction(val)) return val(brew); if(_.isFunction(val)) return val(brew);
@@ -19,7 +20,14 @@ const Snippetbar = createClass({
onInject : ()=>{}, onInject : ()=>{},
onToggle : ()=>{}, onToggle : ()=>{},
showmeta : false, showmeta : false,
showMetaButton : true showMetaButton : true,
renderer : ''
};
},
getInitialState : function() {
return {
renderer : this.props.renderer
}; };
}, },
@@ -28,6 +36,11 @@ const Snippetbar = createClass({
}, },
renderSnippetGroups : function(){ renderSnippetGroups : function(){
if(this.props.renderer == 'V3')
Snippets = SnippetsV3;
else
Snippets = SnippetsLegacy;
return _.map(Snippets, (snippetGroup)=>{ return _.map(Snippets, (snippetGroup)=>{
return <SnippetGroup return <SnippetGroup
brew={this.props.brew} brew={this.props.brew}
@@ -44,7 +57,7 @@ const Snippetbar = createClass({
if(!this.props.showMetaButton) return; if(!this.props.showMetaButton) return;
return <div className={cx('snippetBarButton', 'toggleMeta', { selected: this.props.showmeta })} return <div className={cx('snippetBarButton', 'toggleMeta', { selected: this.props.showmeta })}
onClick={this.props.onToggle}> onClick={this.props.onToggle}>
<i className='fa fa-info-circle' /> <i className='fas fa-info-circle' />
<span className='groupName'>Properties</span> <span className='groupName'>Properties</span>
</div>; </div>;
}, },
@@ -69,7 +82,7 @@ const SnippetGroup = createClass({
return { return {
brew : '', brew : '',
groupName : '', groupName : '',
icon : 'fa-rocket', icon : 'fas fa-rocket',
snippets : [], snippets : [],
onSnippetClick : function(){}, onSnippetClick : function(){},
}; };
@@ -80,7 +93,7 @@ const SnippetGroup = createClass({
renderSnippets : function(){ renderSnippets : function(){
return _.map(this.props.snippets, (snippet)=>{ return _.map(this.props.snippets, (snippet)=>{
return <div className='snippet' key={snippet.name} onClick={()=>this.handleSnippetClick(snippet)}> return <div className='snippet' key={snippet.name} onClick={()=>this.handleSnippetClick(snippet)}>
<i className={`fa fa-fw ${snippet.icon}`} /> <i className={snippet.icon} />
{snippet.name} {snippet.name}
</div>; </div>;
}); });
@@ -89,7 +102,7 @@ const SnippetGroup = createClass({
render : function(){ render : function(){
return <div className='snippetGroup snippetBarButton'> return <div className='snippetGroup snippetBarButton'>
<div className='text'> <div className='text'>
<i className={`fa fa-fw ${this.props.icon}`} /> <i className={this.props.icon} />
<span className='groupName'>{this.props.groupName}</span> <span className='groupName'>{this.props.groupName}</span>
</div> </div>
<div className='dropdown'> <div className='dropdown'>

View File

@@ -12,73 +12,58 @@ module.exports = [
{ {
groupName : 'Editor', groupName : 'Editor',
icon : 'fa-pencil', icon : 'fas fa-pencil-alt',
snippets : [ snippets : [
{ {
name : 'Column Break', name : 'Column Break',
icon : 'fa-columns', icon : 'fas fa-columns',
gen : '```\n```\n\n' gen : '\n\\column\n'
}, },
{ {
name : 'New Page', name : 'New Page',
icon : 'fa-file-text', icon : 'fas fa-file-alt',
gen : '\\page\n\n' gen : '\n\\page\n'
}, },
{ {
name : 'Vertical Spacing', name : 'Vertical Spacing',
icon : 'fa-arrows-v', icon : 'fas fa-times-circle',
gen : '<div style=\'margin-top:140px\'></div>\n\n' gen : ''
}, },
{ {
name : 'Wide Block', name : 'Wide Block',
icon : 'fa-arrows-h', icon : 'fas fa-times-circle',
gen : '<div class=\'wide\'>\nEverything in here will be extra wide. Tables, text, everything! Beware though, CSS columns can behave a bit weird sometimes.\n</div>\n' gen : ''
}, },
{ {
name : 'Image', name : 'Image',
icon : 'fa-image', icon : 'fas fa-times-circle',
gen : [ gen : ''
'<img ',
' src=\'https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg\' ',
' style=\'width:325px\' />',
'Credit: Kyounghwan Kim'
].join('\n')
}, },
{ {
name : 'Background Image', name : 'Background Image',
icon : 'fa-tree', icon : 'fas fa-times-circle',
gen : [ gen : ''
'<img ',
' src=\'http://i.imgur.com/hMna6G0.png\' ',
' style=\'position:absolute; top:50px; right:30px; width:280px\' />'
].join('\n')
}, },
{ {
name : 'Page Number', name : 'Page Number',
icon : 'fa-bookmark', icon : 'fas fa-bookmark',
gen : '<div class=\'pageNumber\'>1</div>\n<div class=\'footnote\'>PART 1 | FANCINESS</div>\n\n' gen : '{{pageNumber\n1\n}}\n{{footnote\nPART 1 | FANCINESS\n}}\n\n'
}, },
{ {
name : 'Auto-incrementing Page Number', name : 'Auto-incrementing Page Number',
icon : 'fa-sort-numeric-asc', icon : 'fas fa-sort-numeric-down',
gen : '<div class=\'pageNumber auto\'></div>\n' gen : '{{\npageNumber,auto\n}}\n\n'
}, },
{ {
name : 'Link to page', name : 'Link to page',
icon : 'fa-link', icon : 'fas fa-link',
gen : '[Click here](#p3) to go to page 3\n' gen : '[Click here](#p3) to go to page 3\n'
}, },
{ {
name : 'Table of Contents', name : 'Table of Contents',
icon : 'fa-book', icon : 'fas fa-book',
gen : TableOfContentsGen gen : TableOfContentsGen
}, },
] ]
}, },
@@ -87,26 +72,26 @@ module.exports = [
{ {
groupName : 'PHB', groupName : 'PHB',
icon : 'fa-book', icon : 'fas fa-book',
snippets : [ snippets : [
{ {
name : 'Spell', name : 'Spell',
icon : 'fa-magic', icon : 'fas fa-magic',
gen : MagicGen.spell, gen : MagicGen.spell,
}, },
{ {
name : 'Spell List', name : 'Spell List',
icon : 'fa-list', icon : 'fas fa-scroll',
gen : MagicGen.spellList, gen : MagicGen.spellList,
}, },
{ {
name : 'Class Feature', name : 'Class Feature',
icon : 'fa-trophy', icon : 'fas fa-mask',
gen : ClassFeatureGen, gen : ClassFeatureGen,
}, },
{ {
name : 'Note', name : 'Note',
icon : 'fa-sticky-note', icon : 'fas fa-sticky-note',
gen : function(){ gen : function(){
return [ return [
'> ##### Time to Drop Knowledge', '> ##### Time to Drop Knowledge',
@@ -118,7 +103,7 @@ module.exports = [
}, },
{ {
name : 'Descriptive Text Box', name : 'Descriptive Text Box',
icon : 'fa-sticky-note-o', icon : 'fas fa-comment-alt',
gen : function(){ gen : function(){
return [ return [
'<div class=\'descriptive\'>', '<div class=\'descriptive\'>',
@@ -132,17 +117,17 @@ module.exports = [
}, },
{ {
name : 'Monster Stat Block', name : 'Monster Stat Block',
icon : 'fa-bug', icon : 'fas fa-spider',
gen : MonsterBlockGen.half, gen : MonsterBlockGen.half,
}, },
{ {
name : 'Wide Monster Stat Block', name : 'Wide Monster Stat Block',
icon : 'fa-paw', icon : 'fas fa-dragon',
gen : MonsterBlockGen.full, gen : MonsterBlockGen.full,
}, },
{ {
name : 'Cover Page', name : 'Cover Page',
icon : 'fa-file-word-o', icon : 'fas fa-file-word',
gen : CoverPageGen, gen : CoverPageGen,
}, },
] ]
@@ -154,21 +139,21 @@ module.exports = [
{ {
groupName : 'Tables', groupName : 'Tables',
icon : 'fa-table', icon : 'fas fa-table',
snippets : [ snippets : [
{ {
name : 'Class Table', name : 'Class Table',
icon : 'fa-table', icon : 'fas fa-table',
gen : ClassTableGen.full, gen : ClassTableGen.full,
}, },
{ {
name : 'Half Class Table', name : 'Half Class Table',
icon : 'fa-list-alt', icon : 'fas fa-list-alt',
gen : ClassTableGen.half, gen : ClassTableGen.half,
}, },
{ {
name : 'Table', name : 'Table',
icon : 'fa-th-list', icon : 'fas fa-th-list',
gen : function(){ gen : function(){
return [ return [
'##### Cookie Tastiness', '##### Cookie Tastiness',
@@ -184,7 +169,7 @@ module.exports = [
}, },
{ {
name : 'Wide Table', name : 'Wide Table',
icon : 'fa-list', icon : 'fas fa-list',
gen : function(){ gen : function(){
return [ return [
'<div class=\'wide\'>', '<div class=\'wide\'>',
@@ -202,7 +187,7 @@ module.exports = [
}, },
{ {
name : 'Split Table', name : 'Split Table',
icon : 'fa-th-large', icon : 'fas fa-th-large',
gen : function(){ gen : function(){
return [ return [
'<div style=\'column-count:2\'>', '<div style=\'column-count:2\'>',
@@ -238,11 +223,11 @@ module.exports = [
{ {
groupName : 'Print', groupName : 'Print',
icon : 'fa-print', icon : 'fas fa-print',
snippets : [ snippets : [
{ {
name : 'A4 PageSize', name : 'A4 PageSize',
icon : 'fa-file-o', icon : 'far fa-file',
gen : ['<style>', gen : ['<style>',
' .phb{', ' .phb{',
' width : 210mm;', ' width : 210mm;',
@@ -253,7 +238,7 @@ module.exports = [
}, },
{ {
name : 'Ink Friendly', name : 'Ink Friendly',
icon : 'fa-tint', icon : 'fas fa-tint',
gen : ['<style>', gen : ['<style>',
' .phb{ background : white;}', ' .phb{ background : white;}',
' .phb img{ display : none;}', ' .phb img{ display : none;}',

View File

@@ -0,0 +1,42 @@
const _ = require('lodash');
module.exports = function(classname){
classname = _.sample(['archivist', 'fancyman', 'linguist', 'fletcher',
'notary', 'berserker-typist', 'fishmongerer', 'manicurist', 'haberdasher', 'concierge']);
classname = classname.toLowerCase();
const hitDie = _.sample([4, 6, 8, 10, 12]);
const abilityList = ['Strength', 'Dexerity', 'Constitution', 'Wisdom', 'Charisma', 'Intelligence'];
const skillList = ['Acrobatics ', 'Animal Handling', 'Arcana', 'Athletics', 'Deception', 'History', 'Insight', 'Intimidation', 'Investigation', 'Medicine', 'Nature', 'Perception', 'Performance', 'Persuasion', 'Religion', 'Sleight of Hand', 'Stealth', 'Survival'];
return [
'## Class Features',
`As a ${classname}, you gain the following class features`,
'#### Hit Points',
'___',
`- **Hit Dice:** 1d${hitDie} per ${classname} level`,
`- **Hit Points at 1st Level:** ${hitDie} + your Constitution modifier`,
`- **Hit Points at Higher Levels:** 1d${hitDie} (or ${hitDie/2 + 1}) + your Constitution modifier per ${classname} level after 1st`,
'',
'#### Proficiencies',
'___',
`- **Armor:** ${_.sampleSize(['Light armor', 'Medium armor', 'Heavy armor', 'Shields'], _.random(0, 3)).join(', ') || 'None'}`,
`- **Weapons:** ${_.sampleSize(['Squeegee', 'Rubber Chicken', 'Simple weapons', 'Martial weapons'], _.random(0, 2)).join(', ') || 'None'}`,
`- **Tools:** ${_.sampleSize(['Artian\'s tools', 'one musical instrument', 'Thieve\'s tools'], _.random(0, 2)).join(', ') || 'None'}`,
'',
'___',
`- **Saving Throws:** ${_.sampleSize(abilityList, 2).join(', ')}`,
`- **Skills:** Choose two from ${_.sampleSize(skillList, _.random(4, 6)).join(', ')}`,
'',
'#### Equipment',
'You start with the following equipment, in addition to the equipment granted by your background:',
'- *(a)* a martial weapon and a shield or *(b)* two martial weapons',
'- *(a)* five javelins or *(b)* any simple melee weapon',
`- ${_.sample(['10 lint fluffs', '1 button', 'a cherished lost sock'])}`,
'\n\n\n'
].join('\n');
};

View File

@@ -0,0 +1,114 @@
const _ = require('lodash');
const features = [
'Astrological Botany',
'Astrological Chemistry',
'Biochemical Sorcery',
'Civil Alchemy',
'Consecrated Biochemistry',
'Demonic Anthropology',
'Divinatory Mineralogy',
'Genetic Banishing',
'Hermetic Geography',
'Immunological Incantations',
'Nuclear Illusionism',
'Ritual Astronomy',
'Seismological Divination',
'Spiritual Biochemistry',
'Statistical Occultism',
'Police Necromancer',
'Sixgun Poisoner',
'Pharmaceutical Gunslinger',
'Infernal Banker',
'Spell Analyst',
'Gunslinger Corruptor',
'Torque Interfacer',
'Exo Interfacer',
'Gunpowder Torturer',
'Orbital Gravedigger',
'Phased Linguist',
'Mathematical Pharmacist',
'Plasma Outlaw',
'Malefic Chemist',
'Police Cultist'
];
const classnames = ['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge'];
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 getFeature = (level)=>{
let res = [];
if(_.includes([4, 6, 8, 12, 14, 16, 19], level+1)){
res = ['Ability Score Improvement'];
}
res = _.union(res, _.sampleSize(features, _.sample([0, 1, 1, 1, 1, 1])));
if(!res.length) return '─';
return res.join(', ');
};
module.exports = {
full : function(){
const classname = _.sample(classnames);
const maxes = [4, 3, 3, 3, 3, 2, 2, 1, 1];
const drawSlots = function(Slots){
let slots = Number(Slots);
return _.times(9, function(i){
const max = maxes[i];
if(slots < 1) return '—';
const res = _.min([max, slots]);
slots -= res;
return res;
}).join(' | ');
};
let cantrips = 3;
let spells = 1;
let slots = 2;
return `<div class='classTable wide'>\n##### The ${classname}\n` +
`| Level | Proficiency Bonus | Features | Cantrips Known | Spells Known | 1st | 2nd | 3rd | 4th | 5th | 6th | 7th | 8th | 9th |\n`+
`|:---:|:---:|:---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n${
_.map(levels, function(levelName, level){
const res = [
levelName,
`+${profBonus[level]}`,
getFeature(level),
cantrips,
spells,
drawSlots(slots)
].join(' | ');
cantrips += _.random(0, 1);
spells += _.random(0, 1);
slots += _.random(0, 2);
return `| ${res} |`;
}).join('\n')}\n</div>\n\n`;
},
half : function(){
const classname = _.sample(classnames);
let featureScore = 1;
return `<div class='classTable'>\n##### The ${classname}\n` +
`| Level | Proficiency Bonus | Features | ${_.sample(features)}|\n` +
`|:---:|:---:|:---|:---:|\n${
_.map(levels, function(levelName, level){
const res = [
levelName,
`+${profBonus[level]}`,
getFeature(level),
`+${featureScore}`
].join(' | ');
featureScore += _.random(0, 1);
return `| ${res} |`;
}).join('\n')}\n</div>\n\n`;
}
};

View File

@@ -0,0 +1,117 @@
const _ = require('lodash');
const titles = [
'The Burning Gallows',
'The Ring of Nenlast',
'Below the Blind Tavern',
'Below the Hungering River',
'Before Bahamut\'s Land',
'The Cruel Grave from Within',
'The Strength of Trade Road',
'Through The Raven Queen\'s Worlds',
'Within the Settlement',
'The Crown from Within',
'The Merchant Within the Battlefield',
'Ioun\'s Fading Traveler',
'The Legion Ingredient',
'The Explorer Lure',
'Before the Charming Badlands',
'The Living Dead Above the Fearful Cage',
'Vecna\'s Hidden Sage',
'Bahamut\'s Demonspawn',
'Across Gruumsh\'s Elemental Chaos',
'The Blade of Orcus',
'Beyond Revenge',
'Brain of Insanity',
'Breed Battle!, A New Beginning',
'Evil Lake, A New Beginning',
'Invasion of the Gigantic Cat, Part II',
'Kraken War 2020',
'The Body Whisperers',
'The Diabolical Tales of the Ape-Women',
'The Doctor Immortal',
'The Doctor from Heaven',
'The Graveyard',
'Azure Core',
'Core Battle',
'Core of Heaven: The Guardian of Amazement',
'Deadly Amazement III',
'Dry Chaos IX',
'Gate Thunder',
'Guardian: Skies of the Dark Wizard',
'Lute of Eternity',
'Mercury\'s Planet: Brave Evolution',
'Ruby of Atlantis: The Quake of Peace',
'Sky of Zelda: The Thunder of Force',
'Vyse\'s Skies',
'White Greatness III',
'Yellow Divinity',
'Zidane\'s Ghost'
];
const subtitles = [
'In an ominous universe, a botanist opposes terrorism.',
'In a demon-haunted city, in an age of lies and hate, a physicist tries to find an ancient treasure and battles a mob of aliens.',
'In a land of corruption, two cyberneticists and a dungeon delver search for freedom.',
'In an evil empire of horror, two rangers battle the forces of hell.',
'In a lost city, in an age of sorcery, a librarian quests for revenge.',
'In a universe of illusions and danger, three time travellers and an adventurer search for justice.',
'In a forgotten universe of barbarism, in an era of terror and mysticism, a virtual reality programmer and a spy try to find vengance and battle crime.',
'In a universe of demons, in an era of insanity and ghosts, three bodyguards and a bodyguard try to find vengance.',
'In a kingdom of corruption and battle, seven artificial intelligences try to save the last living fertile woman.',
'In a universe of virutal reality and agony, in an age of ghosts and ghosts, a fortune-teller and a wanderer try to avert the apocalypse.',
'In a crime-infested kingdom, three martial artists quest for the truth and oppose evil.',
'In a terrifying universe of lost souls, in an era of lost souls, eight dancers fight evil.',
'In a galaxy of confusion and insanity, three martial artists and a duke battle a mob of psychics.',
'In an amazing kingdom, a wizard and a secretary hope to prevent the destruction of mankind.',
'In a kingdom of deception, a reporter searches for fame.',
'In a hellish empire, a swordswoman and a duke try to find the ultimate weapon and battle a conspiracy.',
'In an evil galaxy of illusion, in a time of technology and misery, seven psychiatrists battle crime.',
'In a dark city of confusion, three swordswomen and a singer battle lawlessness.',
'In an ominous empire, in an age of hate, two philosophers and a student try to find justice and battle a mob of mages intent on stealing the souls of the innocent.',
'In a kingdom of panic, six adventurers oppose lawlessness.',
'In a land of dreams and hopelessness, three hackers and a cyborg search for justice.',
'On a planet of mysticism, three travelers and a fire fighter quest for the ultimate weapon and oppose evil.',
'In a wicked universe, five seers fight lawlessness.',
'In a kingdom of death, in an era of illusion and blood, four colonists search for fame.',
'In an amazing kingdom, in an age of sorcery and lost souls, eight space pirates quest for freedom.',
'In a cursed empire, five inventors oppose terrorism.',
'On a crime-ridden planet of conspiracy, a watchman and an artificial intelligence try to find love and oppose lawlessness.',
'In a forgotten land, a reporter and a spy try to stop the apocalypse.',
'In a forbidden land of prophecy, a scientist and an archivist oppose a cabal of barbarians intent on stealing the souls of the innocent.',
'On an infernal world of illusion, a grave robber and a watchman try to find revenge and combat a syndicate of mages intent on stealing the source of all magic.',
'In a galaxy of dark magic, four fighters seek freedom.',
'In an empire of deception, six tomb-robbers quest for the ultimate weapon and combat an army of raiders.',
'In a kingdom of corruption and lost souls, in an age of panic, eight planetologists oppose evil.',
'In a galaxy of misery and hopelessness, in a time of agony and pain, five planetologists search for vengance.',
'In a universe of technology and insanity, in a time of sorcery, a computer techician quests for hope.',
'On a planet of dark magic and barbarism, in an age of horror and blasphemy, seven librarians search for fame.',
'In an empire of dark magic, in a time of blood and illusions, four monks try to find the ultimate weapon and combat terrorism.',
'In a forgotten empire of dark magic, six kings try to prevent the destruction of mankind.',
'In a galaxy of dark magic and horror, in an age of hopelessness, four marines and an outlaw combat evil.',
'In a mysterious city of illusion, in an age of computerization, a witch-hunter tries to find the ultimate weapon and opposes an evil corporation.',
'In a damned kingdom of technology, a virtual reality programmer and a fighter seek fame.',
'In a hellish kingdom, in an age of blasphemy and blasphemy, an astrologer searches for fame.',
'In a damned world of devils, an alien and a ranger quest for love and oppose a syndicate of demons.',
'In a cursed galaxy, in a time of pain, seven librarians hope to avert the apocalypse.',
'In a crime-infested galaxy, in an era of hopelessness and panic, three champions and a grave robber try to solve the ultimate crime.'
];
module.exports = ()=>{
return `<style>
.phb#p1{ text-align:center; }
.phb#p1:after{ display:none; }
</style>
<div style='margin-top:450px;'></div>
# ${_.sample(titles)}
<div style='margin-top:25px'></div>
<div class='wide'>
##### ${_.sample(subtitles)}
</div>
\\page`;
};

View File

@@ -0,0 +1,43 @@
const _ = require('lodash');
const ClassFeatureGen = require('./classfeature.gen.js');
const ClassTableGen = require('./classtable.gen.js');
module.exports = function(){
const classname = _.sample(['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge']);
const image = _.sample(_.map([
'http://orig01.deviantart.net/4682/f/2007/099/f/c/bard_stick_figure_by_wrpigeek.png',
'http://img07.deviantart.net/a3c9/i/2007/099/3/a/archer_stick_figure_by_wrpigeek.png',
'http://pre04.deviantart.net/d596/th/pre/f/2007/099/5/2/adventurer_stick_figure_by_wrpigeek.png',
'http://img13.deviantart.net/d501/i/2007/099/d/4/black_mage_stick_figure_by_wrpigeek.png',
'http://img09.deviantart.net/5cf3/i/2007/099/d/d/dark_knight_stick_figure_by_wrpigeek.png',
'http://pre01.deviantart.net/7a34/th/pre/f/2007/099/6/3/monk_stick_figure_by_wrpigeek.png',
'http://img11.deviantart.net/5dcc/i/2007/099/d/1/mystic_knight_stick_figure_by_wrpigeek.png',
'http://pre08.deviantart.net/ad45/th/pre/f/2007/099/a/0/thief_stick_figure_by_wrpigeek.png',
], function(url){
return `<img src = '${url}' style='max-width:8cm;max-height:25cm' />`;
}));
return `${[
image,
'',
'```',
'```',
'<div style=\'margin-top:240px\'></div>\n\n',
`## ${classname}`,
'Cool intro stuff will go here',
'\\page',
ClassTableGen(classname),
ClassFeatureGen(classname),
].join('\n')}\n\n\n`;
};

View File

@@ -0,0 +1,91 @@
const _ = require('lodash');
const spellNames = [
'Astral Rite of Acne',
'Create Acne',
'Cursed Ramen Erruption',
'Dark Chant of the Dentists',
'Erruption of Immaturity',
'Flaming Disc of Inconvenience',
'Heal Bad Hygene',
'Heavenly Transfiguration of the Cream Devil',
'Hellish Cage of Mucus',
'Irritate Peanut Butter Fairy',
'Luminous Erruption of Tea',
'Mystic Spell of the Poser',
'Sorcerous Enchantment of the Chimneysweep',
'Steak Sauce Ray',
'Talk to Groupie',
'Astonishing Chant of Chocolate',
'Astounding Pasta Puddle',
'Ball of Annoyance',
'Cage of Yarn',
'Control Noodles Elemental',
'Create Nervousness',
'Cure Baldness',
'Cursed Ritual of Bad Hair',
'Dispell Piles in Dentist',
'Eliminate Florists',
'Illusionary Transfiguration of the Babysitter',
'Necromantic Armor of Salad Dressing',
'Occult Transfiguration of Foot Fetish',
'Protection from Mucus Giant',
'Tinsel Blast',
'Alchemical Evocation of the Goths',
'Call Fangirl',
'Divine Spell of Crossdressing',
'Dominate Ramen Giant',
'Eliminate Vindictiveness in Gym Teacher',
'Extra-Planar Spell of Irritation',
'Induce Whining in Babysitter',
'Invoke Complaining',
'Magical Enchantment of Arrogance',
'Occult Globe of Salad Dressing',
'Overwhelming Enchantment of the Chocolate Fairy',
'Sorcerous Dandruff Globe',
'Spiritual Invocation of the Costumers',
'Ultimate Rite of the Confetti Angel',
'Ultimate Ritual of Mouthwash',
];
module.exports = {
spellList : function(){
const levels = ['Cantrips (0 Level)', '2nd Level', '3rd Level', '4th Level', '5th Level', '6th Level', '7th Level', '8th Level', '9th Level'];
const content = _.map(levels, (level)=>{
const spells = _.map(_.sampleSize(spellNames, _.random(5, 15)), (spell)=>{
return `- ${spell}`;
}).join('\n');
return `##### ${level} \n${spells} \n`;
}).join('\n');
return `<div class='spellList'>\n${content}\n</div>`;
},
spell : function(){
const level = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th', '8th', '9th'];
const spellSchools = ['abjuration', 'conjuration', 'divination', 'enchantment', 'evocation', 'illusion', 'necromancy', 'transmutation'];
let components = _.sampleSize(['V', 'S', 'M'], _.random(1, 3)).join(', ');
if(components.indexOf('M') !== -1){
components += ` (${_.sampleSize(['a small doll', 'a crushed button worth at least 1cp', 'discarded gum wrapper'], _.random(1, 3)).join(', ')})`;
}
return [
`#### ${_.sample(spellNames)}`,
`*${_.sample(level)}-level ${_.sample(spellSchools)}*`,
'___',
'- **Casting Time:** 1 action',
`- **Range:** ${_.sample(['Self', 'Touch', '30 feet', '60 feet'])}`,
`- **Components:** ${components}`,
`- **Duration:** ${_.sample(['Until dispelled', '1 round', 'Instantaneous', 'Concentration, up to 10 minutes', '1 hour'])}`,
'',
'A flame, equivalent in brightness to a torch, springs from from an object that you touch. ',
'The effect look like a regular flame, but it creates no heat and doesn\'t use oxygen. ',
'A *continual flame* can be covered or hidden but not smothered or quenched.',
'\n\n\n'
].join('\n');
}
};

View File

@@ -0,0 +1,200 @@
const _ = require('lodash');
const genList = function(list, max){
return _.sampleSize(list, _.random(0, max)).join(', ') || 'None';
};
const getMonsterName = function(){
return _.sample([
'All-devouring Baseball Imp',
'All-devouring Gumdrop Wraith',
'Chocolate Hydra',
'Devouring Peacock',
'Economy-sized Colossus of the Lemonade Stand',
'Ghost Pigeon',
'Gibbering Duck',
'Sparklemuffin Peacock Spider',
'Gum Elemental',
'Illiterate Construct of the Candy Store',
'Ineffable Chihuahua',
'Irritating Death Hamster',
'Irritating Gold Mouse',
'Juggernaut Snail',
'Juggernaut of the Sock Drawer',
'Koala of the Cosmos',
'Mad Koala of the West',
'Milk Djinni of the Lemonade Stand',
'Mind Ferret',
'Mystic Salt Spider',
'Necrotic Halitosis Angel',
'Pinstriped Famine Sheep',
'Ritalin Leech',
'Shocker Kangaroo',
'Stellar Tennis Juggernaut',
'Wailing Quail of the Sun',
'Angel Pigeon',
'Anime Sphinx',
'Bored Avalanche Sheep of the Wasteland',
'Devouring Nougat Sphinx of the Sock Drawer',
'Djinni of the Footlocker',
'Ectoplasmic Jazz Devil',
'Flatuent Angel',
'Gelatinous Duck of the Dream-Lands',
'Gelatinous Mouse',
'Golem of the Footlocker',
'Lich Wombat',
'Mechanical Sloth of the Past',
'Milkshake Succubus',
'Puffy Bone Peacock of the East',
'Rainbow Manatee',
'Rune Parrot',
'Sand Cow',
'Sinister Vanilla Dragon',
'Snail of the North',
'Spider of the Sewer',
'Stellar Sawdust Leech',
'Storm Anteater of Hell',
'Stupid Spirit of the Brewery',
'Time Kangaroo',
'Tomb Poodle',
]);
};
const getType = function(){
return `${_.sample(['Tiny', 'Small', 'Medium', 'Large', 'Gargantuan', 'Stupidly vast'])} ${_.sample(['beast', 'fiend', 'annoyance', 'guy', 'cutie'])}`;
};
const getAlignment = function(){
return _.sample([
'annoying evil',
'chaotic gossipy',
'chaotic sloppy',
'depressed neutral',
'lawful bogus',
'lawful coy',
'manic-depressive evil',
'narrow-minded neutral',
'neutral annoying',
'neutral ignorant',
'oedpipal neutral',
'silly neutral',
'unoriginal neutral',
'weird neutral',
'wordy evil',
'unaligned'
]);
};
const getStats = function(){
return `>|${_.times(6, function(){
const num = _.random(1, 20);
const mod = Math.ceil(num/2 - 5);
return `${num} (${mod >= 0 ? `+${mod}` : mod})`;
}).join('|')}|`;
};
const genAbilities = function(){
return _.sample([
'> ***Pack Tactics.*** These guys work together. Like super well, you don\'t even know.',
'> ***Fowl Appearance.*** While the creature remains motionless, it is indistinguishable from a normal chicken.',
'> ***Onion Stench.*** Any creatures within 5 feet of this thing develops an irrational craving for onion rings.',
'> ***Enormous Nose.*** This creature gains advantage on any check involving putting things in its nose.',
'> ***Sassiness.*** When questioned, this creature will talk back instead of answering.',
'> ***Big Jerk.*** Thinks he is just *waaaay* better than you.',
]);
};
const genAction = function(){
const name = _.sample([
'Abdominal Drop',
'Airplane Hammer',
'Atomic Death Throw',
'Bulldog Rake',
'Corkscrew Strike',
'Crossed Splash',
'Crossface Suplex',
'DDT Powerbomb',
'Dual Cobra Wristlock',
'Dual Throw',
'Elbow Hold',
'Gory Body Sweep',
'Heel Jawbreaker',
'Jumping Driver',
'Open Chin Choke',
'Scorpion Flurry',
'Somersault Stump Fists',
'Suffering Wringer',
'Super Hip Submission',
'Super Spin',
'Team Elbow',
'Team Foot',
'Tilt-a-whirl Chin Sleeper',
'Tilt-a-whirl Eye Takedown',
'Turnbuckle Roll'
]);
return `> ***${name}.*** *Melee Weapon Attack:* +4 to hit, reach 5ft., one target. *Hit* 5 (1d6 + 2) `;
};
module.exports = {
full : function(){
return `${[
'___',
'___',
`> ## ${getMonsterName()}`,
`>*${getType()}, ${getAlignment()}*`,
'> ___',
`> - **Armor Class** ${_.random(10, 20)}`,
`> - **Hit Points** ${_.random(1, 150)}(1d4 + 5)`,
`> - **Speed** ${_.random(0, 50)}ft.`,
'>___',
'>|STR|DEX|CON|INT|WIS|CHA|',
'>|:---:|:---:|:---:|:---:|:---:|:---:|',
getStats(),
'>___',
`> - **Condition Immunities** ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}`,
`> - **Senses** passive Perception ${_.random(3, 20)}`,
`> - **Languages** ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}`,
`> - **Challenge** ${_.random(0, 15)} (${_.random(10, 10000)} XP)`,
'> ___',
_.times(_.random(3, 6), function(){
return genAbilities();
}).join('\n>\n'),
'> ### Actions',
_.times(_.random(4, 6), function(){
return genAction();
}).join('\n>\n'),
].join('\n')}\n\n\n`;
},
half : function(){
return `${[
'___',
`> ## ${getMonsterName()}`,
`>*${getType()}, ${getAlignment()}*`,
'> ___',
`> - **Armor Class** ${_.random(10, 20)}`,
`> - **Hit Points** ${_.random(1, 150)}(1d4 + 5)`,
`> - **Speed** ${_.random(0, 50)}ft.`,
'>___',
'>|STR|DEX|CON|INT|WIS|CHA|',
'>|:---:|:---:|:---:|:---:|:---:|:---:|',
getStats(),
'>___',
`> - **Condition Immunities** ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}`,
`> - **Senses** passive Perception ${_.random(3, 20)}`,
`> - **Languages** ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}`,
`> - **Challenge** ${_.random(0, 15)} (${_.random(10, 10000)} XP)`,
'> ___',
_.times(_.random(2, 3), function(){
return genAbilities();
}).join('\n>\n'),
'> ### Actions',
_.times(_.random(1, 2), function(){
return genAction();
}).join('\n>\n'),
].join('\n')}\n\n\n`;
}
};

View File

@@ -0,0 +1,268 @@
/* eslint-disable max-lines */
const MagicGen = require('./magic.gen.js');
const ClassTableGen = require('./classtable.gen.js');
const MonsterBlockGen = require('./monsterblock.gen.js');
const ClassFeatureGen = require('./classfeature.gen.js');
const CoverPageGen = require('./coverpage.gen.js');
const TableOfContentsGen = require('./tableOfContents.gen.js');
module.exports = [
{
groupName : 'Editor',
icon : 'fas fa-pencil-alt',
snippets : [
{
name : 'Column Break',
icon : 'fas fa-columns',
gen : '```\n```\n\n'
},
{
name : 'New Page',
icon : 'fas fa-file-alt',
gen : '\\page\n\n'
},
{
name : 'Vertical Spacing',
icon : 'fas fa-arrows-alt-v',
gen : '<div style=\'margin-top:140px\'></div>\n\n'
},
{
name : 'Wide Block',
icon : 'fas fa-arrows-alt-h',
gen : '<div class=\'wide\'>\nEverything in here will be extra wide. Tables, text, everything! Beware though, CSS columns can behave a bit weird sometimes.\n</div>\n'
},
{
name : 'Image',
icon : 'fas fa-image',
gen : [
'<img ',
' src=\'https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg\' ',
' style=\'width:325px\' />',
'Credit: Kyounghwan Kim'
].join('\n')
},
{
name : 'Background Image',
icon : 'fas fa-tree',
gen : [
'<img ',
' src=\'http://i.imgur.com/hMna6G0.png\' ',
' style=\'position:absolute; top:50px; right:30px; width:280px\' />'
].join('\n')
},
{
name : 'Page Number',
icon : 'fas fa-bookmark',
gen : '<div class=\'pageNumber\'>1</div>\n<div class=\'footnote\'>PART 1 | FANCINESS</div>\n\n'
},
{
name : 'Auto-incrementing Page Number',
icon : 'fas fa-sort-numeric-down',
gen : '<div class=\'pageNumber auto\'></div>\n'
},
{
name : 'Link to page',
icon : 'fas fa-link',
gen : '[Click here](#p3) to go to page 3\n'
},
{
name : 'Table of Contents',
icon : 'fas fa-book',
gen : TableOfContentsGen
},
]
},
/************************* PHB ********************/
{
groupName : 'PHB',
icon : 'fas fa-book',
snippets : [
{
name : 'Spell',
icon : 'fas fa-magic',
gen : MagicGen.spell,
},
{
name : 'Spell List',
icon : 'fas fa-list',
gen : MagicGen.spellList,
},
{
name : 'Class Feature',
icon : 'fas fa-trophy',
gen : ClassFeatureGen,
},
{
name : 'Note',
icon : 'fas fa-sticky-note',
gen : function(){
return [
'> ##### Time to Drop Knowledge',
'> Use notes to point out some interesting information. ',
'> ',
'> **Tables and lists** both work within a note.'
].join('\n');
},
},
{
name : 'Descriptive Text Box',
icon : 'far fa-sticky-note',
gen : function(){
return [
'<div class=\'descriptive\'>',
'##### Time to Drop Knowledge',
'Use notes to point out some interesting information. ',
'',
'**Tables and lists** both work within a note.',
'</div>'
].join('\n');
},
},
{
name : 'Monster Stat Block',
icon : 'fas fa-bug',
gen : MonsterBlockGen.half,
},
{
name : 'Wide Monster Stat Block',
icon : 'fas fa-paw',
gen : MonsterBlockGen.full,
},
{
name : 'Cover Page',
icon : 'far fa-file-word',
gen : CoverPageGen,
},
]
},
/********************* TABLES *********************/
{
groupName : 'Tables',
icon : 'fas fa-table',
snippets : [
{
name : 'Class Table',
icon : 'fas fa-table',
gen : ClassTableGen.full,
},
{
name : 'Half Class Table',
icon : 'fas fa-list-alt',
gen : ClassTableGen.half,
},
{
name : 'Table',
icon : 'fas fa-th-list',
gen : function(){
return [
'##### Cookie Tastiness',
'| Tastiness | Cookie Type |',
'|:----:|:-------------|',
'| -5 | Raisin |',
'| 8th | Chocolate Chip |',
'| 11th | 2 or lower |',
'| 14th | 3 or lower |',
'| 17th | 4 or lower |\n\n',
].join('\n');
},
},
{
name : 'Wide Table',
icon : 'fas fa-list',
gen : function(){
return [
'<div class=\'wide\'>',
'##### Cookie Tastiness',
'| Tastiness | Cookie Type |',
'|:----:|:-------------|',
'| -5 | Raisin |',
'| 8th | Chocolate Chip |',
'| 11th | 2 or lower |',
'| 14th | 3 or lower |',
'| 17th | 4 or lower |',
'</div>\n\n'
].join('\n');
},
},
{
name : 'Split Table',
icon : 'fas fa-th-large',
gen : function(){
return [
'<div style=\'column-count:2\'>',
'| d10 | Damage Type |',
'|:---:|:------------|',
'| 1 | Acid |',
'| 2 | Cold |',
'| 3 | Fire |',
'| 4 | Force |',
'| 5 | Lightning |',
'',
'```',
'```',
'',
'| d10 | Damage Type |',
'|:---:|:------------|',
'| 6 | Necrotic |',
'| 7 | Poison |',
'| 8 | Psychic |',
'| 9 | Radiant |',
'| 10 | Thunder |',
'</div>\n\n',
].join('\n');
},
}
]
},
/**************** PRINT *************/
{
groupName : 'Print',
icon : 'fas fa-print',
snippets : [
{
name : 'A4 PageSize',
icon : 'far fa-file',
gen : ['<style>',
' .phb{',
' width : 210mm;',
' height : 296.8mm;',
' }',
'</style>'
].join('\n')
},
{
name : 'Ink Friendly',
icon : 'fas fa-tint',
gen : ['<style>',
' .phb{ background : white;}',
' .phb img{ display : none;}',
' .phb hr+blockquote{background : white;}',
'</style>',
''
].join('\n')
},
]
},
];

View File

@@ -0,0 +1,72 @@
const _ = require('lodash');
const getTOC = (pages)=>{
const add1 = (title, page)=>{
res.push({
title : title,
page : page + 1,
children : []
});
};
const add2 = (title, page)=>{
if(!_.last(res)) add1('', page);
_.last(res).children.push({
title : title,
page : page + 1,
children : []
});
};
const add3 = (title, page)=>{
if(!_.last(res)) add1('', page);
if(!_.last(_.last(res).children)) add2('', page);
_.last(_.last(res).children).children.push({
title : title,
page : page + 1,
children : []
});
};
const res = [];
_.each(pages, (page, pageNum)=>{
const lines = page.split('\n');
_.each(lines, (line)=>{
if(_.startsWith(line, '# ')){
const title = line.replace('# ', '');
add1(title, pageNum);
}
if(_.startsWith(line, '## ')){
const title = line.replace('## ', '');
add2(title, pageNum);
}
if(_.startsWith(line, '### ')){
const title = line.replace('### ', '');
add3(title, pageNum);
}
});
});
return res;
};
module.exports = function(brew){
const pages = brew.split('\\page');
const TOC = getTOC(pages);
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);
if(g1.children.length){
_.each(g1.children, (g2, idx2)=>{
r.push(` - [${idx1 + 1}.${idx2 + 1} ${g2.title}](#p${g2.page})`);
if(g2.children.length){
_.each(g2.children, (g3, idx3)=>{
r.push(` - [${idx1 + 1}.${idx2 + 1}.${idx3 + 1} ${g3.title}](#p${g3.page})`);
});
}
});
}
return r;
}, []).join('\n');
return `<div class='toc'>
##### Table Of Contents
${markdown}
</div>\n`;
};

View File

@@ -20,6 +20,7 @@ const Homebrew = createClass({
changelog : '', changelog : '',
version : '0.0.0', version : '0.0.0',
account : null, account : null,
enable_v3 : false,
brew : { brew : {
title : '', title : '',
text : '', text : '',
@@ -33,6 +34,7 @@ const Homebrew = createClass({
componentWillMount : function() { componentWillMount : function() {
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;
}, },
render : function (){ render : function (){

View File

@@ -20,12 +20,12 @@ const Account = createClass({
render : function(){ render : function(){
if(global.account){ if(global.account){
return <Nav.item href={`/user/${global.account.username}`} color='yellow' icon='fa-user'> return <Nav.item href={`/user/${global.account.username}`} color='yellow' icon='fas fa-user'>
{global.account.username} {global.account.username}
</Nav.item>; </Nav.item>;
} }
return <Nav.item href={`http://naturalcrit.com/login?redirect=${this.state.url}`} color='teal' icon='fa-sign-in'> return <Nav.item href={`http://naturalcrit.com/login?redirect=${this.state.url}`} color='teal' icon='fas fa-sign-in-alt'>
login login
</Nav.item>; </Nav.item>;
} }

View File

@@ -6,8 +6,8 @@ module.exports = function(props){
return <Nav.item return <Nav.item
newTab={true} newTab={true}
color='red' color='red'
icon='fa-bug' icon='fas fa-bug'
href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&title=${encodeURIComponent('[Issue] Describe Your Issue Here')}`} > href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&title=${encodeURIComponent('[Issue] Describe Your Issue Here')}`} >
report issue report issue
</Nav.item>; </Nav.item>;
}; };

View File

@@ -8,7 +8,7 @@ module.exports = function(props){
newTab={true} newTab={true}
href='https://www.patreon.com/NaturalCrit' href='https://www.patreon.com/NaturalCrit'
color='green' color='green'
icon='fa-heart'> icon='fas fa-heart'>
help out help out
</Nav.item>; </Nav.item>;
}; };

View File

@@ -3,7 +3,7 @@ const createClass = require('create-react-class');
const Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');
module.exports = function(props){ module.exports = function(props){
return <Nav.item newTab={true} href={`/print/${props.shareId}?dialog=true`} color='purple' icon='fa-file-pdf-o'> return <Nav.item newTab={true} href={`/print/${props.shareId}?dialog=true`} color='purple' icon='far fa-file-pdf'>
get PDF get PDF
</Nav.item>; </Nav.item>;
}; };

View File

@@ -143,7 +143,7 @@ const RecentItems = createClass({
}, },
render : function(){ render : function(){
return <Nav.item icon='fa-clock-o' color='grey' className='recent' return <Nav.item icon='fas fa-history' color='grey' className='recent'
onMouseEnter={()=>this.handleDropdown(true)} onMouseEnter={()=>this.handleDropdown(true)}
onMouseLeave={()=>this.handleDropdown(false)}> onMouseLeave={()=>this.handleDropdown(false)}>
{this.props.text} {this.props.text}

View File

@@ -41,17 +41,18 @@ const EditPage = createClass({
tags : '', tags : '',
published : false, published : false,
authors : [], authors : [],
systems : [] systems : [],
renderer : 'legacy'
} }
}; };
}, },
getInitialState : function() { getInitialState : function() {
return { return {
brew : this.props.brew, brew : this.props.brew,
isSaving : false, isSaving : false,
isPending : false, isPending : false,
alertRenderChange : false,
saveGoogle : this.props.brew.googleId ? true : false, saveGoogle : this.props.brew.googleId ? true : false,
confirmGoogleTransfer : false, confirmGoogleTransfer : false,
errors : null, errors : null,
@@ -66,6 +67,8 @@ const EditPage = createClass({
url : window.location.href url : window.location.href
}); });
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
this.trySave(); this.trySave();
window.onbeforeunload = ()=>{ window.onbeforeunload = ()=>{
if(this.state.isSaving || this.state.isPending){ if(this.state.isSaving || this.state.isPending){
@@ -101,6 +104,11 @@ const EditPage = createClass({
}, },
handleMetadataChange : function(metadata){ handleMetadataChange : function(metadata){
if(metadata.renderer != this.savedBrew.renderer){
this.setState({
alertRenderChange : true
});
}
this.setState((prevState)=>({ this.setState((prevState)=>({
brew : _.merge({}, prevState.brew, metadata), brew : _.merge({}, prevState.brew, metadata),
isPending : true, isPending : true,
@@ -122,8 +130,7 @@ const EditPage = createClass({
}, },
hasChanges : function(){ hasChanges : function(){
const savedBrew = this.savedBrew ? this.savedBrew : this.props.brew; return !_.isEqual(this.state.brew, this.savedBrew);
return !_.isEqual(this.state.brew, savedBrew);
}, },
trySave : function(){ trySave : function(){
@@ -142,6 +149,12 @@ const EditPage = createClass({
this.clearErrors(); this.clearErrors();
}, },
closeAlerts : function(){
this.setState({
alertRenderChange : false
});
},
toggleGoogleStorage : function(){ toggleGoogleStorage : function(){
this.setState((prevState)=>({ this.setState((prevState)=>({
saveGoogle : !prevState.saveGoogle, saveGoogle : !prevState.saveGoogle,
@@ -294,7 +307,7 @@ const EditPage = createClass({
} catch (e){} } catch (e){}
if(this.state.errors.status == '401'){ if(this.state.errors.status == '401'){
return <Nav.item className='save error' icon='fa-warning'> return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops! Oops!
<div className='errorContainer' onClick={this.clearErrors}> <div className='errorContainer' onClick={this.clearErrors}>
You must be signed in to a Google account You must be signed in to a Google account
@@ -312,7 +325,7 @@ const EditPage = createClass({
</Nav.item>; </Nav.item>;
} }
return <Nav.item className='save error' icon='fa-warning'> return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops! Oops!
<div className='errorContainer'> <div className='errorContainer'>
Looks like there was a problem saving. <br /> Looks like there was a problem saving. <br />
@@ -325,16 +338,25 @@ const EditPage = createClass({
} }
if(this.state.isSaving){ if(this.state.isSaving){
return <Nav.item className='save' icon='fa-spinner fa-spin'>saving...</Nav.item>; return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
} }
if(this.state.isPending && this.hasChanges()){ if(this.state.isPending && this.hasChanges()){
return <Nav.item className='save' onClick={this.save} color='blue' icon='fa-save'>Save Now</Nav.item>; return <Nav.item className='save' onClick={this.save} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
} }
if(!this.state.isPending && !this.state.isSaving){ if(!this.state.isPending && !this.state.isSaving){
return <Nav.item className='save saved'>saved.</Nav.item>; return <Nav.item className='save saved'>saved.</Nav.item>;
} }
}, },
// {this.state.alertRenderChange &&
// <div className='errorContainer' onClick={this.closeAlerts}>
// Rendering mode for this brew has been changed! Refresh the page to load the new renderer.<br />
// <div className='confirm'>
// OK
// </div>
// </div>
// }
processShareId : function() { processShareId : function() {
return this.state.brew.googleId ? return this.state.brew.googleId ?
this.state.brew.googleId + this.state.brew.shareId : this.state.brew.googleId + this.state.brew.shareId :
@@ -351,7 +373,7 @@ const EditPage = createClass({
{this.renderGoogleDriveIcon()} {this.renderGoogleDriveIcon()}
{this.renderSaveButton()} {this.renderSaveButton()}
<ReportIssue /> <ReportIssue />
<Nav.item newTab={true} href={`/share/${this.processShareId()}`} color='teal' icon='fa-share-alt'> <Nav.item newTab={true} href={`/share/${this.processShareId()}`} color='teal' icon='fas fa-share-alt'>
Share Share
</Nav.item> </Nav.item>
<PrintLink shareId={this.processShareId()} /> <PrintLink shareId={this.processShareId()} />
@@ -374,8 +396,9 @@ const EditPage = createClass({
onChange={this.handleTextChange} onChange={this.handleTextChange}
metadata={this.state.brew} metadata={this.state.brew}
onMetadataChange={this.handleMetadataChange} onMetadataChange={this.handleMetadataChange}
renderer={this.state.brew.renderer}
/> />
<BrewRenderer text={this.state.brew.text} errors={this.state.htmlErrors} /> <BrewRenderer text={this.state.brew.text} errors={this.state.htmlErrors} renderer={this.state.brew.renderer} />
</SplitPane> </SplitPane>
</div> </div>
</div>; </div>;

View File

@@ -1,8 +1,14 @@
@keyframes glideDown {
0% {transform : translate(-50% + 3px, 0px);
opacity : 0;}
100% {transform : translate(-50% + 3px, 10px);
opacity : 1;}
}
.editPage{ .editPage{
.navItem.save{ .navItem.save{
width : 106px; width : 106px;
text-align : center; text-align : center;
position : relative;
&.saved{ &.saved{
cursor : initial; cursor : initial;
color : #666; color : #666;
@@ -21,12 +27,15 @@
margin : -5px; margin : -5px;
} }
.errorContainer{ .errorContainer{
animation-name: glideDown;
animation-duration: 0.4s;
position : absolute; position : absolute;
top : 100%; top : 100%;
left : 50%; left : 50%;
z-index : 1000; z-index : 100000;
width : 140px; width : 140px;
padding : 3px; padding : 3px;
color : white;
background-color : #333; background-color : #333;
border : 3px solid #444; border : 3px solid #444;
border-radius : 5px; border-radius : 5px;

View File

@@ -55,7 +55,7 @@ const HomePage = createClass({
return <Navbar ver={this.props.ver}> return <Navbar ver={this.props.ver}>
<Nav.section> <Nav.section>
<IssueNavItem /> <IssueNavItem />
<Nav.item newTab={true} href='/changelog' color='purple' icon='fa-file-text-o'> <Nav.item newTab={true} href='/changelog' color='purple' icon='far fa-file-alt'>
Changelog Changelog
</Nav.item> </Nav.item>
<RecentNavItem /> <RecentNavItem />
@@ -77,11 +77,11 @@ const HomePage = createClass({
</div> </div>
<div className={cx('floatingSaveButton', { show: this.props.welcomeText != this.state.text })} onClick={this.handleSave}> <div className={cx('floatingSaveButton', { show: this.props.welcomeText != this.state.text })} onClick={this.handleSave}>
Save current <i className='fa fa-save' /> Save current <i className='fas fa-save' />
</div> </div>
<a href='/new' className='floatingNewButton'> <a href='/new' className='floatingNewButton'>
Create your own <i className='fa fa-magic' /> Create your own <i className='fas fa-magic' />
</a> </a>
</div>; </div>;
} }

View File

@@ -127,11 +127,11 @@ const NewPage = createClass({
renderSaveButton : function(){ renderSaveButton : function(){
if(this.state.isSaving){ if(this.state.isSaving){
return <Nav.item icon='fa-spinner fa-spin' className='saveButton'> return <Nav.item icon='fas fa-spinner fa-spin' className='saveButton'>
save... save...
</Nav.item>; </Nav.item>;
} else { } else {
return <Nav.item icon='fa-save' className='saveButton' onClick={this.save}> return <Nav.item icon='fas fa-save' className='saveButton' onClick={this.save}>
save save
</Nav.item>; </Nav.item>;
} }
@@ -143,7 +143,7 @@ const NewPage = createClass({
}, },
renderLocalPrintButton : function(){ renderLocalPrintButton : function(){
return <Nav.item color='purple' icon='fa-file-pdf-o' onClick={this.print}> return <Nav.item color='purple' icon='far fa-file-pdf' onClick={this.print}>
get PDF get PDF
</Nav.item>; </Nav.item>;
}, },

View File

@@ -4,6 +4,7 @@ const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const { Meta } = require('vitreum/headtags'); const { Meta } = require('vitreum/headtags');
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
const Markdown = require('naturalcrit/markdown.js'); const Markdown = require('naturalcrit/markdown.js');
const PrintPage = createClass({ const PrintPage = createClass({
@@ -11,7 +12,8 @@ const PrintPage = createClass({
return { return {
query : {}, query : {},
brew : { brew : {
text : '', text : '',
renderer : 'legacy'
} }
}; };
}, },
@@ -33,13 +35,24 @@ const PrintPage = createClass({
}, },
renderPages : function(){ renderPages : function(){
return _.map(this.state.brewText.split('\\page'), (page, index)=>{ if(this.props.brew.renderer == 'legacy') {
return <div return _.map(this.state.brewText.split('\\page'), (page, index)=>{
className='phb' return <div
id={`p${index + 1}`} className='phb'
dangerouslySetInnerHTML={{ __html: Markdown.render(page) }} id={`p${index + 1}`}
key={index} />; dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(page) }}
}); key={index} />;
});
} else {
return _.map(this.state.brewText.split(/^\\page/gm), (page, index)=>{
return <div
className='phb3'
id={`p${index + 1}`}
dangerouslySetInnerHTML={{ __html: Markdown.render(page) }}
key={index} />;
});
}
}, },
render : function(){ render : function(){

View File

@@ -22,7 +22,8 @@ const SharePage = createClass({
shareId : null, shareId : null,
createdAt : null, createdAt : null,
updatedAt : null, updatedAt : null,
views : 0 views : 0,
renderer : ''
} }
}; };
}, },
@@ -59,7 +60,7 @@ const SharePage = createClass({
<Nav.section> <Nav.section>
<PrintLink shareId={this.processShareId()} /> <PrintLink shareId={this.processShareId()} />
<Nav.item href={`/source/${this.processShareId()}`} color='teal' icon='fa-code'> <Nav.item href={`/source/${this.processShareId()}`} color='teal' icon='fas fa-code'>
source source
</Nav.item> </Nav.item>
<RecentNavItem brew={this.props.brew} storageKey='view' /> <RecentNavItem brew={this.props.brew} storageKey='view' />
@@ -68,7 +69,7 @@ const SharePage = createClass({
</Navbar> </Navbar>
<div className='content'> <div className='content'>
<BrewRenderer text={this.props.brew.text} /> <BrewRenderer text={this.props.brew.text} renderer={this.props.brew.renderer} />
</div> </div>
</div>; </div>;
} }

View File

@@ -48,7 +48,7 @@ const BrewItem = createClass({
if(!this.props.brew.editId) return; if(!this.props.brew.editId) return;
return <a onClick={this.deleteBrew}> return <a onClick={this.deleteBrew}>
<i className='fa fa-trash' /> <i className='fas fa-trash-alt' />
</a>; </a>;
}, },
@@ -61,7 +61,7 @@ const BrewItem = createClass({
} }
return <a href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'> return <a href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'>
<i className='fa fa-pencil' /> <i className='fas fa-pencil-alt' />
</a>; </a>;
}, },
@@ -74,7 +74,7 @@ const BrewItem = createClass({
} }
return <a href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'> return <a href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
<i className='fa fa-share-alt' /> <i className='fas fa-share-alt' />
</a>; </a>;
}, },
@@ -95,13 +95,13 @@ const BrewItem = createClass({
<div className='info'> <div className='info'>
<span> <span>
<i className='fa fa-user' /> {brew.authors.join(', ')} <i className='fas fa-user' /> {brew.authors.join(', ')}
</span> </span>
<span> <span>
<i className='fa fa-eye' /> {brew.views} <i className='fas fa-eye' /> {brew.views}
</span> </span>
<span> <span>
<i className='fa fa-refresh' /> {moment(brew.updatedAt).fromNow()} <i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
</span> </span>
{this.renderGoogleDriveIcon()} {this.renderGoogleDriveIcon()}
</div> </div>

View File

@@ -22,6 +22,9 @@
font-size : 2.2em; font-size : 2.2em;
} }
.info{ .info{
position: absolute;
bottom: 0px;
margin-bottom: 4px;
font-family : ScalySans; font-family : ScalySans;
font-size : 1.2em; font-size : 1.2em;
&>span{ &>span{

View File

@@ -59,7 +59,7 @@ const UserPage = createClass({
</Nav.section> </Nav.section>
</Navbar> </Navbar>
<div className='content'> <div className='content V3'>
<div className='phb'> <div className='phb'>
<div> <div>
<h1>{this.getUsernameWithS()} brews</h1> <h1>{this.getUsernameWithS()} brews</h1>

View File

@@ -1,7 +1,6 @@
@import (less) 'shared/naturalcrit/styles/reset.less';
@import (less) './client/homebrew/phbStyle/phb.fonts.css'; @import (less) './client/homebrew/phbStyle/phb.fonts.css';
@import (less) './client/homebrew/phbStyle/phb.assets.less'; @import (less) './client/homebrew/phbStyle/phb.assets.less';
@import (less) './client/homebrew/phbStyle/phb.depricated.less';
//Colors //Colors
@background : #EEE5CE; @background : #EEE5CE;
@noteGreen : #e0e5c1; @noteGreen : #e0e5c1;
@@ -18,12 +17,11 @@ body {
} }
.useSansSerif(){ .useSansSerif(){
font-family : ScalySans; font-family : ScalySans;
font-size : 10pt;
em{ em{
font-family : ScalySans;
font-style : italic; font-style : italic;
} }
strong{ strong{
font-family : ScalySans;
font-weight : 800; font-weight : 800;
letter-spacing : -0.02em; letter-spacing : -0.02em;
} }
@@ -40,7 +38,7 @@ body {
-webkit-column-gap : 1cm; -webkit-column-gap : 1cm;
-moz-column-gap : 1cm; -moz-column-gap : 1cm;
} }
.phb{ .phb3{
.useColumns(); .useColumns();
counter-increment : phb-page-numbers; counter-increment : phb-page-numbers;
position : relative; position : relative;
@@ -49,8 +47,7 @@ body {
overflow : hidden; overflow : hidden;
height : 279.4mm; height : 279.4mm;
width : 215.9mm; width : 215.9mm;
padding : 1.0cm 1.7cm; padding : 1.0cm 1.7cm 1.5cm;
padding-bottom : 1.5cm;
background-color : @background; background-color : @background;
background-image : @backgroundImage; background-image : @backgroundImage;
font-family : BookSanity; font-family : BookSanity;
@@ -62,6 +59,7 @@ body {
// * BASE // * BASE
// *****************************/ // *****************************/
p{ p{
overflow-wrap : break-word;
padding-bottom : 0.8em; padding-bottom : 0.8em;
line-height : 1.3em; line-height : 1.3em;
&+p{ &+p{
@@ -124,9 +122,20 @@ body {
&+p::first-letter{ &+p::first-letter{
float : left; float : left;
font-family : Solberry; font-family : Solberry;
font-size : 10em;
color : #222;
line-height : 0.8em; line-height : 0.8em;
font-size: 3.1cm;
padding-top: 4px;
padding-bottom: 4px;
margin-top: -6px;
margin-left: -6px;
background-image: linear-gradient(-45deg, #322814, #998250, #322814);
background-clip: text;
-webkit-background-clip: text;
color: rgba(0, 0, 0, 0);
}
&+p::first-line{
font-size : .385cm;
font-variant : small-caps;
} }
} }
h2{ h2{
@@ -327,12 +336,12 @@ body {
text-indent : -1em; text-indent : -1em;
list-style-type : none; list-style-type : none;
} }
//Column Break .columnSplit {
pre, code{
visibility : hidden; visibility : hidden;
-webkit-column-break-after : always; -webkit-column-break-after : always;
break-after : always; break-after : always;
-moz-column-break-after : always; -moz-column-break-after : always;
break-before : column;
} }
//Avoid breaking up //Avoid breaking up
p,blockquote,table{ p,blockquote,table{
@@ -363,7 +372,7 @@ body {
//***************************** //*****************************
// * SPELL LIST // * SPELL LIST
// *****************************/ // *****************************/
.phb .spellList{ .phb3 .spellList{
.useSansSerif(); .useSansSerif();
column-count : 4; column-count : 4;
column-span : all; column-span : all;
@@ -389,7 +398,7 @@ body {
//***************************** //*****************************
// * WIDE // * WIDE
// *****************************/ // *****************************/
.phb .wide{ .phb3 .wide{
column-span : all; column-span : all;
-webkit-column-span : all; -webkit-column-span : all;
-moz-column-span : all; -moz-column-span : all;
@@ -397,7 +406,7 @@ body {
//***************************** //*****************************
// * CLASS TABLE // * CLASS TABLE
// *****************************/ // *****************************/
.phb .classTable{ .phb3 .classTable{
margin-top : 25px; margin-top : 25px;
margin-bottom : 40px; margin-bottom : 40px;
border-collapse : separate; border-collapse : separate;
@@ -416,7 +425,7 @@ body {
//************************************ //************************************
// * DESCRIPTIVE TEXT BOX // * DESCRIPTIVE TEXT BOX
// ************************************/ // ************************************/
.phb .descriptive{ .phb3 .descriptive{
display : block-inline; display : block-inline;
margin-bottom : 1em; margin-bottom : 1em;
background-color : #faf7ea; background-color : #faf7ea;
@@ -444,13 +453,13 @@ body {
letter-spacing : -0.02em; letter-spacing : -0.02em;
} }
} }
.phb pre+.descriptive{ .phb3 pre+.descriptive{
margin-top : 8px; margin-top : 8px;
} }
//***************************** //*****************************
// * TABLE OF CONTENTS // * TABLE OF CONTENTS
// *****************************/ // *****************************/
.phb .toc{ .phb3 .toc{
-webkit-column-break-inside : avoid; -webkit-column-break-inside : avoid;
page-break-inside : avoid; page-break-inside : avoid;
break-inside : avoid; break-inside : avoid;

View File

@@ -0,0 +1,469 @@
@import (less) './client/homebrew/phbStyle/phb.fonts.css';
@import (less) './client/homebrew/phbStyle/phb.assets.less';
@import (less) './client/homebrew/phbStyle/phb.depricated.less';
//Colors
@background : #EEE5CE;
@noteGreen : #e0e5c1;
@headerUnderline : #c9ad6a;
@horizontalRule : #9c2b1b;
@headerText : #58180D;
@monsterStatBackground : #FDF1DC;
@page { margin: 0; }
body {
counter-reset : phb-page-numbers;
}
*{
-webkit-print-color-adjust : exact;
}
.useSansSerif(){
font-family : ScalySans;
em{
font-family : ScalySans;
font-style : italic;
}
strong{
font-family : ScalySans;
font-weight : 800;
letter-spacing : -0.02em;
}
}
.useColumns(@multiplier : 1){
column-count : 2;
column-fill : auto;
column-gap : 1cm;
column-width : 8cm * @multiplier;
-webkit-column-count : 2;
-moz-column-count : 2;
-webkit-column-width : 8cm * @multiplier;
-moz-column-width : 8cm * @multiplier;
-webkit-column-gap : 1cm;
-moz-column-gap : 1cm;
}
.phb{
.useColumns();
counter-increment : phb-page-numbers;
position : relative;
z-index : 15;
box-sizing : border-box;
overflow : hidden;
height : 279.4mm;
width : 215.9mm;
padding : 1.0cm 1.7cm;
padding-bottom : 1.5cm;
background-color : @background;
background-image : @backgroundImage;
font-family : BookSanity;
font-size : 0.317cm;
text-rendering : optimizeLegibility;
page-break-before : always;
page-break-after : always;
//*****************************
// * BASE
// *****************************/
p{
padding-bottom : 0.8em;
line-height : 1.3em;
&+p{
margin-top : -0.8em;
}
}
ul{
margin-bottom : 0.8em;
padding-left : 1.4em;
line-height : 1.3em;
list-style-position : outside;
list-style-type : disc;
}
ol{
margin-bottom : 0.8em;
padding-left : 1.4em;
line-height : 1.3em;
list-style-position : outside;
list-style-type : decimal;
}
//Indents after p or lists
p+p, ul+p, ol+p{
text-indent : 1em;
}
img{
z-index : -1;
}
strong{
font-weight : bold;
letter-spacing : 0.03em;
}
em{
font-style : italic;
}
sup{
vertical-align : super;
font-size : smaller;
line-height : 0;
}
sub{
vertical-align : sub;
font-size : smaller;
line-height : 0;
}
//*****************************
// * HEADERS
// *****************************/
h1,h2,h3,h4{
margin-top : 0.2em;
margin-bottom : 0.2em;
font-family : MrJeeves;
font-weight : 800;
color : @headerText;
}
h1{
column-span : all;
font-size : 0.987cm;
-webkit-column-span : all;
-moz-column-span : all;
&+p::first-letter{
float : left;
font-family : Solberry;
font-size : 10em;
color : #222;
line-height : 0.8em;
}
}
h2{
font-size : 0.705cm;
}
h3{
font-size : 0.529cm;
border-bottom : 2px solid @headerUnderline;
}
h4{
margin-bottom : 0.00em;
font-size : 0.458cm;
}
h5{
margin-bottom : 0.2em;
font-family : ScalySansSmallCaps;
font-size : 0.423cm;
font-weight : 900;
}
//*****************************
// * TABLE
// *****************************/
table{
.useSansSerif();
width : 100%;
margin-bottom : 1em;
font-size : 10pt;
thead{
display: table-row-group;
font-weight : 800;
th{
vertical-align : bottom;
padding-bottom : 0.3em;
padding-right : 0.1em;
padding-left : 0.1em;
}
}
tbody{
tr{
td{
padding : 0.3em 0.1em;
}
&:nth-child(odd){
background-color : @noteGreen;
}
}
}
}
//*****************************
// * NOTE
// *****************************/
blockquote{
.useSansSerif();
box-sizing : border-box;
margin-bottom : 1em;
padding : 5px 10px;
background-color : @noteGreen;
border-style : solid;
border-width : 11px;
border-image : @noteBorderImage 11;
border-image-outset : 9px 0px;
box-shadow : 1px 4px 14px #888;
p, ul{
font-size : 0.352cm;
line-height : 1.1em;
}
}
//If a note starts a column, give it space at the top to render border
pre+blockquote, h2+blockquote, h3+blockquote, h4+blockquote, h5+blockquote {
margin-top : 13px;
}
//*****************************
// * MONSTER STAT BLOCK
// *****************************/
hr+blockquote{
position : relative;
padding-top : 15px;
background-color : @monsterStatBackground;
border-style : solid;
border-width : 10px;
border-image : @monsterBorderImage 10;
h2{
margin-top : -8px;
margin-bottom : 0px;
&+p{
padding-bottom : 0px;
}
}
h3{
font-family : ScalySans;
font-weight : 400;
border-bottom : 1px solid @headerText;
}
hr+ul{
color : @headerText;
}
ul{
.useSansSerif();
padding-left : 1em;
font-size : 0.352cm;
}
// Monster Ability table
hr+table{
margin : 0;
column-span : 1;
background-color : transparent;
border-style : none;
border-image : none;
-webkit-column-span : 1;
tbody{
tr:nth-child(odd), tr:nth-child(even){
background-color : transparent;
}
}
}
table{
color : @headerText;
}
p+p{
margin-top : 0em;
padding-bottom : 0.5em;
text-indent : 0em;
}
//Triangle dividers
hr{
visibility : visible;
height : 6px;
margin : 4px 0px;
background-image : @redTriangleImage;
background-size : 100% 100%;
border : none;
}
}
//Full Width
hr+hr+blockquote{
.useColumns(0.96);
}
//*****************************
// * FOOTER
// *****************************/
&:after{
content : "";
position : absolute;
bottom : 0px;
left : 0px;
z-index : 100;
height : 50px;
width : 100%;
background-image : @footerAccentImage;
background-size : cover;
}
&:nth-child(even){
&:after{
transform : scaleX(-1);
}
.pageNumber{
left : 2px;
}
.footnote{
left : 80px;
text-align : left;
}
}
.pageNumber{
position : absolute;
right : 2px;
bottom : 22px;
width : 50px;
font-size : 0.9em;
color : #c9ad6a;
text-align : center;
&.auto::after {
content : counter(phb-page-numbers);
}
}
.footnote{
position : absolute;
right : 80px;
bottom : 32px;
z-index : 150;
width : 200px;
font-size : 0.8em;
color : #c9ad6a;
text-align : right;
}
//*****************************
// * EXTRAS
// *****************************/
hr{
visibility : hidden;
margin : 0px;
}
//Modified unorder list, used in spells
hr+ul{
margin-bottom : 0.5em;
padding-left : 1em;
text-indent : -1em;
list-style-type : none;
}
//Column Break
pre, code{
visibility : hidden;
-webkit-column-break-after : always;
break-after : always;
-moz-column-break-after : always;
}
//Avoid breaking up
p,blockquote,table{
z-index : 15;
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
}
//Better spacing for spell blocks
h4+p+hr+ul{
margin-top : -0.5em
}
//Text indent right after table
table+p{
text-indent : 1em;
}
// Nested lists
ul ul,ol ol,ul ol,ol ul{
margin-bottom : 0px;
margin-left : 1.5em;
}
li{
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
}
}
//*****************************
// * SPELL LIST
// *****************************/
.phb .spellList{
.useSansSerif();
column-count : 4;
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
ul+h5{
margin-top : 15px;
}
p, ul{
font-size : 0.352cm;
line-height : 1.3em;
}
ul{
margin-bottom : 0.5em;
padding-left : 1em;
text-indent : -1em;
list-style-type : none;
-webkit-column-break-inside : auto;
page-break-inside : auto;
break-inside : auto;
}
}
//*****************************
// * WIDE
// *****************************/
.phb .wide{
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
}
//*****************************
// * CLASS TABLE
// *****************************/
.phb .classTable{
margin-top : 25px;
margin-bottom : 40px;
border-collapse : separate;
background-color : white;
border : initial;
border-style : solid;
border-image-outset : 25px 17px;
border-image-repeat : stretch;
border-image-slice : 150 200 150 200;
border-image-source : @frameBorderImage;
border-image-width : 47px;
h5{
margin-bottom : 10px;
}
}
//************************************
// * DESCRIPTIVE TEXT BOX
// ************************************/
.phb .descriptive{
display : block-inline;
margin-bottom : 1em;
background-color : #faf7ea;
font-family : ScalySans;
border-style : solid;
border-width : 7px;
border-image : @descriptiveBoxImage 12 stretch;
border-image-outset : 4px;
box-shadow : 0px 0px 6px #faf7ea;
p{
display : block;
padding-bottom : 0px;
line-height : 1.5em;
}
p + p {
padding-top : .8em;
}
em {
font-family : ScalySans;
font-style : italic;
}
strong {
font-family : ScalySans;
font-weight : 800;
letter-spacing : -0.02em;
}
}
.phb pre+.descriptive{
margin-top : 8px;
}
//*****************************
// * TABLE OF CONTENTS
// *****************************/
.phb .toc{
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
a{
color : black;
text-decoration : none;
&:hover{
text-decoration : underline;
}
}
ul{
padding-left : 0;
list-style-type : none;
}
&>ul>li{
margin-bottom : 10px;
}
}

View File

@@ -3,7 +3,7 @@ module.exports = async(name, title = '', props = {})=>{
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<link href="//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.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> <link href=${`/${name}/bundle.css`} rel='stylesheet'></link>
<link rel="icon" href="/assets/homebrew/favicon.ico" type="image/x-icon" /> <link rel="icon" href="/assets/homebrew/favicon.ico" type="image/x-icon" />
@@ -16,4 +16,4 @@ module.exports = async(name, title = '', props = {})=>{
<script>start_app(${JSON.stringify(props)})</script> <script>start_app(${JSON.stringify(props)})</script>
</html> </html>
`; `;
}; };

7
package-lock.json generated
View File

@@ -4410,7 +4410,12 @@
} }
}, },
"marked": { "marked": {
"version": "0.3.19", "version": "npm:marked@1.2.7",
"resolved": "https://registry.npmjs.org/marked/-/marked-1.2.7.tgz",
"integrity": "sha512-No11hFYcXr/zkBvL6qFmAp1z6BKY3zqLMHny/JN/ey+al7qwCM2+CMBL9BOgqMxZU36fz4cCWfn2poWIf7QRXA=="
},
"markedLegacy": {
"version": "npm:marked@0.3.19",
"resolved": "https://registry.npmjs.org/marked/-/marked-0.3.19.tgz", "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.19.tgz",
"integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==" "integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg=="
}, },

View File

@@ -55,10 +55,11 @@
"jwt-simple": "^0.5.6", "jwt-simple": "^0.5.6",
"less": "^3.13.1", "less": "^3.13.1",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"marked": "^0.3.19",
"moment": "^2.29.1", "moment": "^2.29.1",
"mongoose": "^5.11.13", "mongoose": "^5.11.13",
"nanoid": "3.1.20", "nanoid": "3.1.20",
"markedLegacy": "npm:marked@^0.3.19",
"marked": "npm:marked@^1.2.7",
"nconf": "^0.11.1", "nconf": "^0.11.1",
"prop-types": "15.7.2", "prop-types": "15.7.2",
"query-string": "6.13.8", "query-string": "6.13.8",

View File

@@ -21,10 +21,16 @@ const build = async ({ bundle, render, ssr })=>{
await fs.outputFile('./build/homebrew/ssr.js', ssr); await fs.outputFile('./build/homebrew/ssr.js', ssr);
await fs.outputFile('./build/homebrew/render.js', render); await fs.outputFile('./build/homebrew/render.js', render);
//compress files //compress files in production
await fs.outputFile('./build/homebrew/bundle.css.br', zlib.brotliCompressSync(css)); if(!isDev){
await fs.outputFile('./build/homebrew/bundle.js.br', zlib.brotliCompressSync(bundle)); await fs.outputFile('./build/homebrew/bundle.css.br', zlib.brotliCompressSync(css));
await fs.outputFile('./build/homebrew/ssr.js.br', zlib.brotliCompressSync(ssr)); await fs.outputFile('./build/homebrew/bundle.js.br', zlib.brotliCompressSync(bundle));
await fs.outputFile('./build/homebrew/ssr.js.br', zlib.brotliCompressSync(ssr));
} else {
await fs.remove('./build/homebrew/bundle.css.br');
await fs.remove('./build/homebrew/bundle.js.br');
await fs.remove('./build/homebrew/ssr.js.br');
}
}; };
fs.emptyDirSync('./build/homebrew'); fs.emptyDirSync('./build/homebrew');

View File

@@ -222,6 +222,7 @@ app.use((req, res)=>{
brews : req.brews, brews : req.brews,
googleBrews : req.googleBrews, googleBrews : req.googleBrews,
account : req.account, account : req.account,
enable_v3 : config.get('enable_v3')
}; };
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); })

View File

@@ -157,6 +157,7 @@ GoogleActions = {
lastViewed : brew.lastViewed, lastViewed : brew.lastViewed,
views : brew.views, views : brew.views,
version : brew.version, version : brew.version,
renderer : brew.renderer,
tags : brew.tags, tags : brew.tags,
systems : brew.systems.join() } systems : brew.systems.join() }
}, },
@@ -230,6 +231,7 @@ GoogleActions = {
description : brew.description, description : brew.description,
tags : '', tags : '',
published : brew.published, published : brew.published,
renderer : brew.renderer,
authors : [], authors : [],
systems : [] systems : []
}; };
@@ -291,6 +293,7 @@ GoogleActions = {
lastViewed : obj.data.properties.lastViewed, lastViewed : obj.data.properties.lastViewed,
views : parseInt(obj.data.properties.views) || 0, //brews with no view parameter will return undefined views : parseInt(obj.data.properties.views) || 0, //brews with no view parameter will return undefined
version : parseInt(obj.data.properties.version) || 0, version : parseInt(obj.data.properties.version) || 0,
renderer : obj.data.properties.renderer ? obj.data.properties.renderer : 'legacy',
gDrive : true, gDrive : true,
googleId : id googleId : id

View File

@@ -122,8 +122,6 @@ const newGoogleBrew = async (req, res, next)=>{
req.body = brew; req.body = brew;
console.log(oAuth2Client);
const newBrew = await GoogleActions.newGoogleBrew(oAuth2Client, brew); const newBrew = await GoogleActions.newGoogleBrew(oAuth2Client, brew);
return res.status(200).send(newBrew); return res.status(200).send(newBrew);

View File

@@ -13,6 +13,7 @@ const HomebrewSchema = mongoose.Schema({
description : { type: String, default: '' }, description : { type: String, default: '' },
tags : { type: String, default: '' }, tags : { type: String, default: '' },
systems : [String], systems : [String],
renderer : { type: String, default: '' },
authors : [String], authors : [String],
published : { type: Boolean, default: false }, published : { type: Boolean, default: false },
@@ -55,6 +56,8 @@ HomebrewSchema.statics.get = function(query){
unzipped = zlib.inflateRawSync(brews[0].textBin); unzipped = zlib.inflateRawSync(brews[0].textBin);
brews[0].text = unzipped.toString(); brews[0].text = unzipped.toString();
} }
if(!brews[0].renderer)
brews[0].renderer = 'legacy';
return resolve(brews[0]); return resolve(brews[0]);
}); });
}); });

View File

@@ -53,8 +53,8 @@ const RenderWarnings = createClass({
if(_.isEmpty(this.state.warnings)) return null; if(_.isEmpty(this.state.warnings)) return null;
return <div className='renderWarnings'> return <div className='renderWarnings'>
<i className='fa fa-times dismiss' onClick={this.dismiss}/> <i className='fas fa-times dismiss' onClick={this.dismiss}/>
<i className='fa fa-exclamation-triangle ohno' /> <i className='fas fa-exclamation-triangle ohno' />
<h3>Render Warnings</h3> <h3>Render Warnings</h3>
<small>If this homebrew is rendering badly if might be because of the following:</small> <small>If this homebrew is rendering badly if might be because of the following:</small>
<ul>{_.values(this.state.warnings)}</ul> <ul>{_.values(this.state.warnings)}</ul>

View File

@@ -61,16 +61,12 @@ const CodeEditor = createClass({
} }
}, },
componentWillReceiveProps : function(nextProps){ componentDidUpdate : function(prevProps) {
if(this.codeMirror && nextProps.value !== undefined && this.codeMirror.getValue() != nextProps.value) { if(this.codeMirror && this.codeMirror.getValue() != this.props.value) {
this.codeMirror.setValue(nextProps.value); this.codeMirror.setValue(this.props.value);
} }
}, },
shouldComponentUpdate : function(nextProps, nextState) {
return false;
},
setCursorPosition : function(line, char){ setCursorPosition : function(line, char){
setTimeout(()=>{ setTimeout(()=>{
this.codeMirror.focus(); this.codeMirror.focus();

View File

@@ -1,3 +1,4 @@
/* eslint-disable max-lines */
const _ = require('lodash'); const _ = require('lodash');
const Markdown = require('marked'); const Markdown = require('marked');
const renderer = new Markdown.Renderer(); const renderer = new Markdown.Renderer();
@@ -10,9 +11,87 @@ renderer.html = function (html) {
html = html.substring(0, html.lastIndexOf('</div>')); html = html.substring(0, html.lastIndexOf('</div>'));
return `${openTag} ${Markdown(html)} </div>`; return `${openTag} ${Markdown(html)} </div>`;
} }
// if(_.startsWith(_.trim(html), '<style') && _.endsWith(_.trim(html), '</style>')){
// const openTag = html.substring(0, html.indexOf('>')+1);
// html = html.substring(html.indexOf('>')+1);
// html = html.substring(0, html.lastIndexOf('</style>'));
// html = html.replaceAll(/\s(\.[^{]*)/gm, '.V3 $1');
// return `${openTag} ${html} </style>`;
// }
return html; return html;
}; };
// Ensure {{ Divs don't confuse paragraph parsing (else it renders empty paragraphs)
renderer.paragraph = function(text){
if(text.startsWith('<div') || text.startsWith('</div'))
return `${text}`;
else
return `<p>${text}</p>\n`;
};
// Mustache-style Divs {{class \n content ... \n}}
let blockCount = 0;
const blockRegex = /^ *{{(?:="[\w, ]*"|[^"'\s])*$|^ *}}$/gm;
const inlineFullRegex = /{{[^\n]*}}/g;
const inlineRegex = /{{(?:="[\w, ]*"|[^"'\s])*\s*|}}/g;
renderer.text = function(text){
const newText = text.replaceAll('&quot;', '"');
let matches;
//DIV - BLOCK-LEVEL
if(matches = newText.match(blockRegex)) {
let matchIndex = 0;
const res = _.reduce(newText.split(blockRegex), (r, splitText)=>{
if(splitText) r.push(Markdown.parseInline(splitText, { renderer: renderer }));
const block = matches[matchIndex] ? matches[matchIndex].trimLeft() : '';
if(block && block.startsWith('{')) {
const values = processStyleTags(block.substring(2));
r.push(`<div class="block ${values}">`);
blockCount++;
} else if(block == '}}' && blockCount !== 0){
r.push('</div>');
blockCount--;
}
matchIndex++;
return r;
}, []).join('');
return res;
} else if(matches = newText.match(inlineFullRegex)) {
//SPAN - INLINE
matches = newText.match(inlineRegex);
let matchIndex = 0;
const res = _.reduce(newText.split(inlineRegex), (r, splitText)=>{
if(splitText) r.push(Markdown.parseInline(splitText, { renderer: renderer }));
const block = matches[matchIndex] ? matches[matchIndex].trimLeft() : '';
if(block && block.startsWith('{{')) {
const values = processStyleTags(block.substring(2));
r.push(`<span class="inline-block ${values}>`);
blockCount++;
} else if(block == '}}' && blockCount !== 0){
r.push('</span>');
blockCount--;
}
matchIndex++;
return r;
}, []).join('');
return `${res}`;
} else {
if(!matches) {
return `${text}`;
}
}
};
//Fix local links in the Preview iFrame to link inside the frame
renderer.link = function (href, title, text) { renderer.link = function (href, title, text) {
let self = false; let self = false;
if(href[0] == '#') { if(href[0] == '#') {
@@ -79,7 +158,6 @@ const escape = function (html, encode) {
return html.replace(escapeReplaceNoEncode, getEscapeReplacement); return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
} }
} }
return html; return html;
}; };
@@ -95,10 +173,24 @@ const tagRegex = new RegExp(`(${
return `\\<${type}|\\</${type}>`; return `\\<${type}|\\</${type}>`;
}).join('|')})`, 'g'); }).join('|')})`, 'g');
const processStyleTags = (string)=>{
const tags = string.match(/(?:[^, "=]+|="[^"]*")+/g);
if(!tags) return '"';
const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0];
const classes = _.remove(tags, (tag)=>!tag.includes('"'));
const styles = tags.map((tag)=>tag.replace(/="(.*)"/g, ':$1;'));
return `${classes.join(' ')}" ${id ? `id="${id}"` : ''} ${styles ? `style="${styles.join(' ')}"` : ''}`;
};
module.exports = { module.exports = {
marked : Markdown, marked : Markdown,
render : (rawBrewText)=>{ render : (rawBrewText)=>{
blockCount = 0;
rawBrewText = rawBrewText.replace(/^\\column/gm, `<div class='columnSplit'></div>`)
.replace(/^}}/gm, '\n}}')
.replace(/^({{[^\n]*)$/gm, '$1\n');
return Markdown( return Markdown(
sanatizeScriptTags(rawBrewText), sanatizeScriptTags(rawBrewText),
{ renderer: renderer } { renderer: renderer }

View File

@@ -0,0 +1,166 @@
const _ = require('lodash');
const Markdown = require('markedLegacy');
const renderer = new Markdown.Renderer();
//Processes the markdown within an HTML block if it's just a class-wrapper
renderer.html = function (html) {
if(_.startsWith(_.trim(html), '<div') && _.endsWith(_.trim(html), '</div>')){
const openTag = html.substring(0, html.indexOf('>')+1);
html = html.substring(html.indexOf('>')+1);
html = html.substring(0, html.lastIndexOf('</div>'));
return `${openTag} ${Markdown(html)} </div>`;
}
// if(_.startsWith(_.trim(html), '<style') && _.endsWith(_.trim(html), '</style>')){
// const openTag = html.substring(0, html.indexOf('>')+1);
// html = html.substring(html.indexOf('>')+1);
// html = html.substring(0, html.lastIndexOf('</style>'));
// html = html.replaceAll(/\s(\.[^{]*)/gm, '.legacy $1');
// return `${openTag} ${html} </style>`;
// }
return html;
};
renderer.link = function (href, title, text) {
let self = false;
if(href[0] == '#') {
self = true;
}
href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
if(href === null) {
return text;
}
let out = `<a href="${escape(href)}"`;
if(title) {
out += ` title="${title}"`;
}
if(self) {
out += ' target="_self"';
}
out += `>${text}</a>`;
return out;
};
const nonWordAndColonTest = /[^\w:]/g;
const cleanUrl = function (sanitize, base, href) {
if(sanitize) {
let prot;
try {
prot = decodeURIComponent(unescape(href))
.replace(nonWordAndColonTest, '')
.toLowerCase();
} catch (e) {
return null;
}
if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
return null;
}
}
try {
href = encodeURI(href).replace(/%25/g, '%');
} catch (e) {
return null;
}
return href;
};
const escapeTest = /[&<>"']/;
const escapeReplace = /[&<>"']/g;
const escapeTestNoEncode = /[<>"']|&(?!#?\w+;)/;
const escapeReplaceNoEncode = /[<>"']|&(?!#?\w+;)/g;
const escapeReplacements = {
'&' : '&amp;',
'<' : '&lt;',
'>' : '&gt;',
'"' : '&quot;',
'\'' : '&#39;'
};
const getEscapeReplacement = (ch)=>escapeReplacements[ch];
const escape = function (html, encode) {
if(encode) {
if(escapeTest.test(html)) {
return html.replace(escapeReplace, getEscapeReplacement);
}
} else {
if(escapeTestNoEncode.test(html)) {
return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
}
}
return html;
};
const sanatizeScriptTags = (content)=>{
return content
.replace(/<script/ig, '&lt;script')
.replace(/<\/script>/ig, '&lt;/script&gt;');
};
const tagTypes = ['div', 'span', 'a'];
const tagRegex = new RegExp(`(${
_.map(tagTypes, (type)=>{
return `\\<${type}|\\</${type}>`;
}).join('|')})`, 'g');
module.exports = {
marked : Markdown,
render : (rawBrewText)=>{
return Markdown(
sanatizeScriptTags(rawBrewText),
{ renderer: renderer }
);
},
validate : (rawBrewText)=>{
const errors = [];
const leftovers = _.reduce(rawBrewText.split('\n'), (acc, line, _lineNumber)=>{
const lineNumber = _lineNumber + 1;
const matches = line.match(tagRegex);
if(!matches || !matches.length) return acc;
_.each(matches, (match)=>{
_.each(tagTypes, (type)=>{
if(match == `<${type}`){
acc.push({
type : type,
line : lineNumber
});
}
if(match === `</${type}>`){
if(!acc.length){
errors.push({
line : lineNumber,
type : type,
text : 'Unmatched closing tag',
id : 'CLOSE'
});
} else if(_.last(acc).type == type){
acc.pop();
} else {
errors.push({
line : `${_.last(acc).line} to ${lineNumber}`,
type : type,
text : 'Type mismatch on closing tag',
id : 'MISMATCH'
});
acc.pop();
}
}
});
});
return acc;
}, []);
_.each(leftovers, (unmatched)=>{
errors.push({
line : unmatched.line,
type : unmatched.type,
text : 'Unmatched opening tag',
id : 'OPEN'
});
});
return errors;
},
};

View File

@@ -50,7 +50,7 @@ const Nav = {
const classes = cx('navItem', this.props.color, this.props.className); const classes = cx('navItem', this.props.color, this.props.className);
let icon; let icon;
if(this.props.icon) icon = <i className={`fa ${this.props.icon}`} />; if(this.props.icon) icon = <i className={this.props.icon} />;
const props = _.omit(this.props, ['newTab']); const props = _.omit(this.props, ['newTab']);

View File

@@ -56,9 +56,9 @@ const SplitPane = createClass({
renderDivider : function(){ renderDivider : function(){
return <div className='divider' onMouseDown={this.handleDown} > return <div className='divider' onMouseDown={this.handleDown} >
<div className='dots'> <div className='dots'>
<i className='fa fa-circle' /> <i className='fas fa-circle' />
<i className='fa fa-circle' /> <i className='fas fa-circle' />
<i className='fa fa-circle' /> <i className='fas fa-circle' />
</div> </div>
</div>; </div>;
}, },