0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-26 00:53:05 +00:00

Compare commits

..

42 Commits
noHtml ... v3

Author SHA1 Message Date
Scott Tolksdorf
b39f9041c2 Updating DB to handle newer versions of mongo 2017-05-28 11:24:50 -04:00
Scott Tolksdorf
2023ae4f6a Merge branch 'borderShadows' into v3 2017-03-26 15:18:06 -04:00
Scott Tolksdorf
d5f04ca2b6 Shadows now being drawn as a :after element. Beauty 2017-03-26 15:10:36 -04:00
Scott Tolksdorf
f99bcabad0 Updating the build scripts 2017-03-26 12:20:17 -04:00
Scott Tolksdorf
32317bfa6f Merge branch 'collaspeNav' into snippets 2017-03-21 00:15:34 -04:00
Scott Tolksdorf
1946a50ce0 Converted a few nav items over 2017-03-21 00:15:24 -04:00
Scott Tolksdorf
ee1827eab0 Trying to get it working 2017-03-21 00:09:37 -04:00
Scott Tolksdorf
20371a8b3d Adding the brew title to the print page title, so downloads are named properly 2017-03-19 20:23:19 -04:00
Scott Tolksdorf
28a3f31caa Work on the Table of contents snippet 2017-03-19 17:26:55 -04:00
Scott Tolksdorf
c647bdf5ee Added in stlying for blockquotes and clean up logic in footer 2017-03-19 15:07:00 -04:00
Scott Tolksdorf
eb1827cedb Merge branch 'borderTest' into snippets 2017-03-19 13:51:24 -04:00
Scott Tolksdorf
a3251dfa19 Pseudo borders are now working 2017-03-19 13:50:45 -04:00
Scott Tolksdorf
30d3fcf168 Pseudo element borders are working, holy shit 2017-03-19 13:17:33 -04:00
Scott Tolksdorf
393df1b181 Updating the faqw 2017-03-19 12:28:45 -04:00
Scott Tolksdorf
94a3a96960 Adding to faq 2017-03-03 19:20:27 -05:00
Scott Tolksdorf
bfb2cea48e Working on onster block 2017-02-28 23:21:41 -05:00
Scott Tolksdorf
00f2703d0b Moved files into statics, finally fixed the brew editor breaking on resizze 2017-02-28 21:07:37 -05:00
Scott Tolksdorf
4c874149fb added an internal nested div on block elements 2017-02-26 20:18:44 -05:00
Scott Tolksdorf
0705e08381 Added in stlying for code blocks 2017-02-26 19:49:36 -05:00
Scott Tolksdorf
e112808706 Column split now a key word 2017-02-26 19:38:42 -05:00
Scott Tolksdorf
234d216d64 Adding in notes and adding more to blocks 2017-02-26 17:42:21 -05:00
Scott Tolksdorf
ef0265f4fa moving old snippets into the depricated folder 2017-02-26 13:03:12 -05:00
Scott Tolksdorf
fbc18a017c Creating new stlying for the snippet blocks 2017-02-26 13:01:48 -05:00
Scott Tolksdorf
9d4d337bb9 Snippet bar has been replaced and new style of snippets being worked on 2017-02-26 11:53:40 -05:00
Scott Tolksdorf
0d0ce101f3 Starting to set up the snippets 2017-02-24 00:49:21 -05:00
Scott Tolksdorf
540c00cb0c Merge branch 'noHtml' into v3 2017-02-23 10:07:32 -05:00
Scott Tolksdorf
446ae9cbcf Merge branch 'dualRenderer' into noHtml 2017-02-23 10:07:05 -05:00
Scott Tolksdorf
dc486cfba9 Added nested markdown parsering within blocks 2017-02-23 10:03:04 -05:00
Scott Tolksdorf
a6a1f41e77 Merge branch 'newStyle' into dualRenderer 2017-02-23 09:58:38 -05:00
Scott Tolksdorf
fd567352a4 Moved imgs and fonts into the new style folder 2017-02-23 09:58:10 -05:00
Scott Tolksdorf
a33b1d845d Styling is finally split, oh boy 2017-02-23 08:33:13 -05:00
Scott Tolksdorf
b20f4ffb46 PHB style should be fully scoped 2017-02-23 08:11:48 -05:00
Scott Tolksdorf
2f69ef3fe8 Removing the old server files 2017-02-23 07:41:55 -05:00
Scott Tolksdorf
1da1f90a35 Backing up the todo 2017-02-18 14:29:16 -05:00
Scott Tolksdorf
bd08858745 Split off the old stlying in a separate file 2017-02-13 00:45:17 -05:00
Scott Tolksdorf
304cd0ffcd Getting both renderers to play nice 2017-02-12 23:35:19 -05:00
Scott Tolksdorf
b40e5bc4c4 simplifying the issue template, because no one ever actually uses it 2017-02-12 10:21:02 -05:00
Scott Tolksdorf
0663737e1c moved the old parser and renderer into a depreciated folder 2017-02-04 03:27:10 -05:00
Scott Tolksdorf
307dd2d9ba Adding newlines to div injection for blocks 2017-02-01 23:54:20 -05:00
Scott Tolksdorf
95c91b6ba8 Merge branch 'styleEditor' into noHtml 2017-01-30 10:48:34 -05:00
Scott Tolksdorf
c8c46725a2 Making the error looks better 2017-01-30 10:48:05 -05:00
Scott Tolksdorf
7001b71d91 Lots of progress with the new editor 2017-01-28 22:19:14 -05:00
126 changed files with 3419 additions and 923 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

@@ -2,7 +2,12 @@ var React = require('react');
var Nav = require('naturalcrit/nav/nav.jsx');
module.exports = function(props){
return <Nav.item newTab={true} href='https://github.com/stolksdorf/homebrewery/issues' color='red' icon='fa-bug'>
return <Nav.item
{...props}
newTab={true}
href='https://github.com/stolksdorf/homebrewery/issues'
color='red'
icon='fa-bug'>
report issue
</Nav.item>
};

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

@@ -3,6 +3,7 @@ var Nav = require('naturalcrit/nav/nav.jsx');
module.exports = function(props){
return <Nav.item
{...props}
className='patreon'
newTab={true}
href='https://www.patreon.com/stolksdorf'

View File

@@ -8,6 +8,9 @@ var Nav = require('naturalcrit/nav/nav.jsx');
const VIEW_KEY = 'homebrewery-recently-viewed';
const EDIT_KEY = 'homebrewery-recently-edited';
//DEPRICATED
var BaseItem = React.createClass({
getDefaultProps: function() {
return {
@@ -28,6 +31,8 @@ var BaseItem = React.createClass({
},
componentDidMount: function() {
console.log('Recent nav item is depricated');
var brews = JSON.parse(localStorage.getItem(this.props.storageKey) || '[]');
brews = _.filter(brews, (brew)=>{

View File

@@ -1,12 +0,0 @@
//TODO: Depricate
module.exports = function(shareId){
return function(event){
event = event || window.event;
if((event.ctrlKey || event.metaKey) && event.keyCode == 80){
var win = window.open(`/homebrew/print/${shareId}?dialog=true`, '_blank');
win.focus();
event.preventDefault();
}
};
};

View File

@@ -24,10 +24,10 @@ const HomePage = React.createClass({
renderNavbar : function(){
return <Navbar>
<Nav.section>
<PatreonNavItem />
<IssueNavItem />
<Nav.item newTab={true} href='/changelog' color='purple' icon='fa-file-text-o'>
Changelog
<PatreonNavItem collaspe={true} />
<IssueNavItem collaspe={true} />
<Nav.item newTab={true} href='/changelog' color='purple' icon='fa-star' collaspe={true}>
What's new
</Nav.item>
<RecentNavItem.both />
<AccountNavItem />

View File

@@ -47,7 +47,7 @@ const NewPage = React.createClass({
<Nav.item color='purple' icon='fa-file-pdf-o' onClick={Actions.localPrint}>
get PDF
</Nav.item>
<Items.Issue />
<Items.Issue collaspe={true} />
<Items.Account />
</Nav.section>
</Navbar>

View File

@@ -3,38 +3,62 @@ const _ = require('lodash');
const cx = require('classnames');
const Markdown = require('homebrewery/markdown.js');
const Headtags = require('vitreum/headtags');
const PrintPage = React.createClass({
getDefaultProps: function() {
return {
query : {},
brew : {
text : '',
style : ''
}
};
},
getInitialState: function() {
return {
brewText: this.props.brew.text
brew: this.props.brew
};
},
componentDidMount: function() {
if(this.props.query.local){
this.setState({ brewText : localStorage.getItem(this.props.query.local)});
try{
this.setState({
brew : JSON.parse(
localStorage.getItem(this.props.query.local)
)
});
}catch(e){}
}
if(this.props.query.dialog) window.print();
},
//TODO: Print page shouldn't replicate functionality in brew renderer
renderStyle : function(){
if(!this.state.brew.style) return;
return <style>{this.state.brew.style.replace(/;/g, ' !important;')}</style>
},
renderPages : function(){
return _.map(this.state.brewText.split('\\page'), (page, index) => {
return _.map(this.state.brew.text.split('\\page'), (page, index) => {
return <div
className='phb'
className='phb v2'
id={`p${index + 1}`}
dangerouslySetInnerHTML={{__html:Markdown.render(page)}}
key={index} />;
});
},
renderPrintInstructions : function(){
return <div className='printInstructions'>
Hey, I'm really cool instructions!!!!!
</div>
},
render : function(){
return <div>
return <div className='printPage'>
<Headtags.title>{this.state.brew.title}</Headtags.title>
{this.renderPrintInstructions()}
{this.renderStyle()}
{this.renderPages()}
</div>
}

View File

@@ -1,3 +1,17 @@
.printPage{
.printPage{
position : relative;
@media print{
.printInstructions{
display : none;
}
}
.printInstructions{
position : absolute;
top : 0px;
right : 0px;
z-index : 100000;
padding : 30px;
background-color : @blue;
}
}

View File

@@ -18,19 +18,6 @@ const Store = require('homebrewery/brew.store.js');
const Headtags = require('vitreum/headtags');
const SharePage = React.createClass({
getDefaultProps: function() {
return {
brew : {
title : '',
text : '',
shareId : null,
createdAt : null,
updatedAt : null,
views : 0
}
};
},
componentDidMount: function() {
document.addEventListener('keydown', this.handleControlKeys);
},
@@ -78,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

@@ -6,14 +6,15 @@
"dev": "node scripts/dev.js",
"quick": "node scripts/quick.js",
"build": "node scripts/build.js",
"phb": "node scripts/phb.js",
"populate": "node scripts/populate.js",
"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:markdown": "nodemon -x mocha test/markdown.test.js || exit 0"
"snippet": "nodemon scripts/snippet.test.js",
"todo": "./node_modules/.bin/fixme -i node_modules/** -i build/**",
"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",
@@ -46,6 +47,7 @@
"chai": "^3.5.0",
"chai-as-promised": "^6.0.0",
"chai-subset": "^1.4.0",
"fixme": "^0.4.3",
"mocha": "^3.2.0",
"supertest": "^2.0.1",
"supertest-as-promised": "^4.0.2"

File diff suppressed because one or more lines are too long

View File

@@ -2,19 +2,20 @@ const label = 'build';
console.time(label);
const clean = require('vitreum/steps/clean.js');
const jsx = require('vitreum/steps/jsx.js').partial;
const lib = require('vitreum/steps/libs.js').partial;
const less = require('vitreum/steps/less.js').partial;
const asset = require('vitreum/steps/assets.js').partial;
const jsx = require('vitreum/steps/jsx.js');
const lib = require('vitreum/steps/libs.js');
const less = require('vitreum/steps/less.js');
const asset = require('vitreum/steps/assets.js');
const Proj = require('./project.json');
clean()
.then(lib(Proj.libs))
.then(jsx('homebrew', './client/homebrew/homebrew.jsx', Proj.libs, ['./shared']))
.then(less('homebrew', ['./shared']))
.then(jsx('admin', './client/admin/admin.jsx', Proj.libs, ['./shared']))
.then(less('admin', ['./shared']))
.then(asset(Proj.assets, ['./shared', './client']))
.then(console.timeEnd.bind(console, label))
Promise.resolve()
.then(()=>clean())
.then(()=>lib(Proj.libs))
.then(()=>jsx('homebrew', './client/homebrew/homebrew.jsx', Proj.libs, ['./shared']))
.then((deps)=>less('homebrew', ['./shared'], deps))
.then(()=>jsx('admin', './client/admin/admin.jsx', Proj.libs, ['./shared']))
.then((deps)=>less('admin', ['./shared'], deps))
.then(()=>asset(Proj.assets, ['./shared', './client']))
.then(()=>console.timeEnd.bind(console, label))
.catch(console.error);

View File

@@ -1,21 +1,21 @@
const label = 'dev';
console.time(label);
const jsx = require('vitreum/steps/jsx.watch.js').partial;
const less = require('vitreum/steps/less.watch.js').partial;
const assets = require('vitreum/steps/assets.watch.js').partial;
const server = require('vitreum/steps/server.watch.js').partial;
const livereload = require('vitreum/steps/livereload.js').partial;
const jsx = require('vitreum/steps/jsx.watch.js');
const less = require('vitreum/steps/less.watch.js');
const assets = require('vitreum/steps/assets.watch.js');
const server = require('vitreum/steps/server.watch.js');
const livereload = require('vitreum/steps/livereload.js');
const Proj = require('./project.json');
Promise.resolve()
.then(jsx('homebrew', './client/homebrew/homebrew.jsx', Proj.libs, './shared'))
.then(less('homebrew', './shared'))
.then(jsx('admin', './client/admin/admin.jsx', Proj.libs, './shared'))
.then(less('admin', './shared'))
.then(assets(Proj.assets, ['./shared', './client']))
.then(livereload())
.then(server('./server.js', ['server']))
.then(console.timeEnd.bind(console, label))
.then(()=>jsx('homebrew', './client/homebrew/homebrew.jsx', Proj.libs, './shared'))
.then((deps)=>less('homebrew', './shared', deps))
.then(()=>jsx('admin', './client/admin/admin.jsx', Proj.libs, './shared'))
.then((deps)=>less('admin', './shared', deps))
.then(()=>assets(Proj.assets, ['./shared', './client']))
.then(()=>livereload())
.then(()=>server('./server.js', ['server']))
.then(()=>console.timeEnd.bind(console, label))
.catch(console.error)

8
scripts/notes.js Normal file
View File

@@ -0,0 +1,8 @@
require('fixme')({
path: process.cwd(),
ignored_directories: ['node_modules/**', '.git/**', 'build/**'],
file_patterns: ['**/*.js', '**/*.jsx', '**/*.less'],
file_encoding: 'utf8',
line_length_limit: 200
});

View File

@@ -1,10 +0,0 @@
const less = require('less');
const fs = require('fs');
less.render(fs.readFileSync('./client/homebrew/phbStyle/phb.style.less', 'utf8'), {compress : true})
.then((output) => {
fs.writeFileSync('./phb.standalone.css', output.css);
console.log('phb.standalone.css created!');
}, (err) => {
console.error(err);
});

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",

8
scripts/snippet.test.js Normal file
View File

@@ -0,0 +1,8 @@
const snippets = require('../shared/homebrewery/snippets');
console.log(snippets);
//console.log(snippets.brew.spell());
//console.log(snippets.brew.table());
console.log(snippets.brew.noncasterTable());

View File

@@ -7,9 +7,6 @@ const config = require('nconf')
const log = require('loglevel');
log.setLevel(config.get('log_level'));
//DB
require('./server/db.js').connect();
//Server
const app = require('./server/app.js');
@@ -29,7 +26,11 @@ app.use((req, res, next) => {
});
*/
const PORT = process.env.PORT || 8000;
const httpServer = app.listen(PORT, () => {
log.info(`server on port:${PORT}`);
});
require('./server/db.js').connect()
.then(()=>{
const PORT = process.env.PORT || 8000;
const httpServer = app.listen(PORT, () => {
log.info(`server on port:${PORT}`);
});
})
.catch((err)=>console.error(err))

View File

@@ -10,6 +10,7 @@ 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 : ""},
@@ -23,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

@@ -7,7 +7,7 @@ const dbPath = process.env.MONGODB_URI || process.env.MONGOLAB_URI || 'mongodb:/
module.exports = {
connect : ()=>{
return new Promise((resolve, reject)=>{
if(mongoose.connection.readyState == 1){
if(mongoose.connection.readyState !== 0){
log.warn('DB already connected');
return resolve();
}

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,13 @@ 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('./statics/welcome.brew.md', 'utf8'),
changelog : fs.readFileSync('./statics/changelog.md', 'utf8'),
faq : fs.readFileSync('./statics/faq.md', 'utf8'),
testBrew : fs.readFileSync('./statics/test.brew.md', 'utf8'),
oldTest : fs.readFileSync('./statics/oldTest.brew.md', 'utf8'),
};
@@ -40,6 +45,7 @@ router.get('/edit/:editId', mw.loadBrew, renderPage);
//Print Page
router.get('/print/:shareId', mw.viewBrew, renderPage);
router.get('/print', renderPage);
//Source page
router.get('/source/:sharedId', mw.viewBrew, (req, res, next)=>{
@@ -59,6 +65,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,19 +77,48 @@ 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();
}, renderPage);
//faq Page
router.get('/faq', (req, res, next) => {
req.brew = {
text : statics.faq,
title : 'FAQ',
editId : true
};
return next();
}, renderPage);
//New Page
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

@@ -1,12 +1,9 @@
const _ = require('lodash');
const Markdown = require('marked');
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) {
console.log(html);
if(_.startsWith(_.trim(html), '<div') && _.endsWith(_.trim(html), '</div>')){
var openTag = html.substring(0, html.indexOf('>')+1);
html = html.substring(html.indexOf('>')+1);
@@ -15,59 +12,22 @@ renderer.html = function (html) {
}
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)=>{
//Adds in the new div block syntax
let count = 0;
let blockReg = /{{[\w|,]+|}}/g;
const renderer = new Markdown.Renderer();
renderer.paragraph = function (text) {
const matches = text.match(blockReg);
if(!matches) return `<p>${text}</p>\n`;
let matchIndex = 0;
const res = _.reduce(text.split(blockReg), (r, text) => {
if(text) r.push(`<p>${text}</p>\n`);
const block = matches[matchIndex];
if(block && _.startsWith(block, '{{')){
r.push(`<div class="${block.substring(2).split(',').join(' ')}">`);
count++;
}
if(block == '}}' && count !== 0){
r.push('</div>');
count--;
}
matchIndex++;
return r;
}, []).join('\n');
return res;
};
let html = Markdown(rawBrewText, {renderer : renderer, sanitize: true});
html += _.times(count, ()=>{return '</div>'}).join('\n');
return html;
return Markdown(rawBrewText, {renderer : renderer})
},
validate : (rawBrewText) => {
return [];
/*
var errors = [];
var leftovers = _.reduce(rawBrewText.split('\n'), (acc, line, _lineNumber) => {
var lineNumber = _lineNumber + 1;
@@ -117,7 +77,6 @@ module.exports = {
});
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

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

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 : () => {
@@ -67,8 +70,9 @@ const Actions = {
},
localPrint : ()=>{
localStorage.setItem('print', Store.getBrewText());
window.open('/print?dialog=true&local=print','_blank');
const key = 'print';
localStorage.setItem(key, JSON.stringify(Store.getBrew()));
window.open(`/print?dialog=true&local=${key}`,'_blank');
},
print : ()=>{
window.open(`/print/${Store.getBrew().shareId}?dialog=true`, '_blank').focus();

View File

@@ -1,13 +1,14 @@
const _ = require('lodash');
const flux = require('pico-flux');
const Markdown = require('homebrewery/markdown.new.js');
const Markdown = require('homebrewery/markdown.js');
let State = {
version : '0.0.0',
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

@@ -3,34 +3,37 @@ const _ = require('lodash');
const cx = require('classnames');
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
},
isCode : function(){ return this.state.view == 'code' },
isStyle : function(){ return this.state.view == 'style' },
isMeta : function(){ return this.state.view == 'meta' },
componentDidMount: function() {
this.updateEditorSize();
@@ -42,55 +45,80 @@ 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);
const text = (this.isCode() ? this.props.brew.text : this.props.brew.style);
this.handleTextChange(lines.join('\n'));
this.refs.codeEditor.setCursorPosition(this.cursorPosition.line, this.cursorPosition.ch + injectText.length);
const lines = text.split('\n');
const cursorPos = this.refs.codeEditor.getCursorPosition();
lines[cursorPos.line] = splice(lines[cursorPos.line], cursorPos.ch, injectText);
this.refs.codeEditor.setCursorPosition(cursorPos.line, cursorPos.ch + injectText.length);
if(this.state.view == 'code') this.props.onCodeChange(lines.join('\n'));
if(this.state.view == 'style') this.props.onStyleChange(lines.join('\n'));
},
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;
if(!this.isCode()) 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(line.indexOf('\\column') === 0){
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
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
@@ -98,25 +126,44 @@ const BrewEditor = React.createClass({
onChange={this.props.onMetadataChange}
/>
},
*/
renderEditor : function(){
if(this.isMeta()){
return <MetadataEditor
metadata={this.props.brew}
onChange={this.props.onMetaChange} />
}
if(this.isStyle()){
return <CodeEditor key='style'
ref='codeEditor'
language='css'
value={this.props.brew.style}
onChange={this.props.onStyleChange} />
}
if(this.isCode()){
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'>
<Menubar
view={this.state.view}
onViewChange={this.handleViewChange}
onSnippetInject={this.handleInject}
/>
{this.renderEditor()}
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} />
</div>
/*

View File

@@ -8,6 +8,14 @@
background-color : fade(#333, 15%);
border-bottom : #333 solid 1px;
}
.block{
color : purple;
//font-style: italic;
}
.columnSplit{
font-style : italic;
color : grey;
}
}
.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,76 @@
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const SnippetMap = require('./snippet.map.js');
const SnippetGroup = require('./snippetGroup/snippetGroup.jsx');
const Menubar = React.createClass({
getDefaultProps: function() {
return {
view : 'code',
onViewChange : ()=>{},
onSnippetInject : ()=>{},
};
},
//TODO: remove
renderDevGroup : function(){
const Snippets = require('homebrewery/snippets/brew');
const snippets = _.map(Snippets, (gen, name)=>{
return {
name,
gen,
icon : 'fa-question'
}
})
return <SnippetGroup
name='All'
icon='fa-rocket'
snippets={snippets}
onClick={this.props.onSnippetInject}
key='dev'
/>
},
renderSnippets : function(){
if(this.props.view == 'meta') return ;
let mapping;
if(this.props.view == 'code') mapping = SnippetMap.brew;
if(this.props.view == 'style') mapping = SnippetMap.style;
let groups = _.map(mapping, (group)=>{
return <SnippetGroup {...group} onClick={this.props.onSnippetInject} key={group.name} />
});
groups = groups.concat(this.renderDevGroup());
return <div className='snippets'>{groups} </div>
},
render: function(){
return <div className='menubar'>
{this.renderSnippets()}
<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,40 @@
.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');
}
}
}
.snippets{
display : flex;
height : 100%;
}
}

View File

@@ -0,0 +1,48 @@
const Snippets = require('homebrewery/snippets');
module.exports = {
brew : [
{
name : 'PHB',
icon : 'fa-book',
snippets : [
{
name : 'Spell',
icon : 'fa-magic',
gen : Snippets.brew.spell
},
{
name : 'Table',
icon : 'fa-table',
gen : Snippets.brew.table
},
]
},
{
name : 'Mods',
icon : 'fa-gear',
snippets : []
}
],
style : [
{
name : 'Print',
icon : 'fa-print',
snippets : [
{
name : 'Ink Friendly',
icon : 'fa-tint',
gen : Snippets.style.inkFriendly
},
{
name : 'A4 Page Size',
icon : 'fa-file',
gen : Snippets.style.a4
},
]
}
]
}

View File

@@ -0,0 +1,41 @@
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const SnippetGroup = React.createClass({
getDefaultProps: function() {
return {
name : '',
icon : 'fa-rocket',
snippets : [],
onClick : function(){},
};
},
handleSnippetClick : function(snippet){
this.props.onClick(snippet.gen());
},
renderSnippets : function(){
return _.map(this.props.snippets, (snippet)=>{
return <div className='snippet' key={snippet.name} onClick={this.handleSnippetClick.bind(this, snippet)}>
<i className={'fa fa-fw ' + snippet.icon} />
{snippet.name}
</div>
})
},
render : function(){
return <div className='snippetGroup'>
<div className='text'>
<i className={'fa fa-fw ' + this.props.icon} />
<span className='groupName'>{this.props.name}</span>
</div>
<div className='dropdown'>
{this.renderSnippets()}
</div>
</div>
},
});
module.exports = SnippetGroup;

View File

@@ -0,0 +1,56 @@
.snippetGroup{
//display : inline-block;
display : flex;
height : 100%;
align-items : center;
//height : @menuHeight;
padding : 0px 5px;
cursor : pointer;
font-size : 0.6em;
font-weight : 800;
///line-height : @menuHeight;
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 : @menuHeight;
.groupName{
font-size : 10px;
}
}
&:hover{
.dropdown{
visibility : visible;
}
}
.dropdown{
position : absolute;
top : 100%;
visibility : hidden;
z-index : 1000;
margin-left : -5px;
padding : 0px;
background-color : #ddd;
.snippet{
.animate(background-color);
padding : 10px;
cursor : pointer;
font-size : 10px;
i{
margin-right : 8px;
font-size : 13px;
}
&:hover{
background-color : #999;
}
}
}
}

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

@@ -1,94 +0,0 @@
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const Snippets = require('./snippets/snippets.js');
const execute = function(val, brew){
if(_.isFunction(val)) return val(brew);
return val;
}
const Snippetbar = React.createClass({
getDefaultProps: function() {
return {
brew : '',
onInject : ()=>{},
onToggle : ()=>{},
showmeta : false
};
},
handleSnippetClick : function(injectedText){
this.props.onInject(injectedText)
},
renderSnippetGroups : function(){
return _.map(Snippets, (snippetGroup)=>{
return <SnippetGroup
brew={this.props.brew}
groupName={snippetGroup.groupName}
icon={snippetGroup.icon}
snippets={snippetGroup.snippets}
key={snippetGroup.groupName}
onSnippetClick={this.handleSnippetClick}
/>
})
},
render : function(){
return <div className='snippetBar'>
{this.renderSnippetGroups()}
<div className={cx('toggleMeta', {selected: this.props.showmeta})}
onClick={this.props.onToggle}>
<i className='fa fa-bars' />
</div>
</div>
}
});
module.exports = Snippetbar;
const SnippetGroup = React.createClass({
getDefaultProps: function() {
return {
brew : '',
groupName : '',
icon : 'fa-rocket',
snippets : [],
onSnippetClick : function(){},
};
},
handleSnippetClick : function(snippet){
this.props.onSnippetClick(execute(snippet.gen, this.props.brew));
},
renderSnippets : function(){
return _.map(this.props.snippets, (snippet)=>{
return <div className='snippet' key={snippet.name} onClick={this.handleSnippetClick.bind(this, snippet)}>
<i className={'fa fa-fw ' + snippet.icon} />
{snippet.name}
</div>
})
},
render : function(){
return <div className='snippetGroup'>
<div className='text'>
<i className={'fa fa-fw ' + this.props.icon} />
<span className='groupName'>{this.props.groupName}</span>
</div>
<div className='dropdown'>
{this.renderSnippets()}
</div>
</div>
},
});

View File

@@ -1,72 +0,0 @@
.snippetBar{
@height : 25px;
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;
&: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;
}
}
&:hover{
.dropdown{
visibility : visible;
}
}
.dropdown{
position : absolute;
top : 100%;
visibility : hidden;
z-index : 1000;
margin-left : -5px;
padding : 0px;
background-color : #ddd;
.snippet{
.animate(background-color);
padding : 5px;
cursor : pointer;
font-size : 10px;
i{
margin-right : 8px;
font-size : 13px;
}
&:hover{
background-color : #999;
}
}
}
}
}

View File

@@ -7,9 +7,9 @@ const BrewRenderer = require('../brewRenderer/brewRenderer.smart.jsx');
const BrewInterface = React.createClass({
handleSplitMove : function(){
console.log('split move!');
const BrewEditor = this.refs.editor.refs.wrappedComponent;
BrewEditor.updateEditorSize();
},
render: function(){
return <SplitPane onDragFinish={this.handleSplitMove} ref='pane'>

View File

@@ -2,7 +2,9 @@ const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const Markdown = require('homebrewery/markdown.new.js');
const OldBrewRenderer = require('depricated/brewRendererOld/brewRendererOld.jsx');
const Markdown = require('homebrewery/markdown.js');
const ErrorBar = require('./errorBar/errorBar.jsx');
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.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(){
@@ -124,7 +133,15 @@ const BrewRenderer = React.createClass({
return this.lastRender;
},
//TODO: This is pretty bad
renderStyle : function(){
return <style>{this.props.brew.style.replace(/;/g, ' !important;')}</style>
},
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 +150,9 @@ const BrewRenderer = React.createClass({
<ErrorBar errors={this.props.errors} />
<RenderWarnings />
{this.renderStyle()}
<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,51 @@
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');
renderer.image = function(href, title, text){
return `<img src="${href}" class="${text.split(',').join(' ')}"></img>`;
};
module.exports = {
marked : Markdown,
render : (rawBrewText)=>{
return Markdown(rawBrewText, {renderer : renderer})
blockCount = 0;
rawBrewText = rawBrewText.replace(/\\column/g, '{{columnSplit }}')
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: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,211 @@
///////////////////
.spell{
ul:first-of-type{
margin-top : -0.5em;
margin-bottom : 0.5em;
list-style-type : none;
&+p{
text-indent : 0em;
}
}
}
.monster{
.breakAvoid();
.pseudoBorder();
.pseudoShadow();
padding : 17px 14px;
table:nth-of-type(1){
margin-bottom : 0.4em;
margin-top : 0.4em;
color : @crimson;
tbody tr { background-color: transparent };
}
ul:nth-of-type(1),ul:nth-of-type(2){
list-style: none;
padding-left : 1em;
text-indent : -1em;
margin-bottom : 0.5em;
strong{
color : @crimson;
}
}
&:before{
top : 8px;
right : 7px;
bottom : 19px;
left : 7px;
background-color : #FDF1DC;
border-image-slice : 8;
border-image-source : @monsterBorder;
border-image-width : 8px;
}
&.wide{
column-count : 2;
}
}
.note{
.useSansSerif();
.breakAvoid();
.pseudoBorder();
.pseudoShadow();
margin : 9px 0px;
padding : 17px 17px;
&:before{
top : 9px;
right : 9px;
bottom : 19px;
left : 9px;
background-color : @green;
border-width : 11px;
border-image-outset : 9px 0px;
border-image-slice : 11;
border-image-source : @noteBorder;
}
h2,h3,h4{
.useSansSerif();
color : black;
}
p, ul{
font-size : 0.352cm;
line-height : 1.1em;
}
&.alt{
&:before{
border-style : solid;
border-width : 7px;
border-image-outset : 4px;
border-image-slice : 12;
border-image-source : @descriptiveBorder;
}
}
}
.frame{
.breakAvoid();
.pseudoBorder();
padding : 25px 17px;
&:before{
top : 25px;
right : 17px;
bottom : 25px;
left : 17px;
background-color : white;
border-image-outset : 25px 17px;
border-image-slice : 150 200 150 200;
border-image-source : @frameBorder;
border-image-width : 47px;
}
}
.footnote{
position : absolute;
right : 80px;
bottom : 28px;
z-index : 150;
width : 200px;
font-size : 0.9em;
color : @gold;
text-align : right;
}
//*****************************
// * TABLE OF CONTENTS
// *****************************/
.toc{
h1{
text-align : center;
}
li{
margin-bottom : 3px;
strong, em::after{
font-family : BookInsanity;
font-size : 13px;
font-style : normal;
font-weight : 500;
color : black;
}
em{
display : block;
overflow : hidden;
width : auto;
font-style : normal;
white-space : nowrap;
&:after{
content : " ..............................................................................................................";
}
}
strong{
float : right;
margin-left : 4px;
}
h3{
margin-top : 15px;
em{ color : @crimson; }
em::after{ display : none; }
}
h4{
margin-top : 10px;
em{ color : @crimson; }
}
}
a{
color : black;
text-decoration : none;
&:hover{
text-decoration : underline;
}
}
ul{
padding-left : 0;
list-style-type : none;
}
}
.wide{
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
}
.oneColumn{
column-count : 1;
// column-gap : 1cm;
}
.twoColumn{
column-count : 2;
//column-fill: auto;
////column-gap : 1cm;
}
.threeColumn{
column-count : 3;
//column-gap : 1cm;
}
.fourColumn{
column-count : 4;
//column-gap : 1cm;
}
.columnSplit{
visibility : hidden;
-webkit-column-break-bfore : always;
break-before : column;
}
.brushed{
border-image-outset : 25px 17px;
border-image-repeat : round;
border-image-slice : 1250 1250 1250 1250;
border-image-width : 1250px;
border-image-source : url('http : //i.imgur.com/nzPYZyD.png');
}
//basics
.left{
text-align : left;
}
.right{
text-align : right;
}
.center{
text-align : center;
}
.bold{
font-weight : 800;
}
.sansSerif{
.useSansSerif();
}

View File

@@ -0,0 +1,38 @@
//TODO: come up with fun color names
@background : #EEE5CE;
@noteGreen : #e0e5c1;
@headerUnderline : #c9ad6a;
@horizontalRule : #9c2b1b;
@headerText : #58180D;
@monsterStatBackground : #FDF1DC;
.colorElements(@color){
table tbody{
tr:nth-child(odd){
background-color : @color;
}
}
&.note:before{
background-color: @color;
}
}
@crimson : #58180D;
@red : #9c2b1b;
@gold : #c9ad6a; //brown?
@green : #e0e5c1;
@yellow : #faf7ea; //same as background?
@teal : blue;
@blue : blue;
//TODO make a color mixin generator
.teal{ .colorElements(@teal); }
.blue{ .colorElements(@blue); }
.green{ .colorElements(@green); }
.yellow{ .colorElements(@yellow); }
.gold{ .colorElements(@gold); }
.red{ .colorElements(@red); }

View File

@@ -0,0 +1,172 @@
pre{
font-family : monospace;
background-color : @yellow;
padding : 12px;
border: 1px solid #bfbfbf;
white-space: pre-wrap;
color : #333;
-webkit-column-break-inside : avoid;
column-break-inside : avoid;
}
hr{
visibility : visible;
height : 6px;
margin : 4px 0px;
background-image : @dividerImg;
background-size : 100% 100%;
border : none;
}
p{
padding-bottom : 0.8em;
line-height : 1.3em;
&+p{
margin-top : -0.8em;
}
}
blockquote{
font-style : italic;
&>p{
line-height: 1.8em;
&:first-child::first-line{
//TODO: Find the right font for block quotes
font-style: normal;
font-family: ScalySansSmallCaps;
}
}
.cite{
font-style: normal;
text-align: right;
}
}
//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;
}
//******************************
// 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;
}
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;
}
//*****************************
// * 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 : @green;
}
}
}
}

View File

@@ -0,0 +1,55 @@
/* 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;
}

View File

@@ -0,0 +1,16 @@
@footerImg : url('/assets/homebrewery/phb_style/img/footer.png');
@footerFlipImg : url('/assets/homebrewery/phb_style/img/footer_flip.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');
@shadowBorder : url('/assets/homebrewery/phb_style/img/shadow_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');
@monsterBG : url('/assets/homebrewery/phb_style/img/monster_bg.jpg');

View File

@@ -0,0 +1,323 @@
//TODO: Remove this
/*
@media print {
.phb.v2{
.descriptive, blockquote{
box-shadow : none;
}
}
}
*/
.phb.v2{
@import './phb.mixins.less';
@import './phb.fonts.less';
@import './phb.colors.less';
@import './phb.img.less';
@import './phb.blocks.less';
@import './phb.elements.less';
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;
column-count : 2;
column-fill : auto;
column-gap : 1cm;
column-width : 8cm;
background-color : @background;
background-image : @phbBG;
font-family : BookInsanity;
font-size : 0.317cm;
text-rendering : optimizeLegibility;
page-break-before : always;
page-break-after : always;
@page { margin: 0; } //TODO: ????
& *{
-webkit-print-color-adjust : exact;
}
//*****************************
// * FOOTER
// *****************************/
&:after{
content : "Made with The Homebrewery";
position : absolute;
bottom : 0px;
left : 0px;
z-index : 100;
height : 50px;
width : 100%;
background-image : @footerImg;
background-size : cover;
padding: 28px 63px;
box-sizing: border-box;
color : lighten(@gold, 0%);
font-size: 0.7em;
}
&:nth-child(even){
&:after{
background-image: @footerFlipImg;
text-align: right;
}
&:before{
left : 2px;
}
.footnote{
left : 80px;
text-align : left;
}
}
&:before{
content : counter(phb-page-numbers);
position : absolute;
right : 2px;
bottom : 22px;
width : 50px;
font-size : 0.9em;
color : @gold;
text-align : center;
}
//*****************************
// * BASE
// *****************************/
//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;
}
}
//*****************************
// * EXTRAS
// *****************************/
//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
//*****************************
// * 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;
}
*/
//*****************************
// * 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

@@ -0,0 +1,48 @@
.breakAvoid(){
column-break-inside : avoid;
-webkit-column-break-inside : avoid;
}
.pseudoBorder(){
position : relative;
&:before{
content : '';
position : absolute;
z-index : -2;
box-sizing : border-box;
border-style : solid;
border-image-repeat : round;
}
}
.pseudoShadow(){
position : relative;
&:after{
content : '';
position : absolute;
z-index : -1;
box-sizing : border-box;
top : 4px;
right : 0px;
bottom : 10px;
left : 0px;
border-style : solid;
border-image-repeat : round;
border-image-slice : 13 13;
border-image-source : @shadowBorder;
border-image-width : 11px;
}
}
.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,82 @@
const _ = require('lodash');
const Data = require('./random.data.js');
const getFeature = (level)=>{
let res = []
if(_.includes([4,6,8,12,14,16,19], level+1)){
res = ['Ability Score Improvement']
}
res = _.union(res, _.sampleSize(Data.abilities, _.sample([0,1,1,1,1,1])));
if(!res.length) return '─';
return res.join(', ');
};
module.exports = {
casterTable : ()=>{
let featureScore = 1
const rows = _.map(Data.levels, (lvlText, level)=>{
featureScore += _.random(0,1);
return '| ' + [
lvlText,
'+'+Math.floor(level/4 + 2),
getFeature(level),
'+'+featureScore
].join(' | ') + ' |';
}).join('\n');
return `{{frame,wide
##### ${Data.rand('classes')}
| Level | Proficiency Bonus | Features | Cantrips Known | Spells Known | 1st | 2nd | 3rd | 4th | 5th | 6th | 7th | 8th | 9th |
|:---:|:---:|:---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
${rows}
}}`;
},
halfcasterTable : ()=>{
let featureScore = 1
const rows = _.map(Data.levels, (lvlText, level)=>{
featureScore += _.random(0,1);
return '| ' + [
lvlText,
'+'+Math.floor(level/4 + 2),
getFeature(level),
'+'+featureScore
].join(' | ') + ' |';
}).join('\n');
return `{{frame,wide
##### ${Data.rand('classes')}
| Level | Proficiency Bonus | Features | 1st | 2nd | 3rd | 4th | 5th |
|:---:|:---:|:---|:---:|:---:|:---:|:---:|:---:|
${rows}
}}`;
},
noncasterTable : ()=>{
let featureScore = 1
const rows = _.map(Data.levels, (lvlText, level)=>{
featureScore += _.random(0,1);
return '| ' + [
lvlText,
'+'+Math.floor(level/4 + 2),
getFeature(level),
'+'+featureScore
].join(' | ') + ' |';
}).join('\n');
return `{{frame
##### ${Data.rand('classes')}
| Level | Proficiency Bonus | Features | ${Data.rand('abilities')} |
|:---:|:---:|:---|:---:|
${rows}
}}`;
}
}

View File

@@ -0,0 +1,19 @@
const _ = require('lodash');
module.exports = _.merge(
require('./spell.snippet.js'),
require('./table.snippet.js'),
require('./class.snippet.js'),
require('./note.snippet.js'),
require('./monster.snippet.js'),
require('./toc.snippet.js')
//wide
//colors
//brushed
//font
//alignment
);

View File

@@ -0,0 +1,74 @@
const _ = require('lodash');
const Data = require('./random.data.js');
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 getAttributes = ()=>{
return `
- **Saving Throws**
- **Condition Immunities** " + genList(["groggy", "swagged", "weak-kneed", "buzzed", "groovy", "melancholy", "drunk"], 3),
- **Senses** passive Perception " + _.random(3, 20),
- **Languages** ${Data.rand(["Common", "Pottymouth", "Gibberish", "Latin", "Jive"], 2).join(', ')}
- **Challenge** ${_.random(0, 15)} (${_.random(10,10000)} XP)
`;
}
const getAbilities = ()=>{
}
const getActions = ()=>{
}
module.exports = {
monster : ()=>{
const stats = '';
return `{{monster
## ${Data.rand('creatures')}
*${Data.rand('sizes')}, ${Data.rand('alignments')}*
---
- **Armor Class** ${_.random(10,20)}
- **Hit Points** ${_.random(1, 150)} (1d4 + 5)
- **Speed** ${ _.random(0,50)} ft
---
|STR|DEX|CON|INT|WIS|CHA|
|:---:|:---:|:---:|:---:|:---:|:---:|
${getStats()}
---
${getAttributes()}
---
Abilities
### Actions
}}`
}
};

View File

@@ -0,0 +1,22 @@
const _ = require('lodash');
const Data = require('./random.data.js');
module.exports = {
note : ()=>{
return `{{note
##### ${Data.rand('abilities')}
${Data.rand('sentences', 6, 4).join(' ')}
}}`
},
altnote : ()=>{
return `{{note,alt
##### ${Data.rand('abilities')}
${Data.rand('sentences', 6, 4).join(' ')}
}}`
}
}

View File

@@ -0,0 +1,421 @@
const _ = require('lodash');
const Data = {
rand : (name, max = 1, min = 1)=>{
const data = (Data[name] ? Data[name] : name);
return _.sampleSize(data, _.random(min, max));
},
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`,
`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`,
`Vyse's Skies`,
`White Greatness III`,
`Yellow Divinity`,
`Zidane's Ghost`
],
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.`
],
classes : [
'Archivist',
'Armadillomaster',
'Beat Priest',
'Beer Mentalist',
'Berserker-Typist',
'Bonsai Hooligan',
'Candy Finder',
'Coffeemancer',
'Concierge',
'Corn Theif',
'Cottonsmith',
'Dirtmistress',
'Fancyman',
'Fishmongerer',
'Fletcher',
'Flow Robber',
'Haberdasher',
'Hamster Lady',
'Jam Robber',
'Linguist',
'Lizard Trainer',
'Manicurist',
'Markermaster',
'Mint Handler',
'Narwhalologer',
'Notary',
'Otter Mentalist',
'Plastic Diviner',
'Rhymemancer',
'Rum Buster',
'Whaleologer',
],
gear : [
`a squeegee`,
'6 rubber chickens',
'10 lint fluffs',
'1 button',
'a cherished lost sock',
'a small doll',
'hopes and dreams',
'1st born child',
'3rd born child',
'a crushed button worth at least 1cp',
'discarded gum wrapper',
`Broch of Air Blasts`,
`Elven Leather Armor`,
`Glaive of the Deathly Viper`,
`Mystical Eagle's Ointment of the Eagles`,
`Mystical Scintillating Cudgel`,
`Wise Thinker's Anklet`,
`The four fragments of the Disk of Madness`
],
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",
],
effects : [
'Induces politicians to parade through the streets naked, and makes the nearest unbetrothed prince or princess dance around the maypole making dirty jokes.',
'Tricks enchanted princesses to spin straw into gold, and makes princesses trapped in towers steal from the rich and give to the poor.',
'Drives the man or woman of your dreams to jump up and down on the spot, and makes angry dragons grow onions wherever they walk.',
'Causes enchanted talking animals to fall down dead, and makes large pumpkins attract love-struck unicorns.',
'Induces officers of the law to adopt small, fluffy bunnies as pets, and makes enchanted wooden puppets vomit gold coins.',
'Causes accountants to give you all of their possessions, and makes officers of the law grow mushrooms out of their ears.',
'Induces goats to eat until they burst, and makes men with small heads vomit gold coins.',
'Tricks enchanted princesses to turn into small pumpkins, and makes evil landlords declare themselves king.',
'Induces your enemies to steal from the palace cook, and makes rich merchants propose marriage.',
'Causes evil landlords to vomit gold coins, and makes the nearest unbetrothed prince or princess drink beer.',
'Induces men with small heads to grow mushrooms out of their ears, and makes witches steal from the rich and give to the poor.',
`Conjures food with energy equal to whatever was used to cast the spell.`,
`Allows a living target to withstand extreme cold.`,
`Conjures a thick fog that acts as a smoke screen.`,
`Creates a bubble in which time is stopped for a short period.`,
`Creates several bolts of shadowy energy.`,
`Causes a living target to panic for a period of time.`,
`Creates a floating scroll and quill that'll write down everything the caster or target says for a period of time.`,
`Causes whoever is targeted to enter a state of confusion for a period of time.`,
`Creates a magical barrier that blocks all with dark intentions or dark influences over them.`,
`Creates a bolt of demonic energy.`,
`Causes whoever is targeted to drop whatever they're holding.`
],
effects2 : [
'Unless they pass a Constitution save, the creature gains 1 level of Exhaustion.',
'Pushed 5 feet unless they pass a Strength save. ',
'Unless they pass a Wisdom save, the creature is Charmed.',
'Unless they pass a Wisdom save, the creature is Frightened. The creature can remake this save on each of their turns.',
'Unless they pass a Wisdom save, the creature is Frightened. The creature can remake this save on each of their turns.',
'Unless they pass a Wisdom save, the creature is Paralyzed. The creature can remake this save on each of their turns.',
'Pushed 25 feet unless they pass a Strength save. ',
'Unless they pass a Constitution save, the creature is Poisoned. The creature can remake this save on each of their turns.',
'Unless they pass a Wisdom save, the creature is Charmed.',
'Unless they pass a Constitution save, the creature is Slowed. The creature can remake this save on each of their turns.',
'Unless they pass a Constitution save, the creature is Slowed. The creature can remake this save on each of their turns.',
'Knocked Prone unless they pass a Dexterity save. ',
'Unless they pass a Constitution save, the creature is Deafened. The creature can remake this save on each of their turns.',
'Knocked Prone unless they pass a Dexterity save. ',
'Unless they pass a Constitution save, the creature gains 1 level of Exhaustion.',
'Knocked Prone unless they pass a Dexterity save. ',
'Unless they pass a Constitution save, the creature is Deafened. The creature can remake this save on each of their turns.',
'Unless they pass a Constitution save, the creature gains 1 level of Exhaustion.',
'Pushed 20 feet unless they pass a Strength save. ',
'Resistance to Radiant damage until 1 round'
],
attacks : [
`Aquatic Press of the Romantic Demons`,
`Barbarian Raider Pinch of the Cemetary`,
`Beetle Hold of the Fangs`,
`Confident Badger Pinch of Lyres`,
`Emperor's Roll of the Nine Volcanos`,
`Firey Rake of the Endings`,
`Fortuitous Underhook of the Wolves`,
`God's Knee of Blessings`,
`Hawk Dance`,
`Heavenly Rat's Roll`,
`Hellish Meteor`,
`High Noose of the Ruthless Guardian`,
`Hold of Poisons`,
`King Drop of the Fighting Protectors`,
`Leg Clap of the Dogs`,
`Northeastern Seventeen Cats Claw`,
`Phantasmal Plague Finger`,
`Pose of Perfect Sunsets`,
`Seal Hammer of the Forty Sages`,
`Shaman Pull of Destructions`,
`Southeastern Automaton Pull`,
`Southwestern Eighty Chants Clap`,
`Tackle of Foul Leaves`,
`Tornado of the Uncounted Hawks`,
`Yielding Throw of the Mills`,
],
abilities : [
"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"
],
alignments : [
"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",
"Lawful Gossipy",
"Neurotic Good",
"Sarcastic Evil",
"Snotty Neutral",
"Wannabe Good"
],
sizes : ['Microscopic', 'Tiny', 'Small', 'Medium', 'Large', 'Gargantuan', 'Stupidly vast'],
levels : ["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th", "11th", "12th", "13th", "14th", "15th", "16th", "17th", "18th", "19th", "20th"],
sentences : [
`The suspicion arises the narrator of the tale is actually a demon.`,
`There is a predicted hurricane - but it's not what was expected, and this complicates the plans of the protagonist.`,
`The antagonist's believes their life has changed for the strange - this turns out to be this is due to being lied to by others`,
`An accidental cuddle leads to complications.`,
`It's revealed that everything that is happening is all a dream.`,
`There is a sudden hurricane.`,
`The alternate protagonist is revealed to be a different race/species than thought, which suddenly makes what's going on much clearer.`,
`Thanks to alien forces, the characters end up in the earth's past.`,
`Thanks to alien forces, the secondary protagonist ends up in a world after an apocalypse.`,
`Due to a panic attack a character has to get psychological therapy.`,
],
creatures : [
"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"
]
};
module.exports = Data;

Some files were not shown because too many files have changed in this diff Show More