+
Save current
diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx
index 25a0c1e..46d0fe2 100644
--- a/client/homebrew/pages/newPage/newPage.jsx
+++ b/client/homebrew/pages/newPage/newPage.jsx
@@ -3,7 +3,7 @@ const _ = require('lodash');
const cx = require('classnames');
const request = require("superagent");
-const Markdown = require('naturalcrit/markdown.js');
+const Markdown = require('homebrewery/markdown.js');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
diff --git a/client/homebrew/pages/printPage/printPage.jsx b/client/homebrew/pages/printPage/printPage.jsx
index b6ecfd8..91f1785 100644
--- a/client/homebrew/pages/printPage/printPage.jsx
+++ b/client/homebrew/pages/printPage/printPage.jsx
@@ -1,7 +1,7 @@
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
-const Markdown = require('naturalcrit/markdown.js');
+const Markdown = require('homebrewery/markdown.js');
const PrintPage = React.createClass({
getDefaultProps: function() {
diff --git a/scripts/dev.js b/scripts/dev.js
index 6fda537..c112e84 100644
--- a/scripts/dev.js
+++ b/scripts/dev.js
@@ -12,10 +12,8 @@ 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']))
diff --git a/shared/homebrewery/brew.actions.js b/shared/homebrewery/brew.actions.js
new file mode 100644
index 0000000..1a9f99a
--- /dev/null
+++ b/shared/homebrewery/brew.actions.js
@@ -0,0 +1,34 @@
+const dispatch = require('pico-flux').dispatch;
+
+const request = require('superagent');
+const Store = require('./brew.store.js');
+
+const Actions = {
+ init : (initState) => {
+ Store.init(initState);
+ },
+ setBrew : (brew) => {
+ dispatch('SET_BREW', brew);
+ },
+ updateBrewText : (brewText) => {
+ dispatch('UPDATE_BREW_TEXT', brewText)
+ },
+ updateMetaData : (meta) => {
+ dispatch('UPDATE_META', meta);
+ },
+
+
+
+ saveNew : () => {
+ //TODO: Maybe set the status?
+ request.post('/api')
+ .send(Store.getBrew())
+ .end((err, res)=>{
+ if(err) return;
+ const brew = res.body;
+ window.location = '/edit/' + brew.editId;
+ });
+ }
+};
+
+module.exports = Actions;
\ No newline at end of file
diff --git a/shared/homebrewery/brew.store.js b/shared/homebrewery/brew.store.js
new file mode 100644
index 0000000..e521d98
--- /dev/null
+++ b/shared/homebrewery/brew.store.js
@@ -0,0 +1,61 @@
+const _ = require('lodash');
+const flux = require('pico-flux');
+
+const Markdown = require('homebrewery/markdown.js');
+
+let State = {
+ version : '0.0.0',
+
+ brew : {
+ text : '',
+ shareId : undefined,
+ editId : undefined,
+ createdAt : undefined,
+ updatedAt : undefined,
+
+ title : '',
+ description : '',
+ tags : '',
+ published : false,
+ authors : [],
+ systems : []
+ },
+
+ errors : []
+};
+
+const Store = flux.createStore({
+ SET_BREW : (brew) => {
+ State.brew = brew;
+ },
+ UPDATE_BREW_TEXT : (brewText) => {
+ State.brew.text = brewText;
+ State.errors = Markdown.validate(brewText);
+ },
+ UPDATE_META : (meta) => {
+ State.brew = _.merge({}, State.brew, meta);
+ }
+});
+
+
+Store.init = (state)=>{
+ State = _.merge({}, State, state);
+};
+Store.getBrew = ()=>{
+ return State.brew;
+};
+Store.getBrewText = ()=>{
+ return State.brew.text;
+};
+Store.getMetaData = ()=>{
+ return _.omit(State.brew, ['text']);
+};
+Store.getErrors = ()=>{
+ return State.errors;
+};
+
+Store.getVersion = ()=>{
+ return State.version;
+};
+
+module.exports = Store;
\ No newline at end of file
diff --git a/shared/homebrewery/brewEditor/brewEditor.jsx b/shared/homebrewery/brewEditor/brewEditor.jsx
new file mode 100644
index 0000000..a8fa891
--- /dev/null
+++ b/shared/homebrewery/brewEditor/brewEditor.jsx
@@ -0,0 +1,121 @@
+const React = require('react');
+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 splice = function(str, index, inject){
+ return str.slice(0, index) + inject + str.slice(index);
+};
+
+const SNIPPETBAR_HEIGHT = 25;
+
+const BrewEditor = React.createClass({
+ getDefaultProps: function() {
+ return {
+ value : '',
+ onChange : ()=>{},
+
+ metadata : {},
+ onMetadataChange : ()=>{},
+ };
+ },
+ getInitialState: function() {
+ return {
+ showMetadataEditor: false
+ };
+ },
+ cursorPosition : {
+ line : 0,
+ ch : 0
+ },
+
+ componentDidMount: function() {
+ this.updateEditorSize();
+ this.highlightPageLines();
+ window.addEventListener("resize", this.updateEditorSize);
+ },
+ componentWillUnmount: function() {
+ window.removeEventListener("resize", this.updateEditorSize);
+ },
+
+ updateEditorSize : function() {
+ let paneHeight = this.refs.main.parentNode.clientHeight;
+ paneHeight -= SNIPPETBAR_HEIGHT + 1;
+ this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
+ },
+
+ handleTextChange : function(text){
+ this.props.onChange(text);
+ },
+ handleCursorActivty : function(curpos){
+ this.cursorPosition = curpos;
+ },
+ handleInject : function(injectText){
+ const lines = this.props.value.split('\n');
+ lines[this.cursorPosition.line] = splice(lines[this.cursorPosition.line], this.cursorPosition.ch, injectText);
+
+ this.handleTextChange(lines.join('\n'));
+ this.refs.codeEditor.setCursorPosition(this.cursorPosition.line, this.cursorPosition.ch + injectText.length);
+ },
+ handgleToggle : function(){
+ this.setState({
+ showMetadataEditor : !this.state.showMetadataEditor
+ })
+ },
+
+ //Called when there are changes to the editor's dimensions
+ update : function(){
+ this.refs.codeEditor.updateSize();
+ },
+
+ highlightPageLines : function(){
+ if(!this.refs.codeEditor) return;
+ const codeMirror = this.refs.codeEditor.codeMirror;
+
+ const lineNumbers = _.reduce(this.props.value.split('\n'), (r, line, lineNumber)=>{
+ if(line.indexOf('\\page') !== -1){
+ codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
+ r.push(lineNumber);
+ }
+ return r;
+ }, []);
+ return lineNumbers
+ },
+
+ renderMetadataEditor : function(){
+ if(!this.state.showMetadataEditor) return;
+ return
+ },
+
+ render : function(){
+
+ this.highlightPageLines();
+
+ return(
+
+
+ {this.renderMetadataEditor()}
+
+
+ );
+ }
+});
+
+module.exports = BrewEditor;
+
diff --git a/shared/homebrewery/brewEditor/brewEditor.less b/shared/homebrewery/brewEditor/brewEditor.less
new file mode 100644
index 0000000..4d3cf4a
--- /dev/null
+++ b/shared/homebrewery/brewEditor/brewEditor.less
@@ -0,0 +1,15 @@
+
+.brewEditor{
+ position : relative;
+ width : 100%;
+ .codeEditor{
+ height : 100%;
+
+ .pageLine{
+ background-color: fade(@blue, 30%);
+ border-bottom : #333 solid 1px;
+
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/shared/homebrewery/brewEditor/brewEditor.smart.jsx b/shared/homebrewery/brewEditor/brewEditor.smart.jsx
new file mode 100644
index 0000000..2dc647c
--- /dev/null
+++ b/shared/homebrewery/brewEditor/brewEditor.smart.jsx
@@ -0,0 +1,13 @@
+const Actions = require('homebrewery/brew.actions.js');
+const Store = require('homebrewery/brew.store.js');
+
+const BrewEditor = require('./brewEditor.jsx')
+
+module.exports = Store.createSmartComponent(BrewEditor, ()=>{
+ return {
+ value : Store.getBrewText(),
+ onChange : Actions.updateBrewText,
+ metadata : Store.getMetaData(),
+ onMetadataChange : Actions.updateMetaData,
+ };
+});
\ No newline at end of file
diff --git a/shared/homebrewery/brewEditor/metadataEditor/metadataEditor.jsx b/shared/homebrewery/brewEditor/metadataEditor/metadataEditor.jsx
new file mode 100644
index 0000000..694e9db
--- /dev/null
+++ b/shared/homebrewery/brewEditor/metadataEditor/metadataEditor.jsx
@@ -0,0 +1,175 @@
+const React = require('react');
+const _ = require('lodash');
+const cx = require('classnames');
+const request = require("superagent");
+
+const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder']
+
+const MetadataEditor = React.createClass({
+ getDefaultProps: function() {
+ return {
+ metadata: {
+ editId : null,
+ title : '',
+ description : '',
+ tags : '',
+ published : false,
+ authors : [],
+ systems : []
+ },
+ onChange : ()=>{}
+ };
+ },
+
+ handleFieldChange : function(name, e){
+ this.props.onChange(_.merge({}, this.props.metadata, {
+ [name] : e.target.value
+ }))
+ },
+ handleSystem : function(system, e){
+ if(e.target.checked){
+ this.props.metadata.systems.push(system);
+ }else{
+ this.props.metadata.systems = _.without(this.props.metadata.systems, system);
+ }
+ this.props.onChange(this.props.metadata);
+ },
+ handlePublish : function(val){
+ this.props.onChange(_.merge({}, this.props.metadata, {
+ published : val
+ }));
+ },
+
+ handleDelete : function(){
+ if(!confirm("are you sure you want to delete this brew?")) return;
+ if(!confirm("are you REALLY sure? You will not be able to recover it")) return;
+
+ request.get('/api/remove/' + this.props.metadata.editId)
+ .send()
+ .end(function(err, res){
+ window.location.href = '/';
+ });
+ },
+
+ getRedditLink : function(){
+ const meta = this.props.metadata;
+ const title = `${meta.title} [${meta.systems.join(' ')}]`;
+ const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
+
+**[Homebrewery Link](http://homebrewery.naturalcrit.com/share/${meta.shareId})**`;
+
+ return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title)}&text=${encodeURIComponent(text)}`;
+ },
+
+ renderSystems : function(){
+ return _.map(SYSTEMS, (val)=>{
+ return
+
+ {val}
+
+ });
+ },
+
+ renderPublish : function(){
+ if(this.props.metadata.published){
+ return
+ unpublish
+
+ }else{
+ return
+ publish
+
+ }
+ },
+
+ renderDelete : function(){
+ if(!this.props.metadata.editId) return;
+
+ return
+
delete
+
+
+ delete brew
+
+
+
+ },
+
+ renderAuthors : function(){
+ let text = 'None.';
+ if(this.props.metadata.authors.length){
+ text = this.props.metadata.authors.join(', ');
+ }
+ return
+ },
+
+ renderShareToReddit : function(){
+ if(!this.props.metadata.shareId) return;
+
+ return
+ },
+
+ render : function(){
+ return
+
+ title
+
+
+
+ description
+
+
+ {/*}
+
+ tags
+
+
+ */}
+
+
+
systems
+
+ {this.renderSystems()}
+
+
+
+ {this.renderAuthors()}
+
+
+
publish
+
+ {this.renderPublish()}
+ Published homebrews will be publicly viewable and searchable (eventually...)
+
+
+
+ {this.renderShareToReddit()}
+
+ {this.renderDelete()}
+
+
+ }
+});
+
+module.exports = MetadataEditor;
diff --git a/shared/homebrewery/brewEditor/metadataEditor/metadataEditor.less b/shared/homebrewery/brewEditor/metadataEditor/metadataEditor.less
new file mode 100644
index 0000000..c3141f0
--- /dev/null
+++ b/shared/homebrewery/brewEditor/metadataEditor/metadataEditor.less
@@ -0,0 +1,79 @@
+
+.metadataEditor{
+ position : absolute;
+ z-index : 10000;
+ box-sizing : border-box;
+ width : 100%;
+ padding : 25px;
+ background-color : #999;
+ .field{
+ display : flex;
+ width : 100%;
+ margin-bottom : 10px;
+ &>label{
+ display : inline-block;
+ vertical-align : top;
+ width : 80px;
+ font-size : 0.7em;
+ font-weight : 800;
+ line-height : 1.8em;
+ text-transform : uppercase;
+ flex-grow : 0;
+ }
+ &>.value{
+ flex-grow : 1;
+ }
+ }
+ .description.field textarea.value{
+ resize : none;
+ height : 5em;
+ font-family : 'Open Sans', sans-serif;
+ font-size : 0.8em;
+ }
+ .systems.field .value{
+ label{
+ vertical-align : middle;
+ margin-right : 15px;
+ cursor : pointer;
+ font-size : 0.7em;
+ font-weight : 800;
+ user-select : none;
+ }
+ input{
+ vertical-align : middle;
+ cursor : pointer;
+ }
+ }
+ .publish.field .value{
+ position : relative;
+ margin-bottom: 15px;
+ button.publish{
+ .button(@blueLight);
+ }
+ button.unpublish{
+ .button(@silver);
+ }
+ small{
+ position : absolute;
+ bottom : -15px;
+ left : 0px;
+ font-size : 0.6em;
+ font-style : italic;
+ }
+ }
+
+ .delete.field .value{
+ button{
+ .button(@red);
+ }
+ }
+ .reddit.field .value{
+ button{
+ .button(@purple);
+ }
+ }
+ .authors.field .value{
+ font-size: 0.8em;
+ line-height : 1.5em;
+ }
+}
\ No newline at end of file
diff --git a/shared/homebrewery/brewEditor/snippetbar/snippetbar.jsx b/shared/homebrewery/brewEditor/snippetbar/snippetbar.jsx
new file mode 100644
index 0000000..29c6b43
--- /dev/null
+++ b/shared/homebrewery/brewEditor/snippetbar/snippetbar.jsx
@@ -0,0 +1,94 @@
+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
+ })
+ },
+
+ render : function(){
+ return
+ {this.renderSnippetGroups()}
+
+
+
+
+ }
+});
+
+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
+
+ {snippet.name}
+
+ })
+ },
+
+ render : function(){
+ return
+
+
+ {this.props.groupName}
+
+
+ {this.renderSnippets()}
+
+
+ },
+
+});
\ No newline at end of file
diff --git a/shared/homebrewery/brewEditor/snippetbar/snippetbar.less b/shared/homebrewery/brewEditor/snippetbar/snippetbar.less
new file mode 100644
index 0000000..45a6efe
--- /dev/null
+++ b/shared/homebrewery/brewEditor/snippetbar/snippetbar.less
@@ -0,0 +1,72 @@
+
+.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;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/shared/homebrewery/brewEditor/snippetbar/snippets/classfeature.gen.js b/shared/homebrewery/brewEditor/snippetbar/snippets/classfeature.gen.js
new file mode 100644
index 0000000..2ca3abc
--- /dev/null
+++ b/shared/homebrewery/brewEditor/snippetbar/snippets/classfeature.gen.js
@@ -0,0 +1,42 @@
+var _ = require('lodash');
+
+module.exports = function(classname){
+
+ classname = classname || _.sample(['archivist', 'fancyman', 'linguist', 'fletcher',
+ 'notary', 'berserker-typist', 'fishmongerer', 'manicurist', 'haberdasher', 'concierge'])
+
+ classname = classname.toLowerCase();
+
+ var hitDie = _.sample([4, 6, 8, 10, 12]);
+
+ var abilityList = ["Strength", "Dexerity", "Constitution", "Wisdom", "Charisma", "Intelligence"];
+ var skillList = ["Acrobatics ", "Animal Handling", "Arcana", "Athletics", "Deception", "History", "Insight", "Intimidation", "Investigation", "Medicine", "Nature", "Perception", "Performance", "Persuasion", "Religion", "Sleight of Hand", "Stealth", "Survival"];
+
+
+ return [
+ "## Class Features",
+ "As a " + classname + ", you gain the following class features",
+ "#### Hit Points",
+ "___",
+ "- **Hit Dice:** 1d" + hitDie + " per " + classname + " level",
+ "- **Hit Points at 1st Level:** " + hitDie + " + your Constitution modifier",
+ "- **Hit Points at Higher Levels:** 1d" + hitDie + " (or " + (hitDie/2 + 1) + ") + your Constitution modifier per " + classname + " level after 1st",
+ "",
+ "#### Proficiencies",
+ "___",
+ "- **Armor:** " + (_.sampleSize(["Light armor", "Medium armor", "Heavy armor", "Shields"], _.random(0,3)).join(', ') || "None"),
+ "- **Weapons:** " + (_.sampleSize(["Squeegee", "Rubber Chicken", "Simple weapons", "Martial weapons"], _.random(0,2)).join(', ') || "None"),
+ "- **Tools:** " + (_.sampleSize(["Artian's tools", "one musical instrument", "Thieve's tools"], _.random(0,2)).join(', ') || "None"),
+ "",
+ "___",
+ "- **Saving Throws:** " + (_.sampleSize(abilityList, 2).join(', ')),
+ "- **Skills:** Choose two from " + (_.sampleSize(skillList, _.random(4, 6)).join(', ')),
+ "",
+ "#### Equipment",
+ "You start with the following equipment, in addition to the equipment granted by your background:",
+ "- *(a)* a martial weapon and a shield or *(b)* two martial weapons",
+ "- *(a)* five javelins or *(b)* any simple melee weapon",
+ "- " + (_.sample(["10 lint fluffs", "1 button", "a cherished lost sock"])),
+ "\n\n\n"
+ ].join('\n');
+}
diff --git a/shared/homebrewery/brewEditor/snippetbar/snippets/classtable.gen.js b/shared/homebrewery/brewEditor/snippetbar/snippets/classtable.gen.js
new file mode 100644
index 0000000..649d85b
--- /dev/null
+++ b/shared/homebrewery/brewEditor/snippetbar/snippets/classtable.gen.js
@@ -0,0 +1,114 @@
+var _ = require('lodash');
+
+var features = [
+ "Astrological Botany",
+ "Astrological Chemistry",
+ "Biochemical Sorcery",
+ "Civil Alchemy",
+ "Consecrated Biochemistry",
+ "Demonic Anthropology",
+ "Divinatory Mineralogy",
+ "Genetic Banishing",
+ "Hermetic Geography",
+ "Immunological Incantations",
+ "Nuclear Illusionism",
+ "Ritual Astronomy",
+ "Seismological Divination",
+ "Spiritual Biochemistry",
+ "Statistical Occultism",
+ "Police Necromancer",
+ "Sixgun Poisoner",
+ "Pharmaceutical Gunslinger",
+ "Infernal Banker",
+ "Spell Analyst",
+ "Gunslinger Corruptor",
+ "Torque Interfacer",
+ "Exo Interfacer",
+ "Gunpowder Torturer",
+ "Orbital Gravedigger",
+ "Phased Linguist",
+ "Mathematical Pharmacist",
+ "Plasma Outlaw",
+ "Malefic Chemist",
+ "Police Cultist"
+];
+
+var classnames = ['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
+ 'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge'];
+
+var levels = ["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th", "11th", "12th", "13th", "14th", "15th", "16th", "17th", "18th", "19th", "20th"]
+
+var profBonus = [2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,6,6,6,6];
+
+var getFeature = (level)=>{
+ var res = []
+ if(_.includes([4,6,8,12,14,16,19], level+1)){
+ res = ["Ability Score Improvement"]
+ }
+ res = _.union(res, _.sampleSize(features, _.sample([0,1,1,1,1,1])));
+ if(!res.length) return "─";
+ return res.join(', ');
+}
+
+module.exports = {
+ full : function(){
+ var classname = _.sample(classnames)
+
+ var maxes = [4,3,3,3,3,2,2,1,1]
+ var drawSlots = function(Slots){
+ var slots = Number(Slots);
+ return _.times(9, function(i){
+ var max = maxes[i];
+ if(slots < 1) return "—";
+ var res = _.min([max, slots]);
+ slots -= res;
+ return res;
+ }).join(' | ')
+ }
+
+
+ var cantrips = 3;
+ var spells = 1;
+ var slots = 2;
+ return "
\n##### The " + classname + "\n" +
+ "| Level | Proficiency Bonus | Features | Cantrips Known | Spells Known | 1st | 2nd | 3rd | 4th | 5th | 6th | 7th | 8th | 9th |\n"+
+ "|:---:|:---:|:---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n" +
+ _.map(levels, function(levelName, level){
+ var res = [
+ levelName,
+ "+" + profBonus[level],
+ getFeature(level),
+ cantrips,
+ spells,
+ drawSlots(slots)
+ ].join(' | ');
+
+ cantrips += _.random(0,1);
+ spells += _.random(0,1);
+ slots += _.random(0,2);
+
+ return "| " + res + " |";
+ }).join('\n') +'\n
\n\n';
+ },
+
+ half : function(){
+ var classname = _.sample(classnames)
+
+ var featureScore = 1
+ return "
\n##### The " + classname + "\n" +
+ "| Level | Proficiency Bonus | Features | " + _.sample(features) + "|\n" +
+ "|:---:|:---:|:---|:---:|\n" +
+ _.map(levels, function(levelName, level){
+ var res = [
+ levelName,
+ "+" + profBonus[level],
+ getFeature(level),
+ "+" + featureScore
+ ].join(' | ');
+
+ featureScore += _.random(0,1);
+
+ return "| " + res + " |";
+ }).join('\n') +'\n
\n\n';
+ }
+};
\ No newline at end of file
diff --git a/shared/homebrewery/brewEditor/snippetbar/snippets/coverpage.gen.js b/shared/homebrewery/brewEditor/snippetbar/snippets/coverpage.gen.js
new file mode 100644
index 0000000..d4b69d6
--- /dev/null
+++ b/shared/homebrewery/brewEditor/snippetbar/snippets/coverpage.gen.js
@@ -0,0 +1,117 @@
+var _ = require('lodash');
+
+var titles = [
+ "The Burning Gallows",
+ "The Ring of Nenlast",
+ "Below the Blind Tavern",
+ "Below the Hungering River",
+ "Before Bahamut's Land",
+ "The Cruel Grave from Within",
+ "The Strength of Trade Road",
+ "Through The Raven Queen's Worlds",
+ "Within the Settlement",
+ "The Crown from Within",
+ "The Merchant Within the Battlefield",
+ "Ioun's Fading Traveler",
+ "The Legion Ingredient",
+ "The Explorer Lure",
+ "Before the Charming Badlands",
+ "The Living Dead Above the Fearful Cage",
+ "Vecna's Hidden Sage",
+ "Bahamut's Demonspawn",
+ "Across Gruumsh's Elemental Chaos",
+ "The Blade of Orcus",
+ "Beyond Revenge",
+ "Brain of Insanity",
+ "Breed Battle!, A New Beginning",
+ "Evil Lake, A New Beginning",
+ "Invasion of the Gigantic Cat, Part II",
+ "Kraken War 2020",
+ "The Body Whisperers",
+ "The Diabolical Tales of the Ape-Women",
+ "The Doctor Immortal",
+ "The Doctor from Heaven",
+ "The Graveyard",
+ "Azure Core",
+ "Core Battle",
+ "Core of Heaven: The Guardian of Amazement",
+ "Deadly Amazement III",
+ "Dry Chaos IX",
+ "Gate Thunder",
+ "Guardian: Skies of the Dark Wizard",
+ "Lute of Eternity",
+ "Mercury's Planet: Brave Evolution",
+ "Ruby of Atlantis: The Quake of Peace",
+ "Sky of Zelda: The Thunder of Force",
+ "Vyse's Skies",
+ "White Greatness III",
+ "Yellow Divinity",
+ "Zidane's Ghost"
+];
+
+var subtitles = [
+ "In an ominous universe, a botanist opposes terrorism.",
+ "In a demon-haunted city, in an age of lies and hate, a physicist tries to find an ancient treasure and battles a mob of aliens.",
+ "In a land of corruption, two cyberneticists and a dungeon delver search for freedom.",
+ "In an evil empire of horror, two rangers battle the forces of hell.",
+ "In a lost city, in an age of sorcery, a librarian quests for revenge.",
+ "In a universe of illusions and danger, three time travellers and an adventurer search for justice.",
+ "In a forgotten universe of barbarism, in an era of terror and mysticism, a virtual reality programmer and a spy try to find vengance and battle crime.",
+ "In a universe of demons, in an era of insanity and ghosts, three bodyguards and a bodyguard try to find vengance.",
+ "In a kingdom of corruption and battle, seven artificial intelligences try to save the last living fertile woman.",
+ "In a universe of virutal reality and agony, in an age of ghosts and ghosts, a fortune-teller and a wanderer try to avert the apocalypse.",
+ "In a crime-infested kingdom, three martial artists quest for the truth and oppose evil.",
+ "In a terrifying universe of lost souls, in an era of lost souls, eight dancers fight evil.",
+ "In a galaxy of confusion and insanity, three martial artists and a duke battle a mob of psychics.",
+ "In an amazing kingdom, a wizard and a secretary hope to prevent the destruction of mankind.",
+ "In a kingdom of deception, a reporter searches for fame.",
+ "In a hellish empire, a swordswoman and a duke try to find the ultimate weapon and battle a conspiracy.",
+ "In an evil galaxy of illusion, in a time of technology and misery, seven psychiatrists battle crime.",
+ "In a dark city of confusion, three swordswomen and a singer battle lawlessness.",
+ "In an ominous empire, in an age of hate, two philosophers and a student try to find justice and battle a mob of mages intent on stealing the souls of the innocent.",
+ "In a kingdom of panic, six adventurers oppose lawlessness.",
+ "In a land of dreams and hopelessness, three hackers and a cyborg search for justice.",
+ "On a planet of mysticism, three travelers and a fire fighter quest for the ultimate weapon and oppose evil.",
+ "In a wicked universe, five seers fight lawlessness.",
+ "In a kingdom of death, in an era of illusion and blood, four colonists search for fame.",
+ "In an amazing kingdom, in an age of sorcery and lost souls, eight space pirates quest for freedom.",
+ "In a cursed empire, five inventors oppose terrorism.",
+ "On a crime-ridden planet of conspiracy, a watchman and an artificial intelligence try to find love and oppose lawlessness.",
+ "In a forgotten land, a reporter and a spy try to stop the apocalypse.",
+ "In a forbidden land of prophecy, a scientist and an archivist oppose a cabal of barbarians intent on stealing the souls of the innocent.",
+ "On an infernal world of illusion, a grave robber and a watchman try to find revenge and combat a syndicate of mages intent on stealing the source of all magic.",
+ "In a galaxy of dark magic, four fighters seek freedom.",
+ "In an empire of deception, six tomb-robbers quest for the ultimate weapon and combat an army of raiders.",
+ "In a kingdom of corruption and lost souls, in an age of panic, eight planetologists oppose evil.",
+ "In a galaxy of misery and hopelessness, in a time of agony and pain, five planetologists search for vengance.",
+ "In a universe of technology and insanity, in a time of sorcery, a computer techician quests for hope.",
+ "On a planet of dark magic and barbarism, in an age of horror and blasphemy, seven librarians search for fame.",
+ "In an empire of dark magic, in a time of blood and illusions, four monks try to find the ultimate weapon and combat terrorism.",
+ "In a forgotten empire of dark magic, six kings try to prevent the destruction of mankind.",
+ "In a galaxy of dark magic and horror, in an age of hopelessness, four marines and an outlaw combat evil.",
+ "In a mysterious city of illusion, in an age of computerization, a witch-hunter tries to find the ultimate weapon and opposes an evil corporation.",
+ "In a damned kingdom of technology, a virtual reality programmer and a fighter seek fame.",
+ "In a hellish kingdom, in an age of blasphemy and blasphemy, an astrologer searches for fame.",
+ "In a damned world of devils, an alien and a ranger quest for love and oppose a syndicate of demons.",
+ "In a cursed galaxy, in a time of pain, seven librarians hope to avert the apocalypse.",
+ "In a crime-infested galaxy, in an era of hopelessness and panic, three champions and a grave robber try to solve the ultimate crime."
+];
+
+
+module.exports = () => {
+ return `
+
+
+
+# ${_.sample(titles)}
+
+
+
+##### ${_.sample(subtitles)}
+
+
+\\page`
+}
\ No newline at end of file
diff --git a/shared/homebrewery/brewEditor/snippetbar/snippets/fullclass.gen.js b/shared/homebrewery/brewEditor/snippetbar/snippets/fullclass.gen.js
new file mode 100644
index 0000000..9a2da20
--- /dev/null
+++ b/shared/homebrewery/brewEditor/snippetbar/snippets/fullclass.gen.js
@@ -0,0 +1,43 @@
+var _ = require('lodash');
+
+var ClassFeatureGen = require('./classfeature.gen.js');
+
+var ClassTableGen = require('./classtable.gen.js');
+
+module.exports = function(){
+
+ var classname = _.sample(['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
+ 'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge'])
+
+
+ var image = _.sample(_.map([
+ "http://orig01.deviantart.net/4682/f/2007/099/f/c/bard_stick_figure_by_wrpigeek.png",
+ "http://img07.deviantart.net/a3c9/i/2007/099/3/a/archer_stick_figure_by_wrpigeek.png",
+ "http://pre04.deviantart.net/d596/th/pre/f/2007/099/5/2/adventurer_stick_figure_by_wrpigeek.png",
+ "http://img13.deviantart.net/d501/i/2007/099/d/4/black_mage_stick_figure_by_wrpigeek.png",
+ "http://img09.deviantart.net/5cf3/i/2007/099/d/d/dark_knight_stick_figure_by_wrpigeek.png",
+ "http://pre01.deviantart.net/7a34/th/pre/f/2007/099/6/3/monk_stick_figure_by_wrpigeek.png",
+ "http://img11.deviantart.net/5dcc/i/2007/099/d/1/mystic_knight_stick_figure_by_wrpigeek.png",
+ "http://pre08.deviantart.net/ad45/th/pre/f/2007/099/a/0/thief_stick_figure_by_wrpigeek.png",
+ ], function(url){
+ return "
"
+ }))
+
+
+ return [
+ image,
+ "",
+ "```",
+ "```",
+ "
\n\n",
+ "## " + classname,
+ "Cool intro stuff will go here",
+
+ "\\page",
+ ClassTableGen(classname),
+ ClassFeatureGen(classname),
+
+
+
+ ].join('\n') + '\n\n\n';
+};
\ No newline at end of file
diff --git a/shared/homebrewery/brewEditor/snippetbar/snippets/magic.gen.js b/shared/homebrewery/brewEditor/snippetbar/snippets/magic.gen.js
new file mode 100644
index 0000000..82469cd
--- /dev/null
+++ b/shared/homebrewery/brewEditor/snippetbar/snippets/magic.gen.js
@@ -0,0 +1,91 @@
+var _ = require('lodash');
+
+var spellNames = [
+ "Astral Rite of Acne",
+ "Create Acne",
+ "Cursed Ramen Erruption",
+ "Dark Chant of the Dentists",
+ "Erruption of Immaturity",
+ "Flaming Disc of Inconvenience",
+ "Heal Bad Hygene",
+ "Heavenly Transfiguration of the Cream Devil",
+ "Hellish Cage of Mucus",
+ "Irritate Peanut Butter Fairy",
+ "Luminous Erruption of Tea",
+ "Mystic Spell of the Poser",
+ "Sorcerous Enchantment of the Chimneysweep",
+ "Steak Sauce Ray",
+ "Talk to Groupie",
+ "Astonishing Chant of Chocolate",
+ "Astounding Pasta Puddle",
+ "Ball of Annoyance",
+ "Cage of Yarn",
+ "Control Noodles Elemental",
+ "Create Nervousness",
+ "Cure Baldness",
+ "Cursed Ritual of Bad Hair",
+ "Dispell Piles in Dentist",
+ "Eliminate Florists",
+ "Illusionary Transfiguration of the Babysitter",
+ "Necromantic Armor of Salad Dressing",
+ "Occult Transfiguration of Foot Fetish",
+ "Protection from Mucus Giant",
+ "Tinsel Blast",
+ "Alchemical Evocation of the Goths",
+ "Call Fangirl",
+ "Divine Spell of Crossdressing",
+ "Dominate Ramen Giant",
+ "Eliminate Vindictiveness in Gym Teacher",
+ "Extra-Planar Spell of Irritation",
+ "Induce Whining in Babysitter",
+ "Invoke Complaining",
+ "Magical Enchantment of Arrogance",
+ "Occult Globe of Salad Dressing",
+ "Overwhelming Enchantment of the Chocolate Fairy",
+ "Sorcerous Dandruff Globe",
+ "Spiritual Invocation of the Costumers",
+ "Ultimate Rite of the Confetti Angel",
+ "Ultimate Ritual of Mouthwash",
+];
+
+module.exports = {
+
+ spellList : function(){
+ var levels = ['Cantrips (0 Level)', '2nd Level', '3rd Level', '4th Level', '5th Level', '6th Level', '7th Level', '8th Level', '9th Level'];
+
+ var content = _.map(levels, (level)=>{
+ var spells = _.map(_.sampleSize(spellNames, _.random(5, 15)), (spell)=>{
+ return `- ${spell}`;
+ }).join('\n');
+ return `##### ${level} \n${spells} \n`;
+ }).join('\n');
+
+ return `
\n${content}\n
`;
+ },
+
+ spell : function(){
+ var level = ["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th"];
+ var spellSchools = ["abjuration", "conjuration", "divination", "enchantment", "evocation", "illusion", "necromancy", "transmutation"];
+
+
+ var components = _.sampleSize(["V", "S", "M"], _.random(1,3)).join(', ');
+ if(components.indexOf("M") !== -1){
+ components += " (" + _.sampleSize(['a small doll', 'a crushed button worth at least 1cp', 'discarded gum wrapper'], _.random(1,3)).join(', ') + ")"
+ }
+
+ return [
+ "#### " + _.sample(spellNames),
+ "*" + _.sample(level) + "-level " + _.sample(spellSchools) + "*",
+ "___",
+ "- **Casting Time:** 1 action",
+ "- **Range:** " + _.sample(["Self", "Touch", "30 feet", "60 feet"]),
+ "- **Components:** " + components,
+ "- **Duration:** " + _.sample(["Until dispelled", "1 round", "Instantaneous", "Concentration, up to 10 minutes", "1 hour"]),
+ "",
+ "A flame, equivalent in brightness to a torch, springs from from an object that you touch. ",
+ "The effect look like a regular flame, but it creates no heat and doesn't use oxygen. ",
+ "A *continual flame* can be covered or hidden but not smothered or quenched.",
+ "\n\n\n"
+ ].join('\n');
+ }
+}
\ No newline at end of file
diff --git a/shared/homebrewery/brewEditor/snippetbar/snippets/monsterblock.gen.js b/shared/homebrewery/brewEditor/snippetbar/snippets/monsterblock.gen.js
new file mode 100644
index 0000000..abbcd6c
--- /dev/null
+++ b/shared/homebrewery/brewEditor/snippetbar/snippets/monsterblock.gen.js
@@ -0,0 +1,196 @@
+var _ = require('lodash');
+
+var genList = function(list, max){
+ return _.sampleSize(list, _.random(0,max)).join(', ') || "None";
+}
+
+var getMonsterName = function(){
+ return _.sample([
+ "All-devouring Baseball Imp",
+ "All-devouring Gumdrop Wraith",
+ "Chocolate Hydra",
+ "Devouring Peacock",
+ "Economy-sized Colossus of the Lemonade Stand",
+ "Ghost Pigeon",
+ "Gibbering Duck",
+ "Sparklemuffin Peacock Spider",
+ "Gum Elemental",
+ "Illiterate Construct of the Candy Store",
+ "Ineffable Chihuahua",
+ "Irritating Death Hamster",
+ "Irritating Gold Mouse",
+ "Juggernaut Snail",
+ "Juggernaut of the Sock Drawer",
+ "Koala of the Cosmos",
+ "Mad Koala of the West",
+ "Milk Djinni of the Lemonade Stand",
+ "Mind Ferret",
+ "Mystic Salt Spider",
+ "Necrotic Halitosis Angel",
+ "Pinstriped Famine Sheep",
+ "Ritalin Leech",
+ "Shocker Kangaroo",
+ "Stellar Tennis Juggernaut",
+ "Wailing Quail of the Sun",
+ "Angel Pigeon",
+ "Anime Sphinx",
+ "Bored Avalanche Sheep of the Wasteland",
+ "Devouring Nougat Sphinx of the Sock Drawer",
+ "Djinni of the Footlocker",
+ "Ectoplasmic Jazz Devil",
+ "Flatuent Angel",
+ "Gelatinous Duck of the Dream-Lands",
+ "Gelatinous Mouse",
+ "Golem of the Footlocker",
+ "Lich Wombat",
+ "Mechanical Sloth of the Past",
+ "Milkshake Succubus",
+ "Puffy Bone Peacock of the East",
+ "Rainbow Manatee",
+ "Rune Parrot",
+ "Sand Cow",
+ "Sinister Vanilla Dragon",
+ "Snail of the North",
+ "Spider of the Sewer",
+ "Stellar Sawdust Leech",
+ "Storm Anteater of Hell",
+ "Stupid Spirit of the Brewery",
+ "Time Kangaroo",
+ "Tomb Poodle",
+ ]);
+}
+
+var getType = function(){
+ return _.sample(['Tiny', 'Small', 'Medium', 'Large', 'Gargantuan', 'Stupidly vast']) + " " + _.sample(['beast', 'fiend', 'annoyance', 'guy', 'cutie'])
+}
+
+var getAlignment = function(){
+ return _.sample([
+ "annoying evil",
+ "chaotic gossipy",
+ "chaotic sloppy",
+ "depressed neutral",
+ "lawful bogus",
+ "lawful coy",
+ "manic-depressive evil",
+ "narrow-minded neutral",
+ "neutral annoying",
+ "neutral ignorant",
+ "oedpipal neutral",
+ "silly neutral",
+ "unoriginal neutral",
+ "weird neutral",
+ "wordy evil",
+ "unaligned"
+ ]);
+};
+
+var getStats = function(){
+ return '>|' + _.times(6, function(){
+ var num = _.random(1,20);
+ var mod = Math.ceil(num/2 - 5)
+ return num + " (" + (mod >= 0 ? '+'+mod : mod ) + ")"
+ }).join('|') + '|';
+}
+
+var genAbilities = function(){
+ return _.sample([
+ "> ***Pack Tactics.*** These guys work together. Like super well, you don't even know.",
+ "> ***False Appearance. *** While the armor reamin motionless, it is indistinguishable from a normal suit of armor.",
+ ]);
+}
+
+var genAction = function(){
+ var name = _.sample([
+ "Abdominal Drop",
+ "Airplane Hammer",
+ "Atomic Death Throw",
+ "Bulldog Rake",
+ "Corkscrew Strike",
+ "Crossed Splash",
+ "Crossface Suplex",
+ "DDT Powerbomb",
+ "Dual Cobra Wristlock",
+ "Dual Throw",
+ "Elbow Hold",
+ "Gory Body Sweep",
+ "Heel Jawbreaker",
+ "Jumping Driver",
+ "Open Chin Choke",
+ "Scorpion Flurry",
+ "Somersault Stump Fists",
+ "Suffering Wringer",
+ "Super Hip Submission",
+ "Super Spin",
+ "Team Elbow",
+ "Team Foot",
+ "Tilt-a-whirl Chin Sleeper",
+ "Tilt-a-whirl Eye Takedown",
+ "Turnbuckle Roll"
+ ])
+
+ return "> ***" + name + ".*** *Melee Weapon Attack:* +4 to hit, reach 5ft., one target. *Hit* 5 (1d6 + 2) ";
+}
+
+
+module.exports = {
+
+ full : function(){
+ return [
+ "___",
+ "___",
+ "> ## " + getMonsterName(),
+ ">*" + getType() + ", " + getAlignment() + "*",
+ "> ___",
+ "> - **Armor Class** " + _.random(10,20),
+ "> - **Hit Points** " + _.random(1, 150) + "(1d4 + 5)",
+ "> - **Speed** " + _.random(0,50) + "ft.",
+ ">___",
+ ">|STR|DEX|CON|INT|WIS|CHA|",
+ ">|:---:|:---:|:---:|:---:|:---:|:---:|",
+ getStats(),
+ ">___",
+ "> - **Condition Immunities** " + genList(["groggy", "swagged", "weak-kneed", "buzzed", "groovy", "melancholy", "drunk"], 3),
+ "> - **Senses** passive Perception " + _.random(3, 20),
+ "> - **Languages** " + genList(["Common", "Pottymouth", "Gibberish", "Latin", "Jive"], 2),
+ "> - **Challenge** " + _.random(0, 15) + " (" + _.random(10,10000)+ " XP)",
+ "> ___",
+ _.times(_.random(3,6), function(){
+ return genAbilities()
+ }).join('\n>\n'),
+ "> ### Actions",
+ _.times(_.random(4,6), function(){
+ return genAction()
+ }).join('\n>\n'),
+ ].join('\n') + '\n\n\n';
+ },
+
+ half : function(){
+ return [
+ "___",
+ "> ## " + getMonsterName(),
+ ">*" + getType() + ", " + getAlignment() + "*",
+ "> ___",
+ "> - **Armor Class** " + _.random(10,20),
+ "> - **Hit Points** " + _.random(1, 150) + "(1d4 + 5)",
+ "> - **Speed** " + _.random(0,50) + "ft.",
+ ">___",
+ ">|STR|DEX|CON|INT|WIS|CHA|",
+ ">|:---:|:---:|:---:|:---:|:---:|:---:|",
+ getStats(),
+ ">___",
+ "> - **Condition Immunities** " + genList(["groggy", "swagged", "weak-kneed", "buzzed", "groovy", "melancholy", "drunk"], 3),
+ "> - **Senses** passive Perception " + _.random(3, 20),
+ "> - **Languages** " + genList(["Common", "Pottymouth", "Gibberish", "Latin", "Jive"], 2),
+ "> - **Challenge** " + _.random(0, 15) + " (" + _.random(10,10000)+ " XP)",
+ "> ___",
+ _.times(_.random(0,2), function(){
+ return genAbilities()
+ }).join('\n>\n'),
+ "> ### Actions",
+ _.times(_.random(1,2), function(){
+ return genAction()
+ }).join('\n>\n'),
+ ].join('\n') + '\n\n\n';
+ }
+}
diff --git a/shared/homebrewery/brewEditor/snippetbar/snippets/snippets.js b/shared/homebrewery/brewEditor/snippetbar/snippets/snippets.js
new file mode 100644
index 0000000..bcb1df9
--- /dev/null
+++ b/shared/homebrewery/brewEditor/snippetbar/snippets/snippets.js
@@ -0,0 +1,267 @@
+var MagicGen = require('./magic.gen.js');
+var ClassTableGen = require('./classtable.gen.js');
+var MonsterBlockGen = require('./monsterblock.gen.js');
+var ClassFeatureGen = require('./classfeature.gen.js');
+var FullClassGen = require('./fullclass.gen.js');
+var CoverPageGen = require('./coverpage.gen.js');
+var TableOfContentsGen = require('./tableOfContents.gen.js');
+
+
+module.exports = [
+
+ {
+ groupName : 'Editor',
+ icon : 'fa-pencil',
+ snippets : [
+ {
+ name : "Column Break",
+ icon : 'fa-columns',
+ gen : "```\n```\n\n"
+ },
+ {
+ name : "New Page",
+ icon : 'fa-file-text',
+ gen : "\\page\n\n"
+ },
+ {
+ name : "Vertical Spacing",
+ icon : 'fa-arrows-v',
+ gen : "
\n\n"
+ },
+ {
+ name : "Wide Block",
+ icon : 'fa-arrows-h',
+ gen : "
\nEverything in here will be extra wide. Tables, text, everything! Beware though, CSS columns can behave a bit weird sometimes.\n
\n"
+ },
+ {
+ name : "Image",
+ icon : 'fa-image',
+ gen : [
+ "
",
+ "Credit: Kyounghwan Kim"
+ ].join('\n')
+ },
+ {
+ name : "Background Image",
+ icon : 'fa-tree',
+ gen : [
+ "
"
+ ].join('\n')
+ },
+
+ {
+ name : "Page Number",
+ icon : 'fa-bookmark',
+ gen : "
1
\n\n\n"
+ },
+
+ {
+ name : "Auto-incrementing Page Number",
+ icon : 'fa-sort-numeric-asc',
+ gen : "
\n"
+ },
+
+ {
+ name : "Link to page",
+ icon : 'fa-link',
+ gen : "[Click here](#p3) to go to page 3\n"
+ },
+
+ {
+ name : "Table of Contents",
+ icon : 'fa-book',
+ gen : TableOfContentsGen
+ },
+
+
+ ]
+ },
+
+
+ /************************* PHB ********************/
+
+ {
+ groupName : 'PHB',
+ icon : 'fa-book',
+ snippets : [
+ {
+ name : 'Spell',
+ icon : 'fa-magic',
+ gen : MagicGen.spell,
+ },
+ {
+ name : 'Spell List',
+ icon : 'fa-list',
+ gen : MagicGen.spellList,
+ },
+ {
+ name : 'Class Feature',
+ icon : 'fa-trophy',
+ gen : ClassFeatureGen,
+ },
+ {
+ name : 'Note',
+ icon : 'fa-sticky-note',
+ gen : function(){
+ return [
+ "> ##### Time to Drop Knowledge",
+ "> Use notes to point out some interesting information. ",
+ "> ",
+ "> **Tables and lists** both work within a note."
+ ].join('\n');
+ },
+ },
+ {
+ name : 'Descriptive Text Box',
+ icon : 'fa-sticky-note-o',
+ gen : function(){
+ return [
+ "
",
+ "##### Time to Drop Knowledge",
+ "Use notes to point out some interesting information. ",
+ "",
+ "**Tables and lists** both work within a note.",
+ "
"
+ ].join('\n');
+ },
+ },
+ {
+ name : 'Monster Stat Block',
+ icon : 'fa-bug',
+ gen : MonsterBlockGen.half,
+ },
+ {
+ name : 'Wide Monster Stat Block',
+ icon : 'fa-paw',
+ gen : MonsterBlockGen.full,
+ },
+ {
+ name : 'Cover Page',
+ icon : 'fa-file-word-o',
+ gen : CoverPageGen,
+ },
+ ]
+ },
+
+
+
+ /********************* TABLES *********************/
+
+ {
+ groupName : 'Tables',
+ icon : 'fa-table',
+ snippets : [
+ {
+ name : "Class Table",
+ icon : 'fa-table',
+ gen : ClassTableGen.full,
+ },
+ {
+ name : "Half Class Table",
+ icon : 'fa-list-alt',
+ gen : ClassTableGen.half,
+ },
+ {
+ name : 'Table',
+ icon : 'fa-th-list',
+ gen : function(){
+ return [
+ "##### Cookie Tastiness",
+ "| Tastiness | Cookie Type |",
+ "|:----:|:-------------|",
+ "| -5 | Raisin |",
+ "| 8th | Chocolate Chip |",
+ "| 11th | 2 or lower |",
+ "| 14th | 3 or lower |",
+ "| 17th | 4 or lower |\n\n",
+ ].join('\n');
+ },
+ },
+ {
+ name : 'Wide Table',
+ icon : 'fa-list',
+ gen : function(){
+ return [
+ "
",
+ "##### Cookie Tastiness",
+ "| Tastiness | Cookie Type |",
+ "|:----:|:-------------|",
+ "| -5 | Raisin |",
+ "| 8th | Chocolate Chip |",
+ "| 11th | 2 or lower |",
+ "| 14th | 3 or lower |",
+ "| 17th | 4 or lower |",
+ "
\n\n"
+ ].join('\n');
+ },
+ },
+ {
+ name : 'Split Table',
+ icon : 'fa-th-large',
+ gen : function(){
+ return [
+ "
",
+ "| d10 | Damage Type |",
+ "|:---:|:------------|",
+ "| 1 | Acid |",
+ "| 2 | Cold |",
+ "| 3 | Fire |",
+ "| 4 | Force |",
+ "| 5 | Lightning |",
+ "",
+ "```",
+ "```",
+ "",
+ "| d10 | Damage Type |",
+ "|:---:|:------------|",
+ "| 6 | Necrotic |",
+ "| 7 | Poison |",
+ "| 8 | Psychic |",
+ "| 9 | Radiant |",
+ "| 10 | Thunder |",
+ "
\n\n",
+ ].join('\n');
+ },
+ }
+ ]
+ },
+
+
+
+
+ /**************** PRINT *************/
+
+ {
+ groupName : 'Print',
+ icon : 'fa-print',
+ snippets : [
+ {
+ name : "A4 PageSize",
+ icon : 'fa-file-o',
+ gen : [''
+ ].join('\n')
+ },
+ {
+ name : "Ink Friendly",
+ icon : 'fa-tint',
+ gen : ['',
+ ''
+ ].join('\n')
+ },
+ ]
+ },
+
+]
diff --git a/shared/homebrewery/brewEditor/snippetbar/snippets/tableOfContents.gen.js b/shared/homebrewery/brewEditor/snippetbar/snippets/tableOfContents.gen.js
new file mode 100644
index 0000000..448b2f4
--- /dev/null
+++ b/shared/homebrewery/brewEditor/snippetbar/snippets/tableOfContents.gen.js
@@ -0,0 +1,72 @@
+const _ = require('lodash');
+
+const getTOC = (pages) => {
+ const add1 = (title, page)=>{
+ res.push({
+ title : title,
+ page : page + 1,
+ children : []
+ });
+ }
+ const add2 = (title, page)=>{
+ if(!_.last(res)) add1('', page);
+ _.last(res).children.push({
+ title : title,
+ page : page + 1,
+ children : []
+ });
+ }
+ const add3 = (title, page)=>{
+ if(!_.last(res)) add1('', page);
+ if(!_.last(_.last(res).children)) add2('', page);
+ _.last(_.last(res).children).children.push({
+ title : title,
+ page : page + 1,
+ children : []
+ });
+ }
+
+ let res = [];
+ _.each(pages, (page, pageNum)=>{
+ const lines = page.split('\n');
+ _.each(lines, (line) => {
+ if(_.startsWith(line, '# ')){
+ const title = line.replace('# ', '');
+ add1(title, pageNum)
+ }
+ if(_.startsWith(line, '## ')){
+ const title = line.replace('## ', '');
+ add2(title, pageNum);
+ }
+ if(_.startsWith(line, '### ')){
+ const title = line.replace('### ', '');
+ add3(title, pageNum);
+ }
+ })
+ });
+ return res;
+}
+
+module.exports = function(brew){
+ const pages = brew.split('\\page');
+ const TOC = getTOC(pages);
+ const markdown = _.reduce(TOC, (r, g1, idx1)=>{
+ r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`)
+ if(g1.children.length){
+ _.each(g1.children, (g2, idx2) => {
+ r.push(` - [${idx1 + 1}.${idx2 + 1} ${g2.title}](#p${g2.page})`);
+ if(g2.children.length){
+ _.each(g2.children, (g3, idx3) => {
+ r.push(` - [${idx1 + 1}.${idx2 + 1}.${idx3 + 1} ${g3.title}](#p${g3.page})`);
+ });
+ }
+ });
+ }
+ return r;
+ }, []).join('\n');
+
+ return `
+##### Table Of Contents
+${markdown}
+
\n`;
+}
\ No newline at end of file
diff --git a/shared/homebrewery/brewInterface/brewInterface.jsx b/shared/homebrewery/brewInterface/brewInterface.jsx
new file mode 100644
index 0000000..1b5b17c
--- /dev/null
+++ b/shared/homebrewery/brewInterface/brewInterface.jsx
@@ -0,0 +1,22 @@
+const React = require('react');
+const _ = require('lodash');
+
+const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
+const Editor = require('../brewEditor/brewEditor.smart.jsx');
+const BrewRenderer = require('../brewRenderer/brewRenderer.smart.jsx');
+
+
+const BrewInterface = React.createClass({
+
+ handleSplitMove : function(){
+ console.log('split move!');
+ },
+ render: function(){
+ return
+
+
+
+ }
+});
+
+module.exports = BrewInterface;
diff --git a/shared/homebrewery/brewInterface/brewInterface.less b/shared/homebrewery/brewInterface/brewInterface.less
new file mode 100644
index 0000000..ab46600
--- /dev/null
+++ b/shared/homebrewery/brewInterface/brewInterface.less
@@ -0,0 +1,3 @@
+.brewInterface{
+
+}
\ No newline at end of file
diff --git a/shared/homebrewery/brewRenderer/brewRenderer.jsx b/shared/homebrewery/brewRenderer/brewRenderer.jsx
new file mode 100644
index 0000000..ff59cff
--- /dev/null
+++ b/shared/homebrewery/brewRenderer/brewRenderer.jsx
@@ -0,0 +1,143 @@
+const React = require('react');
+const _ = require('lodash');
+const cx = require('classnames');
+
+const Markdown = require('homebrewery/markdown.js');
+const ErrorBar = require('./errorBar/errorBar.jsx');
+
+const Store = require('homebrewery/brew.store.js');
+
+
+const PAGE_HEIGHT = 1056;
+const PPR_THRESHOLD = 50;
+
+const BrewRenderer = React.createClass({
+ getDefaultProps: function() {
+ return {
+ brewText : '',
+ errors : []
+ };
+ },
+ getInitialState: function() {
+ const pages = this.props.brewText.split('\\page');
+
+ return {
+ viewablePageNumber: 0,
+ height : 0,
+ isMounted : false,
+ pages : pages,
+ usePPR : pages.length >= PPR_THRESHOLD
+ };
+ },
+ height : 0,
+ pageHeight : PAGE_HEIGHT,
+ lastRender :
,
+
+ 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.brewText.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);
+
+ this.setState({
+ height : this.refs.main.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('