1
0
mirror of https://github.com/stolksdorf/homebrewery.git synced 2025-12-27 07:41:30 +00:00

Merge branch 'noHtml' into v3

This commit is contained in:
Scott Tolksdorf
2017-02-23 10:07:32 -05:00
72 changed files with 1892 additions and 623 deletions

View File

@@ -1,16 +1,5 @@
Share link to issue brew: http://homebrewery.naturalcrit.com/share/XXXXXXX
### Additional Details
**Share Link** :
or
**Brew code to reproduce** : <details><summary>Click to expand</summary><code><pre>
PASTE BREW CODE HERE
</pre></code></details>

View File

@@ -53,7 +53,13 @@ const Homebrew = React.createClass({
return <PrintPage query={query}/>;
},
'/new' : <NewPage />,
'/changelog' : <SharePage />,
'/test' : <SharePage />,
'/test_old' : <SharePage />,
'*' : <HomePage />,
});
},

View File

@@ -33,7 +33,7 @@ const ContinousSave = React.createClass({
window.onbeforeunload = function(){};
},
actionHandler : function(actionType){
if(actionType == 'UPDATE_BREW_TEXT' || actionType == 'UPDATE_META'){
if(actionType == 'UPDATE_BREW_CODE' || actionType == 'UPDATE_META' || actionType == 'UPDATE_BREW_STYLE'){
Actions.pendingSave();
}
},
@@ -51,6 +51,9 @@ const ContinousSave = React.createClass({
Oops!
<div className='errorContainer'>
Looks like there was a problem saving. <br />
Back up your brew in a text file, just in case.
<br /><br />
Report the issue <a target='_blank' href={'https://github.com/stolksdorf/naturalcrit/issues/new?body='+ encodeURIComponent(errMsg)}>
here
</a>.

View File

@@ -120,7 +120,7 @@
top : 29px;
left : -20px;
z-index : 1000;
width : 120px;
width : 170px;
padding : 8px;
background-color : #333;
a{

View File

@@ -15,20 +15,9 @@ const Utils = require('homebrewery/utils.js');
const Actions = require('homebrewery/brew.actions.js');
const Store = require('homebrewery/brew.store.js');
const SharePage = React.createClass({
getDefaultProps: function() {
return {
brew : {
title : '',
text : '',
shareId : null,
createdAt : null,
updatedAt : null,
views : 0
}
};
},
const Headtags = require('vitreum/headtags');
const SharePage = React.createClass({
componentDidMount: function() {
document.addEventListener('keydown', this.handleControlKeys);
},
@@ -39,9 +28,28 @@ const SharePage = React.createClass({
p : Actions.print
}),
renderMetatags : function(brew){
let metatags = [
<Headtags.meta key='site_name' property='og:site_name' content='Homebrewery'/>,
<Headtags.meta key='type' property='og:type' content='article' />
];
if(brew.title){
metatags.push(<Headtags.meta key='title' property='og:title' content={brew.title} />);
}
if(brew.description){
metatags.push(<Headtags.meta key='description' name='description' content={brew.description} />);
}
if(brew.thumbnail){
metatags.push(<Headtags.meta key='image' property='og:image' content={brew.thumbnail} />);
}
return metatags;
},
render : function(){
const brew = Store.getBrew();
return <div className='sharePage page'>
{this.renderMetatags(brew)}
<Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{brew.title}</Nav.item>
@@ -57,7 +65,7 @@ const SharePage = React.createClass({
</Nav.section>
</Navbar>
<div className='content'>
<BrewRenderer brewText={brew.text} />
<BrewRenderer brew={brew} />
</div>
</div>
}

View File

@@ -1,31 +0,0 @@
.phb{
//Double hr for full width elements
hr+hr+blockquote{
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
}
//*****************************
// * CLASS TABLE
// *****************************/
hr+table{
margin-top : -5px;
margin-bottom : 50px;
padding-top : 10px;
border-collapse : separate;
background-color : white;
border : initial;
border-style : solid;
border-image-outset : 37px 17px;
border-image-repeat : round;
border-image-slice : 150 200 150 200;
border-image-source : @frameBorderImage;
border-image-width : 47px;
}
h5+hr+table{
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 864 B

View File

@@ -6,6 +6,7 @@ module.exports = function(vitreum){
<link href="//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link rel="icon" href="/assets/homebrew/favicon.ico" type="image/x-icon" />
<title>The Homebrewery - NaturalCrit</title>
${vitreum.head}
</head>

View File

@@ -11,8 +11,9 @@
"prod": "set NODE_ENV=production&& npm run build",
"postinstall": "npm run build",
"start": "node server.js",
"test": "mocha test",
"test:dev": "nodemon -x mocha test || exit 0"
"test": "mocha tests",
"test:dev": "nodemon -x mocha tests || exit 0",
"test:markdown": "nodemon -x mocha tests/markdown.test.js || exit 0"
},
"author": "stolksdorf",
"license": "MIT",

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,12 @@
//DEPRICATE
const less = require('less');
const fs = require('fs');
console.log('you should not b using this');
less.render(fs.readFileSync('./client/homebrew/phbStyle/phb.style.less', 'utf8'), {compress : true})
.then((output) => {
fs.writeFileSync('./phb.standalone.css', output.css);

View File

@@ -1,7 +1,6 @@
{
"assets": ["*.png","*.otf", "*.ico"],
"shared":[
],
"assets": ["*.png","*.otf", "*.ico", "*.jpg"],
"shared":["./shared"],
"libs" : [
"react",
"react-dom",
@@ -10,6 +9,7 @@
"codemirror",
"codemirror/mode/gfm/gfm.js",
"codemirror/mode/javascript/javascript.js",
"codemirror/mode/css/css.js",
"moment",
"superagent",
"marked",

View File

@@ -10,10 +10,12 @@ const BrewSchema = mongoose.Schema({
editId : {type : String, default: shortid.generate, index: { unique: true }},
text : {type : String, default : ""},
style : {type : String, default : ""},
title : {type : String, default : ""},
description : {type : String, default : ""},
tags : {type : String, default : ""},
thumbnail : {type : String, default : ""},
systems : [String],
authors : [String],
published : {type : Boolean, default : false},
@@ -22,7 +24,7 @@ const BrewSchema = mongoose.Schema({
updatedAt : { type: Date, default: Date.now},
lastViewed : { type: Date, default: Date.now},
views : {type:Number, default:0},
version : {type: Number, default:1}
version : {type: Number, default:2}
}, {
versionKey: false,
toJSON : {

View File

@@ -1,146 +0,0 @@
const _ = require('lodash');
const Moment = require('moment');
const HomebrewModel = require('./homebrew.model.js').model;
const router = require('express').Router();
//TODO: Possiblity remove
let homebrewTotal = 0;
const refreshCount = ()=>{
HomebrewModel.count({}, (err, total)=>{
homebrewTotal = total;
});
};
refreshCount();
const getTopBrews = (cb)=>{
HomebrewModel.find().sort({views: -1}).limit(5).exec(function(err, brews) {
cb(brews);
});
}
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)=>{
return line;
});
}
};
router.post('/api', (req, res)=>{
let authors = [];
if(req.account) authors = [req.account.username];
const newHomebrew = new HomebrewModel(_.merge({},
req.body,
{authors : authors}
));
if(!newHomebrew.title){
newHomebrew.title = getGoodBrewTitle(newHomebrew.text);
}
newHomebrew.save((err, obj)=>{
if(err){
console.error(err, err.toString(), err.stack);
return res.status(500).send(`Error while creating new brew, ${err.toString()}`);
}
return res.json(obj);
})
});
router.put('/api/update/:id', (req, res)=>{
HomebrewModel.get({editId : req.params.id})
.then((brew)=>{
brew = _.merge(brew, req.body);
brew.updatedAt = new Date();
if(req.account) brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
brew.markModified('authors');
brew.markModified('systems');
brew.save((err, obj)=>{
if(err) throw err;
return res.status(200).send(obj);
})
})
.catch((err)=>{
console.log(err);
return res.status(500).send("Error while saving");
});
});
router.get('/api/remove/:id', (req, res)=>{
HomebrewModel.find({editId : req.params.id}, (err, objs)=>{
if(!objs.length || err) return res.status(404).send("Can not find homebrew with that id");
var resEntry = objs[0];
resEntry.remove((err)=>{
if(err) return res.status(500).send("Error while removing");
return res.status(200).send();
})
});
});
module.exports = router;
/*
module.exports = function(app){
app;
app.get('/api/search', mw.adminOnly, function(req, res){
var page = req.query.page || 0;
var count = req.query.count || 20;
var query = {};
if(req.query && req.query.id){
query = {
"$or" : [{
editId : req.query.id
},{
shareId : req.query.id
}]
};
}
HomebrewModel.find(query, {
text : 0 //omit the text
}, {
skip: page*count,
limit: count*1
}, function(err, objs){
if(err) console.log(err);
return res.json({
page : page,
count : count,
total : homebrewTotal,
brews : objs
});
});
})
return app;
}
*/

View File

@@ -1,81 +0,0 @@
var mongoose = require('mongoose');
var shortid = require('shortid');
var _ = require('lodash');
var HomebrewSchema = mongoose.Schema({
shareId : {type : String, default: shortid.generate, index: { unique: true }},
editId : {type : String, default: shortid.generate, index: { unique: true }},
title : {type : String, default : ""},
text : {type : String, default : ""},
description : {type : String, default : ""},
tags : {type : String, default : ""},
systems : [String],
authors : [String],
published : {type : Boolean, default : false},
createdAt : { type: Date, default: Date.now },
updatedAt : { type: Date, default: Date.now},
lastViewed : { type: Date, default: Date.now},
views : {type:Number, default:0},
version : {type: Number, default:1}
}, { versionKey: false });
HomebrewSchema.methods.sanatize = function(full=false){
const brew = this.toJSON();
delete brew._id;
delete brew.__v;
if(full){
delete brew.editId;
}
return brew;
};
HomebrewSchema.methods.increaseView = function(){
return new Promise((resolve, reject) => {
this.lastViewed = new Date();
this.views = this.views + 1;
this.save((err) => {
if(err) return reject(err);
return resolve(this);
});
});
};
HomebrewSchema.statics.get = function(query){
return new Promise((resolve, reject) => {
Homebrew.find(query, (err, brews)=>{
if(err || !brews.length) return reject('Can not find brew');
return resolve(brews[0]);
});
});
};
HomebrewSchema.statics.getByUser = function(username, allowAccess=false){
return new Promise((resolve, reject) => {
let query = {authors : username, published : true};
if(allowAccess){
delete query.published;
}
Homebrew.find(query, (err, brews)=>{
if(err) return reject('Can not find brew');
return resolve(_.map(brews, (brew)=>{
return brew.sanatize(!allowAccess);
}));
});
});
};
var Homebrew = mongoose.model('Homebrew', HomebrewSchema);
module.exports = {
schema : HomebrewSchema,
model : Homebrew,
}

View File

@@ -1,4 +1,5 @@
const _ = require('lodash');
const fs = require('fs');
const config = require('nconf');
const utils = require('./utils.js');
const BrewData = require('./brew.data.js');
@@ -6,9 +7,12 @@ const router = require('express').Router();
const mw = require('./middleware.js');
const docs = {
welcomeBrew : require('fs').readFileSync('./welcome.brew.md', 'utf8'),
changelog : require('fs').readFileSync('./changelog.md', 'utf8'),
const statics = {
welcomeBrew : fs.readFileSync('./welcome.brew.md', 'utf8'),
changelog : fs.readFileSync('./changelog.md', 'utf8'),
testBrew : fs.readFileSync('./statics/test.brew.md', 'utf8'),
oldTest : fs.readFileSync('./statics/oldTest.brew.md', 'utf8'),
};
@@ -59,6 +63,7 @@ router.get('/user/:username', (req, res, next) => {
//Search Page
router.get('/search', (req, res, next) => {
//TODO: Double check that the defaults are okay
BrewData.search()
.then((brews) => {
req.brews = brews;
@@ -70,7 +75,7 @@ router.get('/search', (req, res, next) => {
//Changelog Page
router.get('/changelog', (req, res, next) => {
req.brew = {
text : docs.changelog,
text : statics.changelog,
title : 'Changelog'
};
return next();
@@ -81,8 +86,26 @@ router.get('/new', renderPage);
//Home Page
router.get('/', (req, res, next) => {
req.brew = { text : docs.welcomeBrew };
req.brew = { text : statics.welcomeBrew };
return next();
}, renderPage);
//Test pages
router.get('/test', (req, res, next) => {
req.brew = {
text : statics.testBrew
};
return next();
}, renderPage);
router.get('/test_old', (req, res, next) => {
req.brew = {
text : statics.oldTest,
version : 1
};
return next();
}, renderPage);
module.exports = router;

View File

@@ -0,0 +1,147 @@
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const Markdown = require('depricated/markdown.old.js');
const ErrorBar = require('./errorBar/errorBar.jsx');
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx')
const Store = require('homebrewery/brew.store.js');
const PAGE_HEIGHT = 1056;
const PPR_THRESHOLD = 50;
const OLD_BrewRenderer = React.createClass({
getDefaultProps: function() {
return {
value : '',
style : '',
errors : []
};
},
getInitialState: function() {
const pages = this.props.value.split('\\page');
return {
viewablePageNumber: 0,
height : 0,
isMounted : false,
pages : pages,
usePPR : pages.length >= PPR_THRESHOLD
};
},
height : 0,
pageHeight : PAGE_HEIGHT,
lastRender : <div></div>,
componentDidMount: function() {
this.updateSize();
window.addEventListener("resize", this.updateSize);
},
componentWillUnmount: function() {
window.removeEventListener("resize", this.updateSize);
},
componentWillReceiveProps: function(nextProps) {
if(this.refs.pages && this.refs.pages.firstChild) this.pageHeight = this.refs.pages.firstChild.clientHeight;
const pages = nextProps.value.split('\\page');
this.setState({
pages : pages,
usePPR : pages.length >= PPR_THRESHOLD
})
},
updateSize : function() {
setTimeout(()=>{
if(this.refs.pages && this.refs.pages.firstChild) this.pageHeight = this.refs.pages.firstChild.clientHeight;
}, 1);
const parentNode = document.querySelector('.page .content');
this.setState({
height : parentNode.clientHeight,
isMounted : true
});
},
handleScroll : function(e){
this.setState({
viewablePageNumber : Math.floor(e.target.scrollTop / this.pageHeight)
});
},
shouldRender : function(pageText, index){
if(!this.state.isMounted) return false;
var viewIndex = this.state.viewablePageNumber;
if(index == viewIndex - 1) return true;
if(index == viewIndex) return true;
if(index == viewIndex + 1) return true;
//Check for style tages
if(pageText.indexOf('<style>') !== -1) return true;
return false;
},
renderPageInfo : function(){
return <div className='pageInfo'>
{this.state.viewablePageNumber + 1} / {this.state.pages.length}
</div>
},
renderPPRmsg : function(){
if(!this.state.usePPR) return;
return <div className='ppr_msg'>
Partial Page Renderer enabled, because your brew is so large. May effect rendering.
</div>
},
renderDummyPage : function(index){
return <div className='phb v1' id={`p${index + 1}`} key={index}>
<i className='fa fa-spinner fa-spin' />
</div>
},
renderPage : function(pageText, index){
return <div className='phb v1' id={`p${index + 1}`} dangerouslySetInnerHTML={{__html:Markdown.render(pageText)}} key={index} />
},
renderPages : function(){
if(this.state.usePPR){
return _.map(this.state.pages, (page, index)=>{
if(this.shouldRender(page, index)){
return this.renderPage(page, index);
}else{
return this.renderDummyPage(index);
}
});
}
if(this.props.errors && this.props.errors.length) return this.lastRender;
this.lastRender = _.map(this.state.pages, (page, index)=>{
return this.renderPage(page, index);
});
return this.lastRender;
},
render : function(){
return <div className='brewRendererOld'
onScroll={this.handleScroll}
ref='main'
style={{height : this.state.height}}>
<ErrorBar errors={this.props.errors} />
<RenderWarnings />
<div className='pages' ref='pages'>
{this.renderPages()}
</div>
{this.renderPageInfo()}
{this.renderPPRmsg()}
</div>
}
});
module.exports = OLD_BrewRenderer;

View File

@@ -0,0 +1,40 @@
@import 'shared/depricated/phb_style_v1/phb.v1.less';
.pane{
position : relative;
}
.brewRendererOld{
overflow-y : scroll;
.pageInfo{
position : absolute;
right : 17px;
bottom : 0;
z-index : 1000;
padding : 8px 10px;
background-color : #333;
font-size : 10px;
font-weight : 800;
color : white;
}
.ppr_msg{
position : absolute;
left : 0px;
bottom : 0;
z-index : 1000;
padding : 8px 10px;
background-color : #333;
font-size : 10px;
font-weight : 800;
color : white;
}
.pages{
margin : 30px 0px;
&>.phb{
margin-right : auto;
margin-bottom : 30px;
margin-left : auto;
box-shadow : 1px 4px 14px #000;
}
}
}

View File

@@ -0,0 +1,73 @@
var React = require('react');
var _ = require('lodash');
var cx = require('classnames');
var ErrorBar = React.createClass({
getDefaultProps: function() {
return {
errors : []
};
},
hasOpenError : false,
hasCloseError : false,
hasMatchError : false,
renderErrors : function(){
this.hasOpenError = false;
this.hasCloseError = false;
this.hasMatchError = false;
var errors = _.map(this.props.errors, (err, idx) => {
if(err.id == 'OPEN') this.hasOpenError = true;
if(err.id == 'CLOSE') this.hasCloseError = true;
if(err.id == 'MISMATCH') this.hasMatchError = true;
return <li key={idx}>
Line {err.line} : {err.text}, '{err.type}' tag
</li>
});
return <ul>{errors}</ul>
},
renderProtip : function(){
var msg = [];
if(this.hasOpenError){
msg.push(<div>
An unmatched opening tag means there's an opened tag that isn't closed, you need to close a tag, like this {'</div>'}. Make sure to match types!
</div>);
}
if(this.hasCloseError){
msg.push(<div>
An unmatched closing tag means you closed a tag without opening it. Either remove it, you check to where you think you opened it.
</div>);
}
if(this.hasMatchError){
msg.push(<div>
A type mismatch means you closed a tag, but the last open tag was a different type.
</div>);
}
return <div className='protips'>
<h4>Protips!</h4>
{msg}
</div>
},
render : function(){
if(!this.props.errors.length) return null;
return <div className='errorBar'>
<i className='fa 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()}
<hr />
{this.renderProtip()}
</div>
}
});
module.exports = ErrorBar;

View File

@@ -0,0 +1,60 @@
.errorBar{
position : absolute;
z-index : 10000;
box-sizing : border-box;
width : 100%;
margin-right : 13px;
padding : 20px;
padding-bottom : 10px;
padding-left : 100px;
background-color : @red;
color : white;
i{
position : absolute;
left : 30px;
opacity : 0.8;
font-size : 3em;
}
h3{
font-size : 1.1em;
font-weight : 800;
}
ul{
margin-top : 15px;
font-size : 0.8em;
list-style-position : inside;
list-style-type : disc;
li{
line-height : 1.6em;
}
}
hr{
box-sizing : border-box;
height : 2px;
width : 150%;
margin-top : 25px;
margin-bottom : 15px;
margin-left : -100px;
background-color : darken(@red, 8%);
border : none;
}
small{
font-size: 0.6em;
opacity: 0.7;
}
.protips{
margin-left : -80px;
font-size : 0.6em;
&>div{
margin-bottom : 10px;
line-height : 1.2em;
}
h4{
opacity : 0.8;
font-weight : 800;
line-height : 1.5em;
text-transform : uppercase;
}
}
}

View File

@@ -0,0 +1,82 @@
var _ = require('lodash');
var Markdown = require('marked');
var 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>')){
var 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>`;
}
return html;
};
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(rawBrewText, {renderer : renderer})
},
validate : (rawBrewText) => {
var errors = [];
var leftovers = _.reduce(rawBrewText.split('\n'), (acc, line, _lineNumber) => {
var lineNumber = _lineNumber + 1;
var 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

@@ -1,47 +1,54 @@
@media print {
.phb.v1{
.descriptive, blockquote{
box-shadow : none;
}
}
}
@import (less) 'shared/naturalcrit/styles/reset.less';
@import (less) './client/homebrew/phbStyle/phb.fonts.css';
@import (less) './client/homebrew/phbStyle/phb.assets.less';
@import (less) './client/homebrew/phbStyle/phb.depricated.less';
//Colors
@background : #EEE5CE;
@noteGreen : #e0e5c1;
@headerUnderline : #c9ad6a;
@horizontalRule : #9c2b1b;
@headerText : #58180D;
@monsterStatBackground : #FDF1DC;
@page { margin: 0; }
body {
counter-reset : phb-page-numbers;
}
*{
-webkit-print-color-adjust : exact;
}
.useSansSerif(){
font-family : ScalySans;
em{
.phb.v1{
@import (less) './phb.fonts.v1.css';
@import (less) './phb.assets.v1.less';
//Colors
@background : #EEE5CE;
@noteGreen : #e0e5c1;
@headerUnderline : #c9ad6a;
@horizontalRule : #9c2b1b;
@headerText : #58180D;
@monsterStatBackground : #FDF1DC;
@page { margin: 0; }
.useSansSerif(){
font-family : ScalySans;
font-style : italic;
em{
font-family : ScalySans;
font-style : italic;
}
strong{
font-family : ScalySans;
font-weight : 800;
letter-spacing : -0.02em;
}
}
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;
}
& *{
-webkit-print-color-adjust : exact;
}
}
.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;
@@ -59,6 +66,7 @@ body {
text-rendering : optimizeLegibility;
page-break-before : always;
page-break-after : always;
//*****************************
// * BASE
// *****************************/
@@ -357,124 +365,157 @@ body {
-webkit-column-break-inside : avoid;
column-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;
column-break-inside : auto;
}
}
//*****************************
// * PRINT
// *****************************/
.phb.print{
blockquote{
box-shadow : none;
}
}
@media print {
.phb .descriptive, .phb blockquote{
box-shadow : none;
}
}
//*****************************
// * 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 : round;
border-image-slice : 150 200 150 200;
border-image-source : @frameBorderImage;
border-image-width : 47px;
h5{
margin-bottom : 10px;
}
}
//*****************************
// * CLASS TABLE
// *****************************/
.phb .descriptive{
display : block-inline;
margin-bottom : 1em;
background-color : #faf7ea;
font-family : ScalySans;
border-style : solid;
border-width : 7px;
border-image : @descriptiveBoxImage 12 round;
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;
column-break-inside : avoid;
a{
color : black;
text-decoration : none;
&:hover{
text-decoration : underline;
//*****************************
// * SPELL LIST
// *****************************/
.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;
column-break-inside : auto;
}
}
ul{
padding-left : 0;
list-style-type : none;
//*****************************
// * PRINT
// *****************************/
&.print{
blockquote{
box-shadow : none;
}
}
&>ul>li{
margin-bottom : 10px;
//*****************************
// * WIDE
// *****************************/
.wide{
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
}
//*****************************
// * CLASS TABLE
// *****************************/
.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 : round;
border-image-slice : 150 200 150 200;
border-image-source : @frameBorderImage;
border-image-width : 47px;
h5{
margin-bottom : 10px;
}
}
//*****************************
// * CLASS TABLE
// *****************************/
.descriptive{
display : block-inline;
margin-bottom : 1em;
background-color : #faf7ea;
font-family : ScalySans;
border-style : solid;
border-width : 7px;
border-image : @descriptiveBoxImage 12 round;
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;
}
}
pre+.descriptive{
margin-top : 8px;
}
//*****************************
// * TABLE OF CONTENTS
// *****************************/
.toc{
-webkit-column-break-inside : avoid;
column-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;
}
}
//*****************************
// * Old Stuff
// *****************************/
//Double hr for full width elements
hr+hr+blockquote{
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
}
//*****************************
// * CLASS TABLE
// *****************************/
hr+table{
margin-top : -5px;
margin-bottom : 50px;
padding-top : 10px;
border-collapse : separate;
background-color : white;
border : initial;
border-style : solid;
border-image-outset : 37px 17px;
border-image-repeat : round;
border-image-slice : 150 200 150 200;
border-image-source : @frameBorderImage;
border-image-width : 47px;
}
h5+hr+table{
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
}
}

View File

@@ -54,10 +54,13 @@ const Actions = {
setBrew : (brew) => {
dispatch('SET_BREW', brew);
},
updateBrewText : (brewText) => {
dispatch('UPDATE_BREW_TEXT', brewText)
updateBrewCode : (brewCode) => {
dispatch('UPDATE_BREW_CODE', brewCode)
},
updateMetaData : (meta) => {
updateBrewStyle : (style) => {
dispatch('UPDATE_BREW_STYLE', style)
},
updateMetadata : (meta) => {
dispatch('UPDATE_META', meta);
},
pendingSave : () => {

View File

@@ -8,6 +8,7 @@ let State = {
brew : {
text : '',
style : '',
shareId : undefined,
editId : undefined,
createdAt : undefined,
@@ -29,9 +30,15 @@ const Store = flux.createStore({
SET_BREW : (brew) => {
State.brew = brew;
},
UPDATE_BREW_TEXT : (brewText) => {
State.brew.text = brewText;
State.errors = Markdown.validate(brewText);
UPDATE_BREW_CODE : (brewCode) => {
State.brew.text = brewCode;
//TODO: Remove?
State.errors = Markdown.validate(brewCode);
},
UPDATE_BREW_STYLE : (style) => {
//TODO: add in an error checker?
State.brew.style = style;
},
UPDATE_META : (meta) => {
State.brew = _.merge({}, State.brew, meta);
@@ -50,11 +57,14 @@ Store.init = (state)=>{
Store.getBrew = ()=>{
return State.brew;
};
Store.getBrewText = ()=>{
Store.getBrewCode = ()=>{
return State.brew.text;
};
Store.getBrewStyle = ()=>{
return State.brew.style;
};
Store.getMetaData = ()=>{
return _.omit(State.brew, ['text']);
return _.omit(State.brew, ['text', 'style']);
};
Store.getErrors = ()=>{
return State.errors;

View File

@@ -6,31 +6,32 @@ const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
const SnippetBar = require('./snippetbar/snippetbar.jsx');
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
const Menubar = require('./menubar/menubar.jsx');
const splice = function(str, index, inject){
return str.slice(0, index) + inject + str.slice(index);
};
const SNIPPETBAR_HEIGHT = 25;
const MENUBAR_HEIGHT = 25;
const BrewEditor = React.createClass({
getDefaultProps: function() {
return {
value : '',
onChange : ()=>{},
brew : {
text : '',
style : '',
},
metadata : {},
onMetadataChange : ()=>{},
onCodeChange : ()=>{},
onStyleChange : ()=>{},
onMetaChange : ()=>{},
};
},
getInitialState: function() {
return {
showMetadataEditor: false
view : 'code', //'code', 'style', 'meta'
};
},
cursorPosition : {
line : 0,
ch : 0
},
componentDidMount: function() {
this.updateEditorSize();
@@ -42,17 +43,15 @@ const BrewEditor = React.createClass({
},
updateEditorSize : function() {
let paneHeight = this.refs.main.parentNode.clientHeight;
paneHeight -= SNIPPETBAR_HEIGHT + 1;
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
if(this.refs.codeEditor){
let paneHeight = this.refs.main.parentNode.clientHeight;
paneHeight -= MENUBAR_HEIGHT + 1;
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
}
},
handleTextChange : function(text){
this.props.onChange(text);
},
handleCursorActivty : function(curpos){
this.cursorPosition = curpos;
},
handleInject : function(injectText){
const lines = this.props.value.split('\n');
lines[this.cursorPosition.line] = splice(lines[this.cursorPosition.line], this.cursorPosition.ch, injectText);
@@ -60,36 +59,51 @@ const BrewEditor = React.createClass({
this.handleTextChange(lines.join('\n'));
this.refs.codeEditor.setCursorPosition(this.cursorPosition.line, this.cursorPosition.ch + injectText.length);
},
handgleToggle : function(){
handleViewChange : function(newView){
this.setState({
showMetadataEditor : !this.state.showMetadataEditor
})
view : newView
}, this.updateEditorSize);
},
brewJump : function(){
const currentPage = this.getCurrentPage();
window.location.hash = 'p' + currentPage;
},
//Called when there are changes to the editor's dimensions
/*
update : function(){
this.refs.codeEditor.updateSize();
if(this.refs.codeEditor) this.refs.codeEditor.updateSize();
},
*/
//TODO: convert this into a generic function for columns and blocks
//MOve this to a util.sj file
highlightPageLines : function(){
if(!this.refs.codeEditor) return;
const codeMirror = this.refs.codeEditor.codeMirror;
const lineNumbers = _.reduce(this.props.value.split('\n'), (r, line, lineNumber)=>{
const lineNumbers = _.reduce(this.props.brew.text.split('\n'), (r, line, lineNumber)=>{
if(line.indexOf('\\page') !== -1){
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
r.push(lineNumber);
}
if(_.startsWith(line, '{{') || _.startsWith(line, '}}')){
codeMirror.addLineClass(lineNumber, 'text', 'block');
}
return r;
}, []);
return lineNumbers
},
/*
renderMetadataEditor : function(){
if(!this.state.showMetadataEditor) return;
return <MetadataEditor
@@ -97,25 +111,50 @@ const BrewEditor = React.createClass({
onChange={this.props.onMetadataChange}
/>
},
*/
renderEditor : function(){
if(this.state.view == 'meta'){
return <MetadataEditor
metadata={this.props.brew}
onChange={this.props.onMetaChange} />
}
if(this.state.view == 'style'){
return <CodeEditor key='style'
ref='codeEditor'
language='css'
value={this.props.brew.style}
onChange={this.props.onStyleChange} />
}
if(this.state.view == 'code'){
return <CodeEditor key='code'
ref='codeEditor'
language='gfm'
value={this.props.brew.text}
onChange={this.props.onCodeChange} />
}
},
render : function(){
this.highlightPageLines();
return<div className='brewEditor' ref='main'>
return <div className='brewEditor' ref='main'>
{/*
<SnippetBar
brew={this.props.value}
onInject={this.handleInject}
onToggle={this.handgleToggle}
showmeta={this.state.showMetadataEditor} />
{this.renderMetadataEditor()}
<CodeEditor
ref='codeEditor'
wrap={true}
language='gfm'
value={this.props.value}
onChange={this.handleTextChange}
onCursorActivity={this.handleCursorActivty} />
*/}
<Menubar
view={this.state.view}
onViewChange={this.handleViewChange}
/>
{this.renderEditor()}
</div>
/*

View File

@@ -8,6 +8,10 @@
background-color : fade(#333, 15%);
border-bottom : #333 solid 1px;
}
.block{
color : blue;
//font-style: italic;
}
}
.brewJump{

View File

@@ -5,9 +5,10 @@ const BrewEditor = require('./brewEditor.jsx')
module.exports = Store.createSmartComponent(BrewEditor, ()=>{
return {
value : Store.getBrewText(),
onChange : Actions.updateBrewText,
metadata : Store.getMetaData(),
onMetadataChange : Actions.updateMetaData,
brew : Store.getBrew(),
onCodeChange : Actions.updateBrewCode,
onStyleChange : Actions.updateBrewStyle,
onMetaChange : Actions.updateMetadata,
};
});

View File

@@ -0,0 +1,35 @@
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const Menubar = React.createClass({
getDefaultProps: function() {
return {
view : '',
onViewChange : ()=>{},
onSnippetInject : ()=>{},
};
},
render: function(){
return <div className='menubar'>
<div className='editors'>
<div className={cx('code', {selected : this.props.view == 'code'})}
onClick={this.props.onViewChange.bind(null, 'code')}>
<i className='fa fa-beer' />
</div>
<div className={cx('style', {selected : this.props.view == 'style'})}
onClick={this.props.onViewChange.bind(null, 'style')}>
<i className='fa fa-paint-brush' />
</div>
<div className={cx('meta', {selected : this.props.view == 'meta'})}
onClick={this.props.onViewChange.bind(null, 'meta')}>
<i className='fa fa-bars' />
</div>
</div>
</div>
}
});
module.exports = Menubar;

View File

@@ -0,0 +1,35 @@
.menubar{
@menuHeight : 25px;
position : relative;
height : @menuHeight;
background-color : #ddd;
.editors{
position : absolute;
display : flex;
top : 0px;
right : 0px;
height : @menuHeight;
width : 90px;
justify-content : space-between;
&>div{
height : @menuHeight;
width : @menuHeight;
cursor : pointer;
line-height : @menuHeight;
text-align : center;
&:hover,&.selected{
background-color : #999;
}
&.code{
.tooltipLeft('Brew Editor');
}
&.style{
.tooltipLeft('Style Editor');
}
&.meta{
.tooltipLeft('Metadata');
}
}
}
}

View File

@@ -74,6 +74,7 @@ const MetadataEditor = React.createClass({
},
renderPublish : function(){
//TODO: Move the publish element into here
if(this.props.metadata.published){
return <button className='unpublish' onClick={this.handlePublish.bind(null, false)}>
<i className='fa fa-ban' /> unpublish
@@ -139,13 +140,12 @@ const MetadataEditor = React.createClass({
<textarea value={this.props.metadata.description} className='value'
onChange={this.handleFieldChange.bind(null, 'description')} />
</div>
{/*}
<div className='field tags'>
<label>tags</label>
<textarea value={this.props.metadata.tags}
onChange={this.handleFieldChange.bind(null, 'tags')} />
<div className='field thumbnail'>
<label>thumbnail</label>
<input type='text' className='value'
value={this.props.metadata.thumbnail}
onChange={this.handleFieldChange.bind(null, 'thumbnail')} />
</div>
*/}
<div className='field systems'>
<label>systems</label>

View File

@@ -1,11 +1,11 @@
.metadataEditor{
position : absolute;
z-index : 10000;
box-sizing : border-box;
width : 100%;
padding : 25px;
background-color : #999;
// background-color : #999;
background-color: white;
.field{
display : flex;
width : 100%;

View File

@@ -2,6 +2,8 @@ const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const OldBrewRenderer = require('depricated/brewRendererOld/brewRendererOld.jsx');
const Markdown = require('homebrewery/markdown.js');
const ErrorBar = require('./errorBar/errorBar.jsx');
@@ -15,12 +17,18 @@ const PPR_THRESHOLD = 50;
const BrewRenderer = React.createClass({
getDefaultProps: function() {
return {
brewText : '',
brew : {
text : '',
style : ''
},
//TODO: maybe remove?
errors : []
};
},
getInitialState: function() {
const pages = this.props.brewText.split('\\page');
const pages = this.props.brew.text.split('\\page');
return {
viewablePageNumber: 0,
@@ -36,16 +44,16 @@ const BrewRenderer = React.createClass({
componentDidMount: function() {
this.updateSize();
window.addEventListener("resize", this.updateSize);
window.addEventListener('resize', this.updateSize);
},
componentWillUnmount: function() {
window.removeEventListener("resize", this.updateSize);
window.removeEventListener('resize', this.updateSize);
},
componentWillReceiveProps: function(nextProps) {
if(this.refs.pages && this.refs.pages.firstChild) this.pageHeight = this.refs.pages.firstChild.clientHeight;
const pages = nextProps.brewText.split('\\page');
const pages = nextProps.brew.text.split('\\page');
this.setState({
pages : pages,
usePPR : pages.length >= PPR_THRESHOLD
@@ -57,6 +65,7 @@ const BrewRenderer = React.createClass({
if(this.refs.pages && this.refs.pages.firstChild) this.pageHeight = this.refs.pages.firstChild.clientHeight;
}, 1);
this.setState({
height : this.refs.main.parentNode.clientHeight,
isMounted : true
@@ -98,13 +107,13 @@ const BrewRenderer = React.createClass({
},
renderDummyPage : function(index){
return <div className='phb' id={`p${index + 1}`} key={index}>
return <div className='phb v2' id={`p${index + 1}`} key={index}>
<i className='fa fa-spinner fa-spin' />
</div>
},
renderPage : function(pageText, index){
return <div className='phb' id={`p${index + 1}`} dangerouslySetInnerHTML={{__html:Markdown.render(pageText)}} key={index} />
return <div className='phb v2' id={`p${index + 1}`} dangerouslySetInnerHTML={{__html:Markdown.render(pageText)}} key={index} />
},
renderPages : function(){
@@ -125,6 +134,9 @@ const BrewRenderer = React.createClass({
},
render : function(){
if(this.props.brew.version == 1) return <OldBrewRenderer value={this.props.brew.text} />;
return <div className='brewRenderer'
onScroll={this.handleScroll}
ref='main'
@@ -133,6 +145,9 @@ const BrewRenderer = React.createClass({
<ErrorBar errors={this.props.errors} />
<RenderWarnings />
<style>{this.props.brew.style}</style>
<div className='pages' ref='pages'>
{this.renderPages()}
</div>

View File

@@ -1,5 +1,5 @@
@import (less) './client/homebrew/phbStyle/phb.style.less';
@import (less) './shared/homebrewery/phb_style/phb.less';
.pane{
position : relative;
}

View File

@@ -3,8 +3,16 @@ const BrewRenderer = require('./brewRenderer.jsx');
module.exports = Store.createSmartComponent(BrewRenderer, () => {
const brew = Store.getBrew();
return {
brewText : Store.getBrewText(),
brew : Store.getBrew(),
brewText : Store.getBrewCode(),
style : Store.getBrewStyle(),
errors : Store.getErrors()
}
});

View File

@@ -1,82 +1,44 @@
var _ = require('lodash');
var Markdown = require('marked');
var renderer = new Markdown.Renderer();
const _ = require('lodash');
const Markdown = require('marked');
//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>')){
var 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>`;
}
return html;
const renderer = new Markdown.Renderer();
let blockCount = 0;
renderer.paragraph = function(text){
const blockReg = /{{[\w|,]+|}}/g;
const matches = text.match(blockReg);
if(!matches) return `\n<p>${text}</p>\n`;
let matchIndex = 0;
const res = _.reduce(text.split(blockReg), (r, text) => {
if(text) r.push(Markdown(text, {renderer : renderer, sanitize: true}));
const block = matches[matchIndex];
if(block && block[0] == '{'){
r.push(`\n\n<div class="block ${block.substring(2).split(',').join(' ')}">`);
blockCount++;
}
if(block == '}}' && blockCount !== 0){
r.push('</div>\n\n');
blockCount--;
}
matchIndex++;
return r;
}, []).join('\n');
return res;
};
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(rawBrewText, {renderer : renderer})
blockCount = 0;
let html = Markdown(rawBrewText, {renderer : renderer, sanitize: true});
//Close all hanging block tags
html += _.times(blockCount, ()=>{return '</div>'}).join('\n');
return html;
},
validate : (rawBrewText) => {
var errors = [];
var leftovers = _.reduce(rawBrewText.split('\n'), (acc, line, _lineNumber) => {
var lineNumber = _lineNumber + 1;
var 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;
return [];
},
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

Before

Width:  |  Height:  |  Size: 327 B

After

Width:  |  Height:  |  Size: 327 B

View File

Before

Width:  |  Height:  |  Size: 530 B

After

Width:  |  Height:  |  Size: 530 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

View File

@@ -0,0 +1,24 @@
//TODO: come up with fun color names
@background : #EEE5CE;
@noteGreen : #e0e5c1;
@headerUnderline : #c9ad6a;
@horizontalRule : #9c2b1b;
@headerText : #58180D;
@monsterStatBackground : #FDF1DC;
@teal : blue;
.colorElements(@color){
table tbody{
tr:nth-child(odd){
background-color : @color;
}
}
}
//TODO make a color mixin generator
.teal{ .colorElements(@teal); }

View File

@@ -0,0 +1,70 @@
/* Main Font */
@font-face {
font-family: BookInsanity;
src: url('/assets/homebrewery/phb_style/fonts/Bookinsanity.otf');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: BookInsanity;
src: url('/assets/homebrewery/phb_style/fonts/Bookinsanity Bold.otf');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: BookInsanity;
src: url('/assets/homebrewery/phb_style/fonts/Bookinsanity Italic.otf');
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: BookInsanity;
src: url('/assets/homebrewery/phb_style/fonts/Bookinsanity Bold Italic.otf');
font-weight: bold;
font-style: italic;
}
/* Notes and Tables */
@font-face {
font-family: ScalySans;
src: url('/assets/homebrewery/phb_style/fonts/Scaly Sans.otf');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: ScalySansSmallCaps;
src: url('/assets/homebrewery/phb_style/fonts/Scaly Sans Caps.otf');
font-weight: normal;
font-style: normal;
}
/* Fancy First Letter */
@font-face {
font-family: Solbera;
src: url('/assets/homebrewery/phb_style/fonts/Solbera Imitation.otf');
font-weight: normal;
font-style: normal;
}
/* Headers */
@font-face {
font-family: MrEaves;
src: url('/assets/homebrewery/phb_style/fonts/Mr Eaves Small Caps.otf');
font-weight: normal;
font-style: normal;
}
//TODO: move the useSansSerif into here
.useSansSerif(){
font-family : ScalySans;
em{
font-family : ScalySans;
font-style : italic;
}
strong{
font-family : ScalySans;
font-weight : 800;
letter-spacing : -0.02em;
}
}

View File

@@ -0,0 +1,12 @@
@footerImg : url('/assets/homebrewery/phb_style/img/footer.png');
@dividerImg : url('/assets/homebrewery/phb_style/img/divider.png');
@frameBorder : url('/assets/homebrewery/phb_style/img/frame_border.png');
@monsterBorder : url('/assets/homebrewery/phb_style/img/monster_border.png');
@noteBorder : url('/assets/homebrewery/phb_style/img/note_border.png');
@descriptiveBorder : url('/assets/homebrewery/phb_style/img/desc_border.png');
@phbBG : url('/assets/homebrewery/phb_style/img/phb_bg.jpg');
@darkBG : url('/assets/homebrewery/phb_style/img/phb_dark_bg.jpg');
@dmgBG : url('/assets/homebrewery/phb_style/img/dmg_bg.jpg');

View File

@@ -0,0 +1,505 @@
@media print {
.phb.v2{
.descriptive, blockquote{
box-shadow : none;
}
}
}
.phb.v2{
@import './phb.fonts.less';
@import './phb.colors.less';
@import './phb.img.less';
@page { margin: 0; }
.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;
}
& *{
-webkit-print-color-adjust : exact;
}
.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 : @phbBG;
font-family : BookInsanity;
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 : MrEaves;
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 : Solbera;
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{
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 : @noteBorder 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 : @monsterBorder 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 : @dividerImg;
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 : @footerImg;
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;
column-break-inside : avoid;
overflow: hidden; /* Firefox fix */
}
//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;
column-break-inside : avoid;
}
//*****************************
// * SPELL LIST
// *****************************/
.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;
column-break-inside : auto;
}
}
//*****************************
// * PRINT
// *****************************/
&.print{
blockquote{
box-shadow : none;
}
}
//*****************************
// * WIDE
// *****************************/
.wide{
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
}
//*****************************
// * CLASS TABLE
// *****************************/
.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 : round;
border-image-slice : 150 200 150 200;
border-image-source : @frameBorder;
border-image-width : 47px;
h5{
margin-bottom : 10px;
}
}
//*****************************
// * CLASS TABLE
// *****************************/
.descriptive{
display : block-inline;
margin-bottom : 1em;
background-color : #faf7ea;
font-family : ScalySans;
border-style : solid;
border-width : 7px;
border-image : @descriptiveBorder 12 round;
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;
}
}
pre+.descriptive{
margin-top : 8px;
}
//*****************************
// * TABLE OF CONTENTS
// *****************************/
.toc{
-webkit-column-break-inside : avoid;
column-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;
}
}
//*****************************
// * Old Stuff
// *****************************/
//Double hr for full width elements
hr+hr+blockquote{
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
}
//*****************************
// * CLASS TABLE
// *****************************/
hr+table{
margin-top : -5px;
margin-bottom : 50px;
padding-top : 10px;
border-collapse : separate;
background-color : white;
border : initial;
border-style : solid;
border-image-outset : 37px 17px;
border-image-repeat : round;
border-image-slice : 150 200 150 200;
border-image-source : @frameBorder;
border-image-width : 47px;
}
h5+hr+table{
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
}
}

View File

@@ -1,70 +1,70 @@
var React = require('react');
var _ = require('lodash');
var cx = require('classnames');
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
var CodeMirror;
let CodeMirror;
if(typeof navigator !== 'undefined'){
var CodeMirror = require('codemirror');
CodeMirror = require('codemirror');
//Language Modes
require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown
require('codemirror/mode/javascript/javascript.js');
require('codemirror/mode/css/css.js');
}
var CodeEditor = React.createClass({
const CodeEditor = React.createClass({
getDefaultProps: function() {
return {
value : '',
language : '',
value : '',
wrap : false,
onChange : function(){},
onCursorActivity : function(){},
wrap : true,
onChange : ()=>{},
};
},
componentDidMount: function() {
this.codeMirror = CodeMirror(this.refs.editor,{
value : this.props.value,
lineNumbers: true,
lineWrapping : this.props.wrap,
mode : this.props.language
});
this.codeMirror.on('change', this.handleChange);
this.codeMirror.on('cursorActivity', this.handleCursorActivity);
this.updateSize();
},
componentWillReceiveProps: function(nextProps){
if(this.props.language !== nextProps.language){
this.buildEditor();
}
if(this.codeMirror && nextProps.value !== undefined && this.codeMirror.getValue() != nextProps.value) {
this.codeMirror.setValue(nextProps.value);
}
},
shouldComponentUpdate: function(nextProps, nextState) {
return false;
},
componentDidMount: function() {
this.buildEditor();
},
buildEditor : function(){
this.codeMirror = CodeMirror(this.refs.editor,{
value : this.props.value,
lineNumbers : true,
lineWrapping : this.props.wrap,
mode : this.props.language,
indentWithTabs : true,
tabSize : 2
});
this.codeMirror.on('change', ()=>{
this.props.onChange(this.codeMirror.getValue());
});
this.updateSize();
},
//Externally Used
setCursorPosition : function(line, char){
setTimeout(()=>{
this.codeMirror.focus();
this.codeMirror.doc.setCursor(line, char);
}, 10);
},
getCursorPosition : function(){
return this.codeMirror.getCursor();
},
updateSize : function(){
this.codeMirror.refresh();
},
handleChange : function(editor){
this.props.onChange(editor.getValue());
},
handleCursorActivity : function(){
this.props.onCursorActivity(this.codeMirror.doc.getCursor());
},
render : function(){
return <div className='codeEditor' ref='editor' />
}

44
statics/oldTest.brew.md Normal file
View File

@@ -0,0 +1,44 @@
# Old test!
#### Create Acne
*5th-level illusion*
___
- **Casting Time:** 1 action
- **Range:** 60 feet
- **Components:** V, M (a crushed button worth at least 1cp, a small doll, discarded gum wrapper)
- **Duration:** Instantaneous
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.
<div class='classTable'>
##### The Archivist
| Level | Proficiency Bonus | Features | Statistical Occultism|
|:---:|:---:|:---|:---:|
| 1st | +2 | Astrological Botany | +1 |
| 2nd | +2 | Genetic Banishing | +1 |
| 3rd | +2 | Genetic Banishing | +2 |
| 4th | +2 | Ability Score Improvement, Statistical Occultism | +3 |
| 5th | +3 | Demonic Anthropology | +4 |
| 6th | +3 | Ability Score Improvement | +5 |
| 7th | +3 | Astrological Chemistry | +5 |
| 8th | +3 | Ability Score Improvement, Plasma Outlaw | +6 |
| 9th | +4 | Consecrated Biochemistry | +7 |
| 10th | +4 | Statistical Occultism | +8 |
| 11th | +4 | Ritual Astronomy | +8 |
| 12th | +4 | Ability Score Improvement | +8 |
| 13th | +5 | Astrological Botany | +9 |
| 14th | +5 | Ability Score Improvement | +10 |
| 15th | +5 | Divinatory Mineralogy | +10 |
| 16th | +5 | Ability Score Improvement, Gunpowder Torturer | +10 |
| 17th | +6 | ─ | +11 |
| 18th | +6 | Infernal Banker | +11 |
| 19th | +6 | Ability Score Improvement | +12 |
| 20th | +6 | Infernal Banker | +12 |
</div>
\page
yo oy oyoyoyo

129
statics/test.brew.md Normal file
View File

@@ -0,0 +1,129 @@
# The Homebrewery
Welcome traveler from an antique land. Please sit and tell us of what you have seen. The unheard of monsters, who slither and bite. Tell us of the wondrous items and and artifacts you have found, their mysteries yet to be unlocked. Of the vexing vocations and surprising skills you have seen.
### Homebrew D&D made easy
The Homebrewery makes the creation and sharing of authentic looking Fifth-Edition homebrews easy. It uses [Markdown](https://help.github.com/articles/markdown-basics/) with a little CSS magic to make your brews come to life.
**Try it! **Simply edit the text on the left and watch it *update live* on the right.
### Editing and Sharing
When you create your own homebrew you will be given a *edit url* and a *share url*. Any changes you make will be automatically saved to the database within a few seconds. Anyone with the edit url will be able to make edits to your homebrew. So be careful about who you share it with.
Anyone with the *share url* will be able to access a read-only version of your homebrew.
## Helping out
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/stolksdorf) to help me keep the servers running.
This tool will **always** be free, never have ads, and I will never offer any "premium" features or whatever.
>##### PDF Exporting
> PDF Printing works best in Chrome. If you are having quality/consistency issues, try using Chrome to print instead.
>
> After clicking the "Print" item in the navbar a new page will open and a print dialog will pop-up.
> * Set the **Destination** to "Save as PDF"
> * Set **Paper Size** to "Letter"
> * If you are printing on A4 paper, make sure to have the "A4 page size snippet" in your brew
> * In **Options** make sure "Background Images" is selected.
> * Hit print and enjoy! You're done!
>
> If you want to save ink or have a monochrome printer, add the **Ink Friendly** snippet to your brew before you print
```
```
## Big things coming in v3.0.0
With the next major release of Homebrewery, v3.0.0, this tool *will no longer support raw HTML input for brew code*. All brews made previous to the release of v3.0.0 will still render normally.
## New Things All The Time!
What's new in the latest update? Check out the full changelog [here](/changelog)
### Bugs, Issues, Suggestions?
Have an idea of how to make The Homebrewery better? Or did you find something that wasn't quite right? Head [here](https://github.com/stolksdorf/homebrewery/issues/new) and let me know!.
### Legal Junk
The Homebrewery is licensed using the [MIT License](https://github.com/stolksdorf/homebrewery/blob/master/license). Which means you are free to use The Homebrewery is any way that you want, except for claiming that you made it yourself.
If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
### More Resources
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/comments/3uwxx9/resources_open_to_the_community/).
<img src='http://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:50px;right:30px;width:280px' />
<div class='pageNumber'>1</div>
<div class='footnote'>PART 1 | FANCINESS</div>
\page
{{classTable,wide
##### The Archivist
| Level | Proficiency Bonus | Features | Statistical Occultism|
|:---:|:---:|:---|:---:|
| 1st | +2 | Astrological Botany | +1 |
| 2nd | +2 | Genetic Banishing | +1 |
| 3rd | +2 | Genetic Banishing | +2 |
| 4th | +2 | Ability Score Improvement, Statistical Occultism | +3 |
| 5th | +3 | Demonic Anthropology | +4 |
| 6th | +3 | Ability Score Improvement | +5 |
| 7th | +3 | Astrological Chemistry | +5 |
| 8th | +3 | Ability Score Improvement, Plasma Outlaw | +6 |
| 9th | +4 | Consecrated Biochemistry | +7 |
| 10th | +4 | Statistical Occultism | +8 |
| 11th | +4 | Ritual Astronomy | +8 |
| 12th | +4 | Ability Score Improvement | +8 |
| 13th | +5 | Astrological Botany | +9 |
| 14th | +5 | Ability Score Improvement | +10 |
| 15th | +5 | Divinatory Mineralogy | +10 |
| 16th | +5 | Ability Score Improvement, Gunpowder Torturer | +10 |
| 17th | +6 | ─ | +11 |
| 18th | +6 | Infernal Banker | +11 |
| 19th | +6 | Ability Score Improvement | +12 |
| 20th | +6 | Infernal Banker | +12 |
}}
# Appendix
### Not quite Markdown
Although the Homebrewery uses Markdown, to get all the styling features from the PHB, we had to get a little creative. Some base HTML elements are not used as expected and I've had to include a few new keywords.
___
* **Horizontal Rules** are generally used to *modify* existing elements into a different style. For example, a horizontal rule before a blockquote will give it the style of a Monster Stat Block instead of a note.
* **New Pages** are controlled by the author. It's impossible for the site to detect when the end of a page is reached, so indicate you'd like to start a new page, use the new page snippet to get the syntax.
* **Code Blocks** are used only to indicate column breaks. Since they don't allow for styling within them, they weren't that useful to use.
* **HTML** can be used to get *just* the right look for your homebrew. I've included some examples in the snippet icons above the editor.
```
```
### Images
Images must be hosted online somewhere, like imgur. You use the address to that image to reference it in your brew. Images can be included 'inline' with the text using Markdown-style images. However for background images more control is needed.
Background images should be included as HTML-style img tags. Using inline CSS you can precisely position your image where you'd like it to be. I have added both a inflow image snippet and a background image snippet to give you exmaples of how to do it.
### Crediting Me
If you'd like to credit The Homebrewery in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
<div class='pageNumber'>2</div>
<div class='footnote'>PART 2 | BORING STUFF</div>

View File

View File

@@ -8,7 +8,7 @@ module.exports = {
random : (num = 20)=>{
return _.times(num, ()=>{
//TODO: Build better generator
//TODO: Build better generator, use new snippets?
return {
title : 'BrewA',
description : '',
@@ -20,6 +20,20 @@ module.exports = {
};
});
},
old : () => {
return [
{
title : 'OLD - div test',
description : '',
authors : ['old'],
text : `<div class='wide' style='text-align:center'> neato </div>`,
systems : [],
views : 0,
published : true,
version : 1
}
]
},
static : () => {
return {
BrewA : {

View File

@@ -88,6 +88,9 @@ describe('Brew Data', () => {
}).then((brew) => {
brew.should.have.property('title').equal('Actual Title');
});
});
it.skip('should work with "# title"', ()=>{
});
it('should use the first header found if no title provided', () => {
return BrewData.create({

20
tests/markdown.test.js Normal file
View File

@@ -0,0 +1,20 @@
const testing = require('./test.init.js');
const Markdown = require('../shared/homebrewery/markdown.js');
describe('Markdown', ()=>{
it('should do a thing', ()=>{
const res = Markdown.render(`
test
<div> cool stuff </div>
test2
`)
console.log(res);
});
});

113
todo.backup.md Normal file
View File

@@ -0,0 +1,113 @@
## Session Todo
- Add in the `/test` page
- Add in the `/static` folder, welcome, changelog, testBrew
- Add in a /style route for seeing a brew's style
- Clicking source should open both
- Setup a basic search page. (just loads all the brews right now).
- Make sure the brewrenderer is working perfectly
- Make sure the brewrenderer is loading the depricated styles as well
- Generate the old phb styles?
- Remove inlining the images and fonts
- Move the phb styling into `/shared/phb_style`
- Break out the fonts and images
-
### v3.0.0
- [x] Add better error messaging for not having mongo running
- [x] Remove Docker support
- [x] Improve docs, explain what that project is
- [ ] Default node_env in configs using nconf (default to local)
- [x] Make a `dev.routes.js` file to store things like login and search
- [x] Figure out why prod builds are breaking (set the right babelrc?)
- [x] Remove old brew editor and brew renderer files
- [x] Change icon on editor bar to a menu icon
- [x] Upgrade to Vitreum v4
- [x] Add in `egads` for error handling
- [x] Fix Changelog spacing
- [ ] Add `NODE_ENV` vars to staging and prod servers
- [ ] Add your own MIT license link, https://github.com/remy/mit-license
- [ ] Remove `license` file, move it to the readme
- [x] split out a brew.routes, brew.api, dev.routes, and admin.routes
- [x] Add a thumbnail link to the metadata
- [ ] Share page should still load with JS turned off
#### Account
- [x] Make login self contained
- [x] Add a faked internal login page/route
- [ ] Fix styling on user page for items
- [ ] Add in a logout to user page
#### Search
- [x] Implement the search api, with pagniation
- [x] Add in a `search.test.js` for deedicated search tests
- [x] Finish tests for sorting and pagniation
- [ ] Add in a `markdown.test.js` file
#### Admin
- [x] Fix search API for admin?
- [x] Add a share -> edit id lookup on admin page
- [x] Add a version field to each brew
- [x] Should use a header to store and send the admin key, not a query param
#### Editing
- [x] Remove support for HTML tags
- [x] Make a separate style editor
- [ ] Add in a `\column` to replace the code blocks
- [ ] Add in styling for the code blocks now
- [x] Add in a new 'block' markdown element that creates simple divs with classes `{{myBlock }}`
- [ ] Add syntax highlighting for the new blocks
- [x] In the markdown parser, if unclosed blocks, add them to the end of the page.
- [x] Add comma support to the block syntax
- [ ] Have a triple ediotr selector in the menubar for markdown, style, and meta
- [ ] Add a css validator for the style
- [ ] Propogate up both markdown and style errors (maybe?)
~~- [ ] Make a way to have people add 'id' to blocks? NOPE, bad idea, can't use PPR then~~
- [ ] Make a test brew that loads the edit page at `/test`
- This should use all the snippets and features through the doc
#### Rendering
- [ ] Rewrite the phb.style to support the new div-centered
- [ ] Add a 'brushed' class using the borderimage prop to add a brushed border (http://homebrewery.naturalcrit.com/share/HJmEWQCEj)
- [ ] If ver1 of brew, uses old rendering code and style
- [ ] Cold storage one last compile of the phb.old.css to use for old rendering
- [ ] Move all 'old' files (markdown renderer, old phb style etc.) to it's own folder. (./shared/depreciated)
- [ ] Headers should add their content to an id
- [ ] Switch over to completely use PPR
- [ ] Blend the box-shadow into the border images
- [ ] Make the horizontal rule have a better background image for the red triangle
- [ ] Notes and monsterblocks should just use blocks now,
- [ ] Have blockquotes make actual quotes (PHB p.18)
- [ ] Add color modifier classes (green, purple, brown, teal), used for notes and tables
#### UX
- [x] Add syntax highlighting to a `\page` line
- [ ] Look into snapping from editor position -> renderer position
- [x] Break the ctrl => keys into a little util
- [x] Add the metadata editor button back to homepage
- [ ] Create a `/docs` folder for the markdown text to auto-read it
- [ ] Add in a faq.md, and move the changelog.d and welcome.md here
- [ ] On Split move, fix the cursor position
- Maybe have to add a component ref passthrough to pico-flux
- [ ] Make a smart brewtitle navitem
- [ ] Can pass prop to make it editable via actions
- [x] Make a navitems.js file which makes to all the navitems
- [ ] Add a `small` modifier to nav items to shrink the text
- [ ] Make the recent brew nav item work with the new errors to remove
- [ ] Clean up the recent nav item to just show both
#### Snippets
- [ ] Add a 'credits' snippet
- [ ] Change snippetbar to menubar
- [ ] Split snippets up into two folders, for style and markdown.
- [ ] Move snippets into shared
- [ ] Split each snippet into it's own file? Or at least organize them better