0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-24 12:02:48 +00:00

Merge branch 'master' into pr/1198

This commit is contained in:
Trevor Buckner
2021-03-06 22:42:15 -05:00
85 changed files with 3973 additions and 1060 deletions

View File

@@ -1,7 +1,7 @@
module.exports = {
root : true,
parserOptions : {
ecmaVersion : 9,
ecmaVersion : 2021,
sourceType : 'module',
ecmaFeatures : {
jsx : true

View File

@@ -1,7 +1,31 @@
<style>
h5 {
font-size: .35cm !important;
}
</style>
# changelog
### Saturday, 05/3/2021 - v2.11.0
- Many background things for upcoming v3. Get pumped.
##### G-Ambatte :
- Fixed new brews failing to save when auto-generated file name is too long.
- "New" button added to the Nav bar.
- Reduced download size and improved caching.
##### RKuerten :
- Bold and Italics hotkeys for Mac users (Cmd+B, Cmd+I)
### Friday, 25/1/2021 - v2.10.7
- Cover Page snippet now flips left-right page numbering.
- Added instructions for [installing on a FreeBSD Jail](https://github.com/naturalcrit/homebrewery/blob/master/README.FREEBSD.md).
- Fix for box-shadows breaking across columns. <br>(Thanks G-Ambatte for all of these!)
- Small user interface tweaks (Thanks Ericsheid)
### Friday, 02/1/2021 - v2.10.6
- Fixed punctuation for usernames ending with 's' on the user page. (Thanks @AlexeySachkov)
- Fixed punctuation for usernames ending with 's' on the user page. (Thanks AlexeySachkov)
- Fixed server crashes due to excessive long lines in brews
- Fixed "automated request" lockouts from Google
@@ -24,6 +48,9 @@
- Fixed issue with users unable to create new brews
- Fixing brews being lost when loaded via back button
```
```
### Wednesday, 07/10/2020 - v2.10.0
- Google Drive integration -- Sign in with your Google account to link it with your Homebrewery profile. A new button in the Edit page will let you transfer your file to your personal Google Drive storage, and Google will keep a backup of each version! No more lost work surprises!
@@ -37,9 +64,6 @@
### Wednesday, 20/05/2020 - v2.9.0
- Major refactoring of site backend to work with updated dependencies for security (should be invisible to users)
```
```
### Wednesday, 11/03/2020 - v2.8.2
- Fixed delete button removing everyone's copy for brews with multiple authors
- Compressed homebrew text in database
@@ -70,10 +94,12 @@
### Friday, 03/03/2017 - v2.7.3
- Increasing the range on the Partial Page Rendering for a quick-fix for it getting out of sync on long brews.
\page
### Saturday, 18/02/2017 - v2.7.2
- Adding ability to delete a brew from the user page, incase the user creates a brew that makes the edit page unrender-able. (re:309)
### Thursday, 19/01/2017 - v2.7.0
### Thursday, 19/01/2017 - v2.7.1
- Fixed saving multiple authors and multiple systems on brew metadata (thanks u/PalaNolho re:282)
- Adding in line highlight for new pages
- Added in a simple brew lookup for admin
@@ -81,8 +107,6 @@
### Saturday, 14/01/2017 - v2.7.0
- Added a new Render Warning overlay. It detects situations where the brew may not be rendering correctly (wrong browser, browser is zoomed in...) and let's the user know
\page
### Sunday, 25/12/2016 - v2.7.0
- Switching over to using Vitreum v4
- Removed gulp, all tasks are run through npm scripts
@@ -110,6 +134,9 @@
- Added in a snippet for a split table
- Added an account nav item to new page
```
```
### Sunday, 27/11/2016 - v2.5.1
- Fixed the column rendering on the new user page. Really should have tested that better
- Added a hover tooltip to fully read the brew description
@@ -125,9 +152,6 @@
- Added a new user page to see others published brews, as well as all of your own brews.
- Added a new nav item for accessing your profile and logging in
```
```
### Monday, 14/11/2016
- Updated snippet bar style
- You can now print from a new page without saving
@@ -152,6 +176,8 @@
- Fixed the noteblock overlapping into titles (thanks u/dsompura!)
- Fixed a bad search route in the admin panel (thanks u/SnappyTom!)
\page
### Friday, 29/07/2016 - v2.2.7
- Adding in descriptive note blocks. (Thanks calculuschild!)
@@ -166,8 +192,6 @@
### Tuesday, 07/06/2016 - v2.2.2
- Fixed bug with new markdown lexer and aprser not working on print page
\page
### Sunday, 05/06/2016 - v2.2.1
- Adding in a new Class table div block. The old Class table block used weird stacking of HTML elements, resulting is difficult to control behaviour and poor interactiosn with the rest of the page. This new block is much easier to style and work with.
- Added in a new wide table snippet
@@ -191,9 +215,6 @@
### Wednesday, 25/05/2016 -v2.0.5
- The class table generators have the proper ability score improvement progression.
```
```
### Tuesday, 24/05/2016 - v2.0.4
- Fixed extra wide monster stat blocks sometimes only being one column
- The class table generators now follow the proper progression from the PHB (thakns u/IrishBandit)

View File

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

View File

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

View File

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

View File

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

View File

@@ -37,7 +37,7 @@ const Stats = createClass({
</dl>
{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>;
}

View File

@@ -4,6 +4,7 @@ const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
const Markdown = require('naturalcrit/markdown.js');
const ErrorBar = require('./errorBar/errorBar.jsx');
@@ -18,12 +19,18 @@ const PPR_THRESHOLD = 50;
const BrewRenderer = createClass({
getDefaultProps : function() {
return {
text : '',
errors : []
text : '',
renderer : 'legacy',
errors : []
};
},
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 {
viewablePageNumber : 0,
@@ -34,7 +41,7 @@ const BrewRenderer = createClass({
usePPR : pages.length >= PPR_THRESHOLD,
visibility : 'hidden',
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='/homebrew/bundle.css' rel='stylesheet' />
<base target=_blank>
@@ -48,12 +55,19 @@ const BrewRenderer = createClass({
window.removeEventListener('resize', this.updateSize);
},
componentWillReceiveProps : function(nextProps) {
const pages = nextProps.text.split('\\page');
this.setState({
pages : pages,
usePPR : pages.length >= PPR_THRESHOLD
});
componentDidUpdate : function(prevProps) {
if(prevProps.text !== this.props.text) {
let pages;
if(this.props.renderer == 'legacy') {
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() {
@@ -103,12 +117,15 @@ const BrewRenderer = createClass({
renderDummyPage : function(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>;
},
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(){
@@ -159,7 +176,7 @@ const BrewRenderer = createClass({
: null}
<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}
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{
will-change : transform;
overflow-y : scroll;
@@ -14,8 +13,17 @@
margin-left : auto;
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{
position : absolute;
right : 17px;
@@ -37,4 +45,4 @@
font-size : 10px;
font-weight : 800;
color : white;
}
}

View File

@@ -62,7 +62,7 @@ const ErrorBar = createClass({
if(!this.props.errors.length) return null;
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>
<small>If these aren't fixed your brew will not render properly when you print it to PDF or share it</small>
{this.renderErrors()}

View File

@@ -60,8 +60,8 @@ const NotificationPopup = createClass({
if(_.isEmpty(this.state.notifications)) return null;
return <div className='notificationPopup'>
<i className='fa fa-times dismiss' onClick={this.dismiss}/>
<i className='fa fa-info-circle info' />
<i className='fas fa-times dismiss' onClick={this.dismiss}/>
<i className='fas fa-info-circle info' />
<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>
<ul>{_.values(this.state.notifications)}</ul>

View File

@@ -18,12 +18,14 @@ const SNIPPETBAR_HEIGHT = 25;
const Editor = createClass({
getDefaultProps : function() {
return {
value : '',
brew : {
text : ''
},
onChange : ()=>{},
metadata : {},
onMetadataChange : ()=>{},
showMetaButton : true
showMetaButton : true,
renderer : 'legacy'
};
},
getInitialState : function() {
@@ -38,7 +40,7 @@ const Editor = createClass({
componentDidMount : function() {
this.updateEditorSize();
this.highlightPageLines();
this.highlightCustomMarkdown();
window.addEventListener('resize', this.updateEditorSize);
},
componentWillUnmount : function() {
@@ -58,7 +60,7 @@ const Editor = createClass({
this.cursorPosition = curpos;
},
handleInject : function(injectText){
const lines = this.props.value.split('\n');
const lines = this.props.brew.text.split('\n');
lines[this.cursorPosition.line] = splice(lines[this.cursorPosition.line], this.cursorPosition.ch, injectText);
this.handleTextChange(lines.join('\n'));
@@ -71,22 +73,75 @@ const Editor = createClass({
},
getCurrentPage : function(){
const lines = this.props.value.split('\n').slice(0, this.cursorPosition.line + 1);
const lines = this.props.brew.text.split('\n').slice(0, this.cursorPosition.line + 1);
return _.reduce(lines, (r, line)=>{
if(line.indexOf('\\page') !== -1) r++;
return r;
}, 1);
},
highlightPageLines : function(){
highlightCustomMarkdown : function(){
if(!this.refs.codeEditor) return;
const codeMirror = this.refs.codeEditor.codeMirror;
const lineNumbers = _.reduce(this.props.value.split('\n'), (r, line, lineNumber)=>{
if(line.indexOf('\\page') !== -1){
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
r.push(lineNumber);
//reset custom text styles
const customHighlights = codeMirror.getAllMarks();
for (let i=0;i<customHighlights.length;i++) customHighlights[i].clear();
const lineNumbers = _.reduce(this.props.brew.text.split('\n'), (r, line, lineNumber)=>{
//reset custom line styles
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.match(/^\\column$/)){
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
r.push(lineNumber);
}
// Highlight inline spans {{content}}
if(line.includes('{{') && line.includes('}}')){
const regex = /{{(?:="[\w,\-. ]*"|[^"'\s])*\s*|}}/g;
let match;
let blockCount = 0;
while ((match = regex.exec(line)) != null) {
if(match[0].startsWith('{')) {
blockCount += 1;
} else {
blockCount -= 1;
}
if(blockCount < 0) {
blockCount = 0;
continue;
}
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'inline-block' });
}
} else if(line.trimLeft().startsWith('{{') || line.trimLeft().startsWith('}}')){
// Highlight block divs {{\n Content \n}}
let endCh = line.length+1;
const match = line.match(/^ *{{(?:="[\w,\-. ]*"|[^"'\s])*$|^ *}}$/);
if(match)
endCh = match.index+match[0].length;
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
}
}
return r;
}, []);
return lineNumbers;
@@ -106,33 +161,34 @@ const Editor = createClass({
renderMetadataEditor : function(){
if(!this.state.showMetadataEditor) return;
return <MetadataEditor
metadata={this.props.metadata}
metadata={this.props.brew}
onChange={this.props.onMetadataChange}
/>;
},
render : function(){
this.highlightPageLines();
this.highlightCustomMarkdown();
return (
<div className='editor' ref='main'>
<SnippetBar
brew={this.props.value}
brew={this.props.brew}
onInject={this.handleInject}
onToggle={this.handgleToggle}
showmeta={this.state.showMetadataEditor}
showMetaButton={this.props.showMetaButton} />
showMetaButton={this.props.showMetaButton}
renderer={this.props.renderer} />
{this.renderMetadataEditor()}
<CodeEditor
ref='codeEditor'
wrap={true}
language='gfm'
value={this.props.value}
value={this.props.brew.text}
onChange={this.handleTextChange}
onCursorActivity={this.handleCursorActivty} />
{/*
<div className='brewJump' onClick={this.brewJump}>
<i className='fa fa-arrow-right' />
<i className='fas fa-arrow-right' />
</div>
*/}
</div>

View File

@@ -9,6 +9,22 @@
background-color : fade(#333, 15%);
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{
@@ -26,4 +42,4 @@
.tooltipLeft("Jump to brew page");
}
}
}

View File

@@ -17,7 +17,8 @@ const MetadataEditor = createClass({
tags : '',
published : false,
authors : [],
systems : []
systems : [],
renderer : 'legacy'
},
onChange : ()=>{}
};
@@ -36,6 +37,12 @@ const MetadataEditor = createClass({
}
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){
this.props.onChange(_.merge({}, this.props.metadata, {
published : val
@@ -43,7 +50,7 @@ const MetadataEditor = createClass({
},
handleDelete : function(){
if(this.props.metadata.authors.length <= 1){
if(this.props.metadata.authors && this.props.metadata.authors.length <= 1){
if(!confirm('Are you sure you want to delete this brew? Because you are the only owner of this brew, the document will be deleted permanently.')) return;
if(!confirm('Are you REALLY sure? You will not be able to recover the document.')) return;
} else {
@@ -83,11 +90,11 @@ const MetadataEditor = createClass({
renderPublish : function(){
if(this.props.metadata.published){
return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
<i className='fa fa-ban' /> unpublish
<i className='fas fa-ban' /> unpublish
</button>;
} else {
return <button className='publish' onClick={()=>this.handlePublish(true)}>
<i className='fa fa-globe' /> publish
<i className='fas fa-globe' /> publish
</button>;
}
},
@@ -99,7 +106,7 @@ const MetadataEditor = createClass({
<label>delete</label>
<div className='value'>
<button className='publish' onClick={this.handleDelete}>
<i className='fa fa-trash' /> delete brew
<i className='fas fa-trash-alt' /> delete brew
</button>
</div>
</div>;
@@ -107,7 +114,7 @@ const MetadataEditor = createClass({
renderAuthors : function(){
let text = 'None.';
if(this.props.metadata.authors.length){
if(this.props.metadata.authors && this.props.metadata.authors.length){
text = this.props.metadata.authors.join(', ');
}
return <div className='field authors'>
@@ -126,13 +133,42 @@ const MetadataEditor = createClass({
<div className='value'>
<a href={this.getRedditLink()} target='_blank' rel='noopener noreferrer'>
<button className='publish'>
<i className='fa fa-reddit-alien' /> share to reddit
<i className='fab fa-reddit-alien' /> share to reddit
</button>
</a>
</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(){
return <div className='metadataEditor'>
<div className='field title'>
@@ -154,6 +190,8 @@ const MetadataEditor = createClass({
</div>
*/}
{this.renderAuthors()}
<div className='field systems'>
<label>systems</label>
<div className='value'>
@@ -161,7 +199,7 @@ const MetadataEditor = createClass({
</div>
</div>
{this.renderAuthors()}
{this.renderRenderOptions()}
<div className='field publish'>
<label>publish</label>

View File

@@ -5,7 +5,8 @@ const _ = require('lodash');
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){
if(_.isFunction(val)) return val(brew);
@@ -15,11 +16,18 @@ const execute = function(val, brew){
const Snippetbar = createClass({
getDefaultProps : function() {
return {
brew : '',
brew : {},
onInject : ()=>{},
onToggle : ()=>{},
showmeta : false,
showMetaButton : true
showMetaButton : true,
renderer : ''
};
},
getInitialState : function() {
return {
renderer : this.props.renderer
};
},
@@ -28,6 +36,11 @@ const Snippetbar = createClass({
},
renderSnippetGroups : function(){
if(this.props.renderer == 'V3')
Snippets = SnippetsV3;
else
Snippets = SnippetsLegacy;
return _.map(Snippets, (snippetGroup)=>{
return <SnippetGroup
brew={this.props.brew}
@@ -42,9 +55,10 @@ const Snippetbar = createClass({
renderMetadataButton : function(){
if(!this.props.showMetaButton) return;
return <div className={cx('toggleMeta', { selected: this.props.showmeta })}
return <div className={cx('snippetBarButton', 'toggleMeta', { selected: this.props.showmeta })}
onClick={this.props.onToggle}>
<i className='fa fa-bars' />
<i className='fas fa-info-circle' />
<span className='groupName'>Properties</span>
</div>;
},
@@ -66,9 +80,9 @@ module.exports = Snippetbar;
const SnippetGroup = createClass({
getDefaultProps : function() {
return {
brew : '',
brew : {},
groupName : '',
icon : 'fa-rocket',
icon : 'fas fa-rocket',
snippets : [],
onSnippetClick : function(){},
};
@@ -79,16 +93,16 @@ const SnippetGroup = createClass({
renderSnippets : function(){
return _.map(this.props.snippets, (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}
</div>;
});
},
render : function(){
return <div className='snippetGroup'>
return <div className='snippetGroup snippetBarButton'>
<div className='text'>
<i className={`fa fa-fw ${this.props.icon}`} />
<i className={this.props.icon} />
<span className='groupName'>{this.props.groupName}</span>
</div>
<div className='dropdown'>

View File

@@ -4,44 +4,33 @@
position : relative;
height : @height;
background-color : #ddd;
.toggleMeta{
position : absolute;
top : 0px;
right : 0px;
height : @height;
width : @height;
cursor : pointer;
line-height : @height;
text-align : center;
.tooltipLeft("Edit Brew Metadata");
.snippetBarButton{
height : @height;
line-height : @height;
display : inline-block;
padding : 0px 5px;
font-weight : 800;
font-size : 0.625em;
text-transform : uppercase;
cursor : pointer;
&:hover, &.selected{
background-color : #999;
}
}
.snippetGroup{
display : inline-block;
height : @height;
padding : 0px 5px;
cursor : pointer;
font-size : 0.6em;
font-weight : 800;
line-height : @height;
text-transform : uppercase;
border-right : 1px solid black;
i{
vertical-align : middle;
margin-right : 3px;
font-size : 1.2em;
}
&:hover, &.selected{
background-color : #999;
}
.text{
line-height : @height;
.groupName{
font-size : 10px;
}
font-size : 1.4em;
}
}
.toggleMeta{
position : absolute;
top : 0px;
right : 0px;
border-left : 1px solid black;
.tooltipLeft("Edit Brew Properties");
}
.snippetGroup{
border-right : 1px solid black;
&:hover{
.dropdown{
visibility : visible;
@@ -62,7 +51,7 @@
font-size : 10px;
i{
margin-right : 8px;
font-size : 13px;
font-size : 1.2em;
}
&:hover{
background-color : #999;
@@ -70,4 +59,4 @@
}
}
}
}
}

View File

@@ -47,6 +47,12 @@ const spellNames = [
'Ultimate Rite of the Confetti Angel',
'Ultimate Ritual of Mouthwash',
];
const itemNames = [
'Doorknob of Niceness',
'Paper Armor of Folding',
'Mixtape of Sadness',
'Staff of Endless Confetti',
];
module.exports = {
@@ -87,5 +93,17 @@ module.exports = {
'A *continual flame* can be covered or hidden but not smothered or quenched.',
'\n\n\n'
].join('\n');
},
item : function() {
return [
`#### ${_.sample(itemNames)}`,
`*${_.sample(['Wondrous item', 'Armor', 'Weapon'])}, ${_.sample(['Common', 'Uncommon', 'Rare', 'Very Rare', 'Legendary', 'Artifact'])} (requires attunement)*`,
`:`,
`This knob is pretty nice. When attached to a door, it allows a user to`,
`open that door with the strength of the nearest animal. For example, if`,
`there is a cow nearby, the user will have the "strength of a cow" while`,
`opening this door.`
].join('\n');
}
};
};

View File

@@ -12,73 +12,71 @@ module.exports = [
{
groupName : 'Editor',
icon : 'fa-pencil',
icon : 'fas fa-pencil-alt',
snippets : [
{
name : 'Column Break',
icon : 'fa-columns',
gen : '```\n```\n\n'
icon : 'fas fa-columns',
gen : '\n\\column\n'
},
{
name : 'New Page',
icon : 'fa-file-text',
gen : '\\page\n\n'
icon : 'fas fa-file-alt',
gen : '\n\\page\n'
},
{
name : 'Vertical Spacing',
icon : 'fa-arrows-v',
gen : '<div style=\'margin-top:140px\'></div>\n\n'
icon : 'fas fa-times-circle',
gen : ''
},
{
name : 'Wide Block',
icon : 'fa-arrows-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'
icon : 'fas fa-times-circle',
gen : ''
},
{
name : 'Image',
icon : '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')
icon : 'fas fa-times-circle',
gen : ''
},
{
name : 'Background Image',
icon : 'fa-tree',
gen : [
'<img ',
' src=\'http://i.imgur.com/hMna6G0.png\' ',
' style=\'position:absolute; top:50px; right:30px; width:280px\' />'
].join('\n')
icon : 'fas fa-tree',
gen : `<img src='http://i.imgur.com/hMna6G0.png' ` +
`style='position:absolute; top:50px; right:30px; width:280px'/>`
},
{
name : 'QR Code',
icon : 'fas fa-qrcode',
gen : (brew)=>{
return `<img ` +
`src='https://api.qrserver.com/v1/create-qr-code/?data=` +
`https://homebrewery.naturalcrit.com/share/${brew.shareId}` +
`&amp;size=100x100' ` +
`style='width:100px;mix-blend-mode:multiply'/>`;
}
},
{
name : 'Page Number',
icon : 'fa-bookmark',
gen : '<div class=\'pageNumber\'>1</div>\n<div class=\'footnote\'>PART 1 | FANCINESS</div>\n\n'
icon : 'fas fa-bookmark',
gen : '{{pageNumber\n1\n}}\n{{footnote\nPART 1 | FANCINESS\n}}\n\n'
},
{
name : 'Auto-incrementing Page Number',
icon : 'fa-sort-numeric-asc',
gen : '<div class=\'pageNumber auto\'></div>\n'
icon : 'fas fa-sort-numeric-down',
gen : '{{\npageNumber,auto\n}}\n\n'
},
{
name : 'Link to page',
icon : 'fa-link',
icon : 'fas fa-link',
gen : '[Click here](#p3) to go to page 3\n'
},
{
name : 'Table of Contents',
icon : 'fa-book',
icon : 'fas fa-book',
gen : TableOfContentsGen
},
]
},
@@ -87,26 +85,26 @@ module.exports = [
{
groupName : 'PHB',
icon : 'fa-book',
icon : 'fas fa-book',
snippets : [
{
name : 'Spell',
icon : 'fa-magic',
icon : 'fas fa-magic',
gen : MagicGen.spell,
},
{
name : 'Spell List',
icon : 'fa-list',
icon : 'fas fa-scroll',
gen : MagicGen.spellList,
},
{
name : 'Class Feature',
icon : 'fa-trophy',
icon : 'fas fa-mask',
gen : ClassFeatureGen,
},
{
name : 'Note',
icon : 'fa-sticky-note',
icon : 'fas fa-sticky-note',
gen : function(){
return [
'> ##### Time to Drop Knowledge',
@@ -118,7 +116,7 @@ module.exports = [
},
{
name : 'Descriptive Text Box',
icon : 'fa-sticky-note-o',
icon : 'fas fa-comment-alt',
gen : function(){
return [
'<div class=\'descriptive\'>',
@@ -132,19 +130,24 @@ module.exports = [
},
{
name : 'Monster Stat Block',
icon : 'fa-bug',
icon : 'fas fa-spider',
gen : MonsterBlockGen.half,
},
{
name : 'Wide Monster Stat Block',
icon : 'fa-paw',
icon : 'fas fa-dragon',
gen : MonsterBlockGen.full,
},
{
name : 'Cover Page',
icon : 'fa-file-word-o',
icon : 'fas fa-file-word',
gen : CoverPageGen,
},
{
name : 'Magic Item',
icon : 'fas fa-hat-wizard',
gen : MagicGen.item,
},
]
},
@@ -154,21 +157,21 @@ module.exports = [
{
groupName : 'Tables',
icon : 'fa-table',
icon : 'fas fa-table',
snippets : [
{
name : 'Class Table',
icon : 'fa-table',
icon : 'fas fa-table',
gen : ClassTableGen.full,
},
{
name : 'Half Class Table',
icon : 'fa-list-alt',
icon : 'fas fa-list-alt',
gen : ClassTableGen.half,
},
{
name : 'Table',
icon : 'fa-th-list',
icon : 'fas fa-th-list',
gen : function(){
return [
'##### Cookie Tastiness',
@@ -184,7 +187,7 @@ module.exports = [
},
{
name : 'Wide Table',
icon : 'fa-list',
icon : 'fas fa-list',
gen : function(){
return [
'<div class=\'wide\'>',
@@ -202,7 +205,7 @@ module.exports = [
},
{
name : 'Split Table',
icon : 'fa-th-large',
icon : 'fas fa-th-large',
gen : function(){
return [
'<div style=\'column-count:2\'>',
@@ -238,11 +241,11 @@ module.exports = [
{
groupName : 'Print',
icon : 'fa-print',
icon : 'fas fa-print',
snippets : [
{
name : 'A4 PageSize',
icon : 'fa-file-o',
icon : 'far fa-file',
gen : ['<style>',
' .phb{',
' width : 210mm;',
@@ -253,7 +256,7 @@ module.exports = [
},
{
name : 'Ink Friendly',
icon : 'fa-tint',
icon : 'fas fa-tint',
gen : ['<style>',
' .phb{ background : white;}',
' .phb img{ display : none;}',

View File

@@ -48,7 +48,7 @@ const getTOC = (pages)=>{
};
module.exports = function(brew){
const pages = brew.split('\\page');
const pages = brew.text.split('\\page');
const TOC = getTOC(pages);
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);
@@ -69,4 +69,4 @@ module.exports = function(brew){
##### Table Of Contents
${markdown}
</div>\n`;
};
};

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.text.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 : '',
version : '0.0.0',
account : null,
enable_v3 : false,
brew : {
title : '',
text : '',
@@ -33,6 +34,7 @@ const Homebrew = createClass({
componentWillMount : function() {
global.account = this.props.account;
global.version = this.props.version;
global.enable_v3 = this.props.enable_v3;
},
render : function (){
@@ -42,10 +44,11 @@ const Homebrew = createClass({
<Switch>
<Route path='/edit/:id' component={(routeProps)=><EditPage id={routeProps.match.params.id} brew={this.props.brew} />}/>
<Route path='/share/:id' component={(routeProps)=><SharePage id={routeProps.match.params.id} brew={this.props.brew} />}/>
<Route path='/new/:id' component={(routeProps)=><NewPage id={routeProps.match.params.id} brew={this.props.brew} />}/>
<Route path='/new' exact component={NewPage}/>
<Route path='/user/:username' component={(routeProps)=><UserPage username={routeProps.match.params.username} brews={this.props.brews} />}/>
<Route path='/print/:id' component={(routeProps)=><PrintPage brew={this.props.brew} query={queryString.parse(routeProps.location.search)} /> } />
<Route path='/print' exact component={(routeProps)=><PrintPage query={queryString.parse(routeProps.location.search)} /> } />
<Route path='/new' exact component={NewPage}/>
<Route path='/changelog' exact component={()=><SharePage brew={{ title: 'Changelog', text: this.props.changelog }} />}/>
<Route path='/' component={()=><HomePage welcomeText={this.props.welcomeText}/>}/>
</Switch>

View File

@@ -20,12 +20,12 @@ const Account = createClass({
render : function(){
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}
</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
</Nav.item>;
}

View File

@@ -6,8 +6,8 @@ module.exports = function(props){
return <Nav.item
newTab={true}
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')}`} >
report issue
</Nav.item>;
};
};

View File

@@ -0,0 +1,11 @@
const React = require('react');
const Nav = require('naturalcrit/nav/nav.jsx');
module.exports = function(props){
return <Nav.item
href='/new'
color='purple'
icon='fas fa-plus-square'>
new
</Nav.item>;
};

View File

@@ -1,5 +1,4 @@
const React = require('react');
const createClass = require('create-react-class');
const Nav = require('naturalcrit/nav/nav.jsx');
module.exports = function(props){
@@ -8,7 +7,7 @@ module.exports = function(props){
newTab={true}
href='https://www.patreon.com/NaturalCrit'
color='green'
icon='fa-heart'>
icon='fas fa-heart'>
help out
</Nav.item>;
};

View File

@@ -3,7 +3,7 @@ const createClass = require('create-react-class');
const Nav = require('naturalcrit/nav/nav.jsx');
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
</Nav.item>;
};
};

View File

@@ -143,7 +143,7 @@ const RecentItems = createClass({
},
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)}
onMouseLeave={()=>this.handleDropdown(false)}>
{this.props.text}

View File

@@ -9,6 +9,7 @@ const { Meta } = require('vitreum/headtags');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const ReportIssue = require('../../navbar/issue.navitem.jsx');
const PrintLink = require('../../navbar/print.navitem.jsx');
const Account = require('../../navbar/account.navitem.jsx');
@@ -41,17 +42,18 @@ const EditPage = createClass({
tags : '',
published : false,
authors : [],
systems : []
systems : [],
renderer : 'legacy'
}
};
},
getInitialState : function() {
return {
brew : this.props.brew,
brew : this.props.brew,
isSaving : false,
isPending : false,
alertRenderChange : false,
saveGoogle : this.props.brew.googleId ? true : false,
confirmGoogleTransfer : false,
errors : null,
@@ -66,6 +68,8 @@ const EditPage = createClass({
url : window.location.href
});
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
this.trySave();
window.onbeforeunload = ()=>{
if(this.state.isSaving || this.state.isPending){
@@ -101,6 +105,11 @@ const EditPage = createClass({
},
handleMetadataChange : function(metadata){
if(metadata.renderer != this.savedBrew.renderer){
this.setState({
alertRenderChange : true
});
}
this.setState((prevState)=>({
brew : _.merge({}, prevState.brew, metadata),
isPending : true,
@@ -122,8 +131,7 @@ const EditPage = createClass({
},
hasChanges : function(){
const savedBrew = this.savedBrew ? this.savedBrew : this.props.brew;
return !_.isEqual(this.state.brew, savedBrew);
return !_.isEqual(this.state.brew, this.savedBrew);
},
trySave : function(){
@@ -142,6 +150,12 @@ const EditPage = createClass({
this.clearErrors();
},
closeAlerts : function(){
this.setState({
alertRenderChange : false
});
},
toggleGoogleStorage : function(){
this.setState((prevState)=>({
saveGoogle : !prevState.saveGoogle,
@@ -294,7 +308,7 @@ const EditPage = createClass({
} catch (e){}
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!
<div className='errorContainer' onClick={this.clearErrors}>
You must be signed in to a Google account
@@ -312,7 +326,7 @@ const EditPage = createClass({
</Nav.item>;
}
return <Nav.item className='save error' icon='fa-warning'>
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer'>
Looks like there was a problem saving. <br />
@@ -325,16 +339,25 @@ const EditPage = createClass({
}
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()){
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){
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() {
return this.state.brew.googleId ?
this.state.brew.googleId + this.state.brew.shareId :
@@ -350,8 +373,9 @@ const EditPage = createClass({
<Nav.section>
{this.renderGoogleDriveIcon()}
{this.renderSaveButton()}
<NewBrew />
<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
</Nav.item>
<PrintLink shareId={this.processShareId()} />
@@ -370,12 +394,12 @@ const EditPage = createClass({
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
<Editor
ref='editor'
value={this.state.brew.text}
brew={this.state.brew}
onChange={this.handleTextChange}
metadata={this.state.brew}
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>
</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{
.navItem.save{
width : 106px;
text-align : center;
position : relative;
&.saved{
cursor : initial;
color : #666;
@@ -21,12 +27,15 @@
margin : -5px;
}
.errorContainer{
animation-name: glideDown;
animation-duration: 0.4s;
position : absolute;
top : 100%;
left : 50%;
z-index : 1000;
z-index : 100000;
width : 140px;
padding : 3px;
color : white;
background-color : #333;
border : 3px solid #444;
border-radius : 5px;

View File

@@ -7,6 +7,7 @@ const { Meta } = require('vitreum/headtags');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const AccountNavItem = require('../../navbar/account.navitem.jsx');
@@ -21,6 +22,9 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const HomePage = createClass({
getDefaultProps : function() {
return {
brew : {
text : ''
},
welcomeText : '',
ver : '0.0.0'
};
@@ -29,13 +33,15 @@ const HomePage = createClass({
},
getInitialState : function() {
return {
text : this.props.welcomeText
brew : {
text : this.props.welcomeText
}
};
},
handleSave : function(){
request.post('/api')
.send({
text : this.state.text
text : this.state.brew.text
})
.end((err, res)=>{
if(err) return;
@@ -48,14 +54,15 @@ const HomePage = createClass({
},
handleTextChange : function(text){
this.setState({
text : text
brew : { text: text }
});
},
renderNavbar : function(){
return <Navbar ver={this.props.ver}>
<Nav.section>
<NewBrewItem />
<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
</Nav.item>
<RecentNavItem />
@@ -71,17 +78,17 @@ const HomePage = createClass({
<div className='content'>
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
<Editor value={this.state.text} onChange={this.handleTextChange} showMetaButton={false} ref='editor'/>
<BrewRenderer text={this.state.text} />
<Editor brew={this.state.brew} onChange={this.handleTextChange} showMetaButton={false} ref='editor'/>
<BrewRenderer text={this.state.brew.text} />
</SplitPane>
</div>
<div className={cx('floatingSaveButton', { show: this.props.welcomeText != this.state.text })} onClick={this.handleSave}>
Save current <i className='fa fa-save' />
<div className={cx('floatingSaveButton', { show: this.props.welcomeText != this.state.brew.text })} onClick={this.handleSave}>
Save current <i className='fas fa-save' />
</div>
<a href='/new' className='floatingNewButton'>
Create your own <i className='fa fa-magic' />
Create your own <i className='fas fa-magic' />
</a>
</div>;
}

View File

@@ -20,9 +20,30 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const KEY = 'homebrewery-new';
const NewPage = createClass({
getDefaultProps : function() {
return {
brew : {
text : '',
shareId : null,
editId : null,
createdAt : null,
updatedAt : null,
gDrive : false,
title : '',
description : '',
tags : '',
published : false,
authors : [],
systems : []
}
};
},
getInitialState : function() {
return {
metadata : {
brew : {
text : this.props.brew.text,
gDrive : false,
title : '',
description : '',
@@ -32,7 +53,6 @@ const NewPage = createClass({
systems : []
},
text : '',
isSaving : false,
saveGoogle : (global.account && global.account.googleId ? true : false),
errors : []
@@ -41,9 +61,9 @@ const NewPage = createClass({
componentDidMount : function() {
const storage = localStorage.getItem(KEY);
if(storage){
if(!this.props.brew.text && storage){
this.setState({
text : storage
brew : { text: storage }
});
}
document.addEventListener('keydown', this.handleControlKeys);
@@ -70,13 +90,13 @@ const NewPage = createClass({
handleMetadataChange : function(metadata){
this.setState({
metadata : _.merge({}, this.state.metadata, metadata)
brew : _.merge({}, this.state.brew, metadata)
});
},
handleTextChange : function(text){
this.setState({
text : text,
brew : { text: text },
errors : Markdown.validate(text)
});
localStorage.setItem(KEY, text);
@@ -92,7 +112,7 @@ const NewPage = createClass({
if(this.state.saveGoogle) {
const res = await request
.post('/api/newGoogle/')
.send(_.merge({}, this.state.metadata, { text: this.state.text }))
.send(this.state.brew)
.catch((err)=>{
console.log(err.status === 401
? 'Not signed in!'
@@ -106,9 +126,7 @@ const NewPage = createClass({
window.location = `/edit/${brew.googleId}${brew.editId}`;
} else {
request.post('/api')
.send(_.merge({}, this.state.metadata, {
text : this.state.text
}))
.send(this.state.brew)
.end((err, res)=>{
if(err){
this.setState({
@@ -122,28 +140,27 @@ const NewPage = createClass({
window.location = `/edit/${brew.editId}`;
});
}
},
renderSaveButton : function(){
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...
</Nav.item>;
} 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
</Nav.item>;
}
},
print : function(){
localStorage.setItem('print', this.state.text);
localStorage.setItem('print', this.state.brew.text);
window.open('/print?dialog=true&local=print', '_blank');
},
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
</Nav.item>;
},
@@ -152,7 +169,7 @@ const NewPage = createClass({
return <Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{this.state.metadata.title}</Nav.item>
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
</Nav.section>
<Nav.section>
@@ -172,12 +189,11 @@ const NewPage = createClass({
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
<Editor
ref='editor'
value={this.state.text}
brew={this.state.brew}
onChange={this.handleTextChange}
metadata={this.state.metadata}
onMetadataChange={this.handleMetadataChange}
/>
<BrewRenderer text={this.state.text} errors={this.state.errors} />
<BrewRenderer text={this.state.brew.text} errors={this.state.errors} />
</SplitPane>
</div>
</div>;

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ const Navbar = require('../../navbar/navbar.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const Account = require('../../navbar/account.navitem.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const BrewItem = require('./brewItem/brewItem.jsx');
// const brew = {
@@ -54,12 +55,13 @@ const UserPage = createClass({
return <div className='userPage page'>
<Navbar>
<Nav.section>
<NewBrew />
<RecentNavItem />
<Account />
</Nav.section>
</Navbar>
<div className='content'>
<div className='content V3'>
<div className='phb'>
<div>
<h1>{this.getUsernameWithS()} brews</h1>

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,73 @@
/* Main Font, serif */
@font-face {
font-family: BookInsanityRemake;
src: url('../fonts/v3/Bookinsanity.woff2');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: BookInsanityRemake;
src: url('../fonts/v3/Bookinsanity Bold.woff2');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: BookInsanityRemake;
src: url('../fonts/v3/Bookinsanity Italic.woff2');
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: BookInsanityRemake;
src: url('../fonts/v3/Bookinsanity Bold Italic.woff2');
font-weight: bold;
font-style: italic;
}
/* Notes and Tables, sans-serif */
@font-face {
font-family: ScalySansRemake;
src: url('../fonts/v3/Scaly Sans.woff2');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: ScalySansRemake;
src: url('../fonts/v3/Scaly Sans Bold.woff2');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: ScalySansRemake;
src: url('../fonts/v3/Scaly Sans Italic.woff2');
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: ScalySansRemake;
src: url('../fonts/v3/Scaly Sans Bold Italic.woff2');
font-weight: bold;
font-style: italic;
}
@font-face {
font-family: ScalySansSmallCapsRemake;
src: url('../fonts/v3/Scaly Sans Caps.woff2');
font-weight: normal;
font-style: normal;
}
/* Headers */
@font-face {
font-family: MrEavesRemake;
src: url('../fonts/v3/Mr Eaves Small Caps.woff2');
font-weight: normal;
font-style: normal;
}
/* Fancy Drop Cap */
@font-face {
font-family: SolberaImitationRemake; //Tweaked v3 version
src: url('../fonts/v3/Solbera Imitation Tweak.woff2');
font-weight: normal;
font-style: normal;
}

View File

@@ -0,0 +1,55 @@
/* Main Font, serif */
@font-face {
font-family: BookSanity;
src: url('../fonts/legacy/Bookinsanity.woff2');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: BookSanity;
src: url('../fonts/legacy/Bookinsanity Bold.woff2');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: BookSanity;
src: url('../fonts/legacy/Bookinsanity Italic.woff2');
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: BookSanity;
src: url('../fonts/legacy/Bookinsanity Bold Italic.woff2');
font-weight: bold;
font-style: italic;
}
/* Notes and Tables, sans-serif */
@font-face {
font-family: ScalySans;
src: url('../fonts/legacy/Scaly Sans.woff2');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: ScalySansSmallCaps;
src: url('../fonts/legacy/Scaly Sans Caps.woff2');
font-weight: normal;
font-style: normal;
}
/* Headers */
@font-face {
font-family: MrJeeves;
src: url('../fonts/legacy/Mr Eaves Small Caps.woff2');
font-weight: normal;
font-style: normal;
}
/* Fancy Drop Cap */
@font-face {
font-family: Solberry;
src: url('../fonts/legacy/Solbera Imitation.woff2');
font-weight: normal;
font-style: normal;
}

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.less';
@import (less) './client/homebrew/phbStyle/phb.assets.less';
@import (less) './client/homebrew/phbStyle/phb.depricated.less';
//Colors
@background : #EEE5CE;
@noteGreen : #e0e5c1;
@@ -17,13 +16,12 @@ body {
-webkit-print-color-adjust : exact;
}
.useSansSerif(){
font-family : ScalySans;
font-family : ScalySansRemake;
font-size : 10pt;
em{
font-family : ScalySans;
font-style : italic;
}
strong{
font-family : ScalySans;
font-weight : 800;
letter-spacing : -0.02em;
}
@@ -40,7 +38,7 @@ body {
-webkit-column-gap : 1cm;
-moz-column-gap : 1cm;
}
.phb{
.phb3{
.useColumns();
counter-increment : phb-page-numbers;
position : relative;
@@ -49,11 +47,10 @@ body {
overflow : hidden;
height : 279.4mm;
width : 215.9mm;
padding : 1.0cm 1.7cm;
padding-bottom : 1.5cm;
padding : 1.0cm 1.7cm 1.5cm;
background-color : @background;
background-image : @backgroundImage;
font-family : BookSanity;
font-family : BookInsanityRemake;
font-size : 0.317cm;
text-rendering : optimizeLegibility;
page-break-before : always;
@@ -62,10 +59,11 @@ body {
// * BASE
// *****************************/
p{
padding-bottom : 0.8em;
overflow-wrap : break-word;
padding-top : 0em;
line-height : 1.3em;
&+p{
margin-top : -0.8em;
padding-top : 0em;
}
}
ul{
@@ -112,7 +110,7 @@ body {
h1,h2,h3,h4{
margin-top : 0.2em;
margin-bottom : 0.2em;
font-family : MrJeeves;
font-family : MrEavesRemake;
font-weight : 800;
color : @headerText;
}
@@ -123,10 +121,23 @@ body {
-moz-column-span : all;
&+p::first-letter{
float : left;
font-family : Solberry;
font-size : 10em;
color : #222;
font-family : SolberaImitationRemake;
line-height : 0.8em;
font-size: 3.5cm;
padding-left: 40px;
margin-left: -40px;
padding-top:10px;
margin-top:-8px;
padding-bottom:10px;
margin-bottom:-20px;
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{
@@ -142,7 +153,7 @@ body {
}
h5{
margin-bottom : 0.2em;
font-family : ScalySansSmallCaps;
font-family : ScalySansSmallCapsRemake;
font-size : 0.423cm;
font-weight : 900;
}
@@ -189,6 +200,7 @@ body {
border-image : @noteBorderImage 11;
border-image-outset : 9px 0px;
box-shadow : 1px 4px 14px #888;
-webkit-transform : translateZ(0); //Prevents shadows from breaking across columns
p, ul{
font-size : 0.352cm;
line-height : 1.1em;
@@ -216,7 +228,7 @@ body {
}
}
h3{
font-family : ScalySans;
font-family : ScalySansRemake;
font-weight : 400;
border-bottom : 1px solid @headerText;
}
@@ -326,12 +338,12 @@ body {
text-indent : -1em;
list-style-type : none;
}
//Column Break
pre, code{
.columnSplit {
visibility : hidden;
-webkit-column-break-after : always;
break-after : always;
-moz-column-break-after : always;
break-before : column;
}
//Avoid breaking up
p,blockquote,table{
@@ -362,7 +374,7 @@ body {
//*****************************
// * SPELL LIST
// *****************************/
.phb .spellList{
.phb3 .spellList{
.useSansSerif();
column-count : 4;
column-span : all;
@@ -388,7 +400,7 @@ body {
//*****************************
// * WIDE
// *****************************/
.phb .wide{
.phb3 .wide{
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
@@ -396,7 +408,7 @@ body {
//*****************************
// * CLASS TABLE
// *****************************/
.phb .classTable{
.phb3 .classTable{
margin-top : 25px;
margin-bottom : 40px;
border-collapse : separate;
@@ -415,11 +427,11 @@ body {
//************************************
// * DESCRIPTIVE TEXT BOX
// ************************************/
.phb .descriptive{
.phb3 .descriptive{
display : block-inline;
margin-bottom : 1em;
background-color : #faf7ea;
font-family : ScalySans;
font-family : ScalySansRemake;
border-style : solid;
border-width : 7px;
border-image : @descriptiveBoxImage 12 stretch;
@@ -434,22 +446,22 @@ body {
padding-top : .8em;
}
em {
font-family : ScalySans;
font-family : ScalySansRemake;
font-style : italic;
}
strong {
font-family : ScalySans;
font-family : ScalySansRemake;
font-weight : 800;
letter-spacing : -0.02em;
}
}
.phb pre+.descriptive{
.phb3 pre+.descriptive{
margin-top : 8px;
}
//*****************************
// * TABLE OF CONTENTS
// *****************************/
.phb .toc{
.phb3 .toc{
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
@@ -468,3 +480,38 @@ body {
margin-bottom : 10px;
}
}
//*****************************
// * MUSTACHE DIVS/SPANS
// *****************************/
.phb3 {
.inline-block {
display : block;
}
}
//*****************************
// * DEFINITION LISTS
// *****************************/
.phb3 {
// dl {
// margin-top: 10px;
// }
dt {
float: left;
//clear: left; //Doesn't seem necessary
margin-right: 5px;
}
// dd {
// margin-left: 0px;
// }
}
//*****************************
// * BLANK LINE
// *****************************/
.phb3 {
.blank {
height: 0.8em;
}
}

View File

@@ -0,0 +1,469 @@
@import (less) './client/homebrew/phbStyle/phb.fontsLegacy.less';
@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>
<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=${`/${name}/bundle.css`} rel='stylesheet'></link>
<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>
</html>
`;
};
};

2122
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
{
"name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown",
"version": "2.10.6",
"version": "2.11.0",
"engines": {
"node": "12.16.x"
"node": "14.15.x"
},
"repository": {
"type": "git",
@@ -40,28 +40,29 @@
]
},
"dependencies": {
"@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"@babel/core": "^7.13.8",
"@babel/preset-env": "^7.13.9",
"@babel/preset-react": "^7.12.13",
"body-parser": "^1.19.0",
"classnames": "^2.2.6",
"codemirror": "^5.59.1",
"codemirror": "^5.59.4",
"cookie-parser": "^1.4.5",
"create-react-class": "^15.7.0",
"express": "^4.17.1",
"express-static-gzip": "2.1.0",
"fs-extra": "9.0.1",
"googleapis": "66.0.0",
"express-static-gzip": "2.1.1",
"fs-extra": "9.1.0",
"googleapis": "67.1.1",
"jwt-simple": "^0.5.6",
"less": "^3.13.1",
"lodash": "^4.17.20",
"marked": "^0.3.19",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"mongoose": "^5.11.9",
"mongoose": "^5.11.18",
"nanoid": "3.1.20",
"nconf": "^0.11.0",
"markedLegacy": "npm:marked@^0.3.19",
"marked": "2.0.1",
"nconf": "^0.11.2",
"prop-types": "15.7.2",
"query-string": "6.13.8",
"query-string": "6.14.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-frame-component": "4.1.3",
@@ -71,7 +72,7 @@
"vitreum": "github:calculuschild/vitreum#21a8e1c9421f1d3a3b474c12f480feb2fbd28c5b"
},
"devDependencies": {
"eslint": "^7.16.0",
"eslint": "^7.21.0",
"eslint-plugin-react": "^7.22.0",
"pico-check": "^2.0.3"
}

View File

@@ -1,2 +1,4 @@
# Notes
User-agent: *
Disallow: /edit/

View File

@@ -19,12 +19,18 @@ const build = async ({ bundle, render, ssr })=>{
await fs.outputFile('./build/homebrew/bundle.css', css);
await fs.outputFile('./build/homebrew/bundle.js', bundle);
await fs.outputFile('./build/homebrew/ssr.js', ssr);
await fs.outputFile('./build/homebrew/render.js', render);
await fs.copy('./client/homebrew/phbStyle/fonts', './build/fonts');
//compress files
await fs.outputFile('./build/homebrew/bundle.css.br', zlib.brotliCompressSync(css));
await fs.outputFile('./build/homebrew/bundle.js.br', zlib.brotliCompressSync(bundle));
await fs.outputFile('./build/homebrew/ssr.js.br', zlib.brotliCompressSync(ssr));
//compress files in production
if(!isDev){
await fs.outputFile('./build/homebrew/bundle.css.br', zlib.brotliCompressSync(css));
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');
@@ -42,6 +48,6 @@ pack('./client/homebrew/homebrew.jsx', {
if(isDev){
livereload('./build');
watchFile('./server.js', {
watch : ['./homebrew'] // Watch additional folders if you want
watch : ['./client'] // Watch additional folders if you want
});
}

View File

@@ -1,21 +1,15 @@
/*eslint max-lines: ["warn", {"max": 250, "skipBlankLines": true, "skipComments": true}]*/
const _ = require('lodash');
const jwt = require('jwt-simple');
const expressStaticGzip = require('express-static-gzip');
const express = require('express');
const app = express();
const homebrewApi = require('./server/homebrew.api.js');
const GoogleActions = require('./server/googleActions.js');
const serveCompressedStaticAssets = require('./server/static-assets.mv.js');
const sanitizeFilename = require('sanitize-filename');
// Serve brotli-compressed static files if available
app.use('/', expressStaticGzip(`${__dirname}/build`, {
enableBrotli : true,
orderPreference : ['br'],
index : false
}));
app.use('/', serveCompressedStaticAssets(`${__dirname}/build`));
process.chdir(__dirname);
@@ -33,7 +27,7 @@ const config = require('nconf')
//DB
const mongoose = require('mongoose');
mongoose.connect(config.get('mongodb_uri') || config.get('mongolab_uri') || 'mongodb://localhost/naturalcrit',
{ retryWrites: false, useNewUrlParser: true });
{ retryWrites: false, useNewUrlParser: true, useCreateIndex: true, useUnifiedTopology: true });
mongoose.connection.on('error', ()=>{
console.log('Error : Could not connect to a Mongo Database.');
console.log(' If you are running locally, make sure mongodb.exe is running.');
@@ -203,6 +197,33 @@ app.get('/edit/:id', (req, res, next)=>{
}
});
//New Page
app.get('/new/:id', (req, res, next)=>{
if(req.params.id.length > 12) {
const googleId = req.params.id.slice(0, -12);
const shareId = req.params.id.slice(-12);
GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, shareId, 'share')
.then((brew)=>{
req.brew = brew; //TODO Need to sanitize later
return next();
})
.catch((err)=>{
console.log(err);
return res.status(400).send('Can\'t get brew from Google');
});
} else {
HomebrewModel.get({ shareId: req.params.id })
.then((brew)=>{
req.brew = brew;
return next();
})
.catch((err)=>{
console.log(err);
return res.status(400).send(`Can't get that`);
});
}
});
//Share Page
app.get('/share/:id', (req, res, next)=>{
if(req.params.id.length > 12) {
@@ -277,6 +298,7 @@ app.use((req, res)=>{
brews : req.brews,
googleBrews : req.googleBrews,
account : req.account,
enable_v3 : config.get('enable_v3')
};
templateFn('homebrew', title = req.brew ? req.brew.title : '', props)
.then((page)=>{ res.send(page); })

View File

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

View File

@@ -3,6 +3,7 @@ const HomebrewModel = require('./homebrew.model.js').model;
const router = require('express').Router();
const zlib = require('zlib');
const GoogleActions = require('./googleActions.js');
const Markdown = require('../shared/naturalcrit/markdown.js');
// const getTopBrews = (cb) => {
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
@@ -10,14 +11,12 @@ const GoogleActions = require('./googleActions.js');
// });
// };
const MAX_TITLE_LENGTH = 100;
const getGoodBrewTitle = (text)=>{
const titlePos = text.indexOf('# ');
if(titlePos !== -1) {
const ending = text.indexOf('\n', titlePos);
return text.substring(titlePos + 2, ending);
} else {
return _.find(text.split('\n'), (line)=>line);
}
const tokens = Markdown.marked.lexer(text);
return (tokens.find((token)=>token.type == 'heading' || token.type == 'paragraph')?.text || 'No Title')
.slice(0, MAX_TITLE_LENGTH);
};
const newBrew = (req, res)=>{
@@ -126,8 +125,6 @@ const newGoogleBrew = async (req, res, next)=>{
req.body = brew;
console.log(oAuth2Client);
const newBrew = await GoogleActions.newGoogleBrew(oAuth2Client, brew);
return res.status(200).send(newBrew);

View File

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

View File

@@ -0,0 +1,31 @@
const expressStaticGzip = require('express-static-gzip');
// Serve brotli-compressed static files if available
const customCacheControlHandler=(response, path)=>{
if(path.endsWith('.br')) {
// Drop .br suffix to help mime understand the actual type of the file
path = path.slice(0, -3);
}
if(path.endsWith('.js') || path.endsWith('.css')) {
// .js and .css files are allowed to be cached up to 12 hours, but then
// they must be revalidated to see if there are any updates
response.setHeader('Cache-Control', 'public, max-age: 43200, must-revalidate');
} else {
// Everything else is cached up to a months as we don't update our images
// or fonts frequently
response.setHeader('Cache-Control', 'public, max-age=2592000, must-revalidate');
}
};
const init=(pathToAssets)=>{
return expressStaticGzip(pathToAssets, {
enableBrotli : true,
orderPreference : ['br'],
index : false,
serveStatic : {
cacheControl : false, // we are going to use custom cache-control
setHeaders : customCacheControlHandler
} });
};
module.exports = init;

View File

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

View File

@@ -34,7 +34,11 @@ const CodeEditor = createClass({
mode : this.props.language,
extraKeys : {
'Ctrl-B' : this.makeBold,
'Ctrl-I' : this.makeItalic
'Cmd-B' : this.makeBold,
'Ctrl-I' : this.makeItalic,
'Cmd-I' : this.makeItalic,
'Ctrl-M' : this.makeSpan,
'Cmd-M' : this.makeSpan,
}
});
@@ -44,8 +48,8 @@ const CodeEditor = createClass({
},
makeBold : function() {
const selection = this.codeMirror.getSelection();
this.codeMirror.replaceSelection(`**${selection}**`, 'around');
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '**' && selection.slice(-2) === '**';
this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `**${selection}**`, 'around');
if(selection.length === 0){
const cursor = this.codeMirror.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
@@ -53,22 +57,27 @@ const CodeEditor = createClass({
},
makeItalic : function() {
const selection = this.codeMirror.getSelection();
this.codeMirror.replaceSelection(`*${selection}*`, 'around');
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '_' && selection.slice(-1) === '_';
this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `_${selection}_`, 'around');
if(selection.length === 0){
const cursor = this.codeMirror.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
}
},
componentWillReceiveProps : function(nextProps){
if(this.codeMirror && nextProps.value !== undefined && this.codeMirror.getValue() != nextProps.value) {
this.codeMirror.setValue(nextProps.value);
makeSpan : function() {
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}';
this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `{{ ${selection}}}`, 'around');
if(selection.length === 0){
const cursor = this.codeMirror.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
}
},
shouldComponentUpdate : function(nextProps, nextState) {
return false;
componentDidUpdate : function(prevProps) {
if(this.codeMirror && this.codeMirror.getValue() != this.props.value) {
this.codeMirror.setValue(this.props.value);
}
},
setCursorPosition : function(line, char){

View File

@@ -1,3 +1,4 @@
/* eslint-disable max-lines */
const _ = require('lodash');
const Markdown = require('marked');
const renderer = new Markdown.Renderer();
@@ -13,6 +14,80 @@ renderer.html = function (html) {
return html;
};
// Don't wrap {{ Divs or {{ empty Spans in <p> tags
renderer.paragraph = function(text){
let match;
if(text.startsWith('<div') || text.startsWith('</div'))
return `${text}`;
else if(match = text.match(/(^|^.*?\n)<span class="inline([^>]*><\/span>)$/))
return `<p>${match[1]}</p><span class="inline-block"${match[2]}`;
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;
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 ${values}>`);
blockCount++;
} else if(block == '}}' && blockCount !== 0){
r.push('</span>');
blockCount--;
}
matchIndex++;
return r;
}, []).join('');
return `${res}`;
} else if(matches = newText.match(blockRegex)) {
//DIV - BLOCK-LEVEL
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) {
return `${text}`;
}
}
};
//Fix local links in the Preview iFrame to link inside the frame
renderer.link = function (href, title, text) {
let self = false;
if(href[0] == '#') {
@@ -79,7 +154,6 @@ const escape = function (html, encode) {
return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
}
}
return html;
};
@@ -95,10 +169,28 @@ const tagRegex = new RegExp(`(${
return `\\<${type}|\\</${type}>`;
}).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 = {
marked : Markdown,
render : (rawBrewText)=>{
blockCount = 0;
rawBrewText = rawBrewText.replace(/^\\column$/gm, `<div class='columnSplit'></div>`)
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`)
.replace(/(?:^|>) *:([^:\n]*):([^\n]*)\n/gm, (match, term, def)=>`<dt>${Markdown.parseInline(term)}</dt><dd>${def}</dd>`)
.replace(/(<dt>.*<\/dt><dd>.*<\/dd>\n?)+/gm, `<dl>$1</dl>\n\n`)
.replace(/^}}/gm, '\n}}')
.replace(/^({{[^\n]*)$/gm, '$1\n');
console.log(rawBrewText);
return Markdown(
sanatizeScriptTags(rawBrewText),
{ 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);
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']);

View File

@@ -1,4 +1,3 @@
//@import (less) 'naturalcrit/styles/style.fonts.css';
nav{
background-color : #333;
.navContent{

View File

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

View File

@@ -15,7 +15,7 @@
height : 100%;
width : 12px;
cursor : ew-resize;
background-color : #ddd;
background-color : #bbb;
text-align : center;
.dots{
display : table-cell;