0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-27 07:12:39 +00:00

Merge remote-tracking branch 'upstream/master' into Style-Editor-Menu-and-comments

This commit is contained in:
Gazook89
2021-09-05 15:11:58 -05:00
51 changed files with 1266 additions and 591 deletions

View File

@@ -32,7 +32,7 @@ module.exports = {
skipBlankLines : true,
}],
'max-depth' : ['warn', { max: 4 }],
'max-params' : ['warn', { max: 4 }],
'max-params' : ['warn', { max: 5 }],
'no-restricted-syntax' : ['warn', 'ClassDeclaration', 'SwitchStatement'],
'no-unused-vars' : ['warn', {
vars : 'all',

View File

@@ -6,6 +6,17 @@ h5 {
# changelog
### Tuesday, 17/08/2021 - v2.13.4
- Fixed user page crashing when user has untitled brew
##### G-Ambatte:
- Tweaks to user page tool tips
- Fix view counts being reset on Google Drive files
##### Gazook89 :
- New **PHB → Artist Credit** snippet
- **PRINT** snippets moved to the **Style Editor** tab
### Monday, 09/08/2021 - v2.13.3
##### G-Ambatte :
@@ -48,6 +59,8 @@ myStyle {color: black}
- Pasting your brew into a "New" page and saving will transfer any CSS in the code fence to the Style tab.
- Unsaved work in the New page Style tab is now cached to your browser storage if you navigate away.
\page
### Thursday, 10/6/2021 - v2.12.0

View File

@@ -30,7 +30,7 @@ const BrewRenderer = createClass({
if(this.props.renderer == 'legacy') {
pages = this.props.text.split('\\page');
} else {
pages = this.props.text.split(/^\\page/gm);
pages = this.props.text.split(/^\\page$/gm);
}
return {
@@ -62,7 +62,7 @@ const BrewRenderer = createClass({
if(this.props.renderer == 'legacy') {
pages = this.props.text.split('\\page');
} else {
pages = this.props.text.split(/^\\page/gm);
pages = this.props.text.split(/^\\page$/gm);
}
this.setState({
pages : pages,
@@ -130,8 +130,14 @@ const BrewRenderer = createClass({
renderPage : function(pageText, index){
if(this.props.renderer == 'legacy')
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }} key={index} />;
else
return <div className='phb3 page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} key={index} />;
else {
pageText += `\n\\column\n&nbsp;`; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
return (
<div className='page' id={`p${index + 1}`} key={index} >
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} />
</div>
);
}
},
renderPages : function(){

View File

@@ -68,15 +68,18 @@ const Editor = createClass({
},
handleInject : function(injectText){
const text = (this.isText() ? this.props.brew.text : this.props.brew.style);
let text;
if(this.isText()) text = this.props.brew.text;
if(this.isStyle()) text = this.props.brew.style ?? DEFAULT_STYLE_TEXT;
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 + injectText.split('\n').length, cursorPos.ch + injectText.length);
const injectLines = injectText.split('\n');
this.refs.codeEditor.setCursorPosition(cursorPos.line + injectLines.length, cursorPos.ch + injectLines[injectLines.length - 1].length);
if(this.isText()) this.props.onTextChange(lines.join('\n'));
if(this.isText()) this.props.onTextChange(lines.join('\n'));
if(this.isStyle()) this.props.onStyleChange(lines.join('\n'));
},
@@ -119,7 +122,7 @@ const Editor = createClass({
// New Codemirror styling for V3 renderer
if(this.props.renderer == 'V3') {
if(line.startsWith('\\page')){
if(line.match(/^\\page$/)){
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
r.push(lineNumber);
}

View File

@@ -167,6 +167,10 @@ const MetadataEditor = createClass({
onChange={(e)=>this.handleRenderer('V3', e)} />
V3
</label>
<a href='/v3_preview' target='_blank' rel='noopener noreferrer'>
Click here for a quick intro to V3!
</a>
</div>
</div>;
},

View File

@@ -43,6 +43,11 @@
display : inline-flex;
align-items : center;
}
a {
font-size : 0.7em;
font-weight : 800;
display : inline-flex;
}
input{
vertical-align : middle;
cursor : pointer;
@@ -62,9 +67,6 @@
.button(@silver);
}
small{
position : absolute;
bottom : -15px;
left : 0px;
font-size : 0.6em;
font-style : italic;
}

View File

@@ -40,19 +40,9 @@ const Snippetbar = createClass({
let snippets = [];
if(this.props.renderer === 'V3')
if(this.props.view === 'text') {
snippets = SnippetsV3.filter((SnippetsV3)=>SnippetsV3.view === 'text');
} else if(this.props.view === 'style') {
snippets = SnippetsV3.filter((SnippetsV3)=>SnippetsV3.view === 'style');
} else
snippets = null;
snippets = SnippetsV3.filter((snippetGroup)=>snippetGroup.view === this.props.view);
else
if(this.props.view === 'text') {
snippets = SnippetsLegacy.filter((SnippetsLegacy)=>SnippetsLegacy.view === 'text');
} else if(this.props.view === 'style') {
snippets = SnippetsLegacy.filter((SnippetsLegacy)=>SnippetsLegacy.view === 'style');
} else
snippets = null;
snippets = SnippetsLegacy.filter((snippetGroup)=>snippetGroup.view === this.props.view);
return _.map(snippets, (snippetGroup)=>{
return <SnippetGroup

View File

@@ -2,86 +2,77 @@ const _ = require('lodash');
const features = [
'Astrological Botany',
'Astrological Chemistry',
'Biochemical Sorcery',
'Civil Alchemy',
'Consecrated Biochemistry',
'Civil Divination',
'Consecrated Augury',
'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',
'Genetic Banishing',
'Gunpowder Torturer',
'Orbital Gravedigger',
'Phased Linguist',
'Mathematical Pharmacist',
'Plasma Outlaw',
'Gunslinger Corruptor',
'Hermetic Geography',
'Immunological Cultist',
'Malefic Chemist',
'Police Cultist'
'Mathematical Pharmacy',
'Nuclear Biochemistry',
'Orbital Gravedigger',
'Pharmaceutical Outlaw',
'Phased Linguist',
'Plasma Gunslinger',
'Police Necromancer',
'Ritual Astronomy',
'Sixgun Poisoner',
'Seismological Alchemy',
'Spiritual Illusionism',
'Statistical Occultism',
'Spell Analyst',
'Torque Interfacer'
];
const classnames = ['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge'];
const classnames = ['Ackerman', 'Berserker-Typist', 'Concierge', 'Fishmonger',
'Haberdasher', 'Manicurist', 'Netrunner', 'Weirkeeper'];
const levels = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th', '8th', '9th', '10th', '11th', '12th', '13th', '14th', '15th', '16th', '17th', '18th', '19th', '20th'];
const levels = ['1st', '2nd', '3rd', '4th', '5th',
'6th', '7th', '8th', '9th', '10th',
'11th', '12th', '13th', '14th', '15th',
'16th', '17th', '18th', '19th', '20th'];
const profBonus = [2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6];
const getFeature = (level)=>{
let res = [];
if(_.includes([4, 6, 8, 12, 14, 16, 19], level+1)){
res = ['Ability Score Improvement'];
}
res = _.union(res, _.sampleSize(features, _.sample([0, 1, 1, 1, 1, 1])));
if(!res.length) return '─';
return res.join(', ');
const maxes = [4, 3, 3, 3, 3, 2, 2, 1, 1];
const drawSlots = function(Slots, rows, padding){
let slots = Number(Slots);
return _.times(rows, function(i){
const max = maxes[i];
if(slots < 1) return _.pad('—', padding);
const res = _.min([max, slots]);
slots -= res;
return _.pad(res.toString(), padding);
}).join(' | ');
};
module.exports = {
full : function(){
full : function(classes){
const classname = _.sample(classnames);
const maxes = [4, 3, 3, 3, 3, 2, 2, 1, 1];
const drawSlots = function(Slots){
let slots = Number(Slots);
return _.times(9, function(i){
const max = maxes[i];
if(slots < 1) return '—';
const res = _.min([max, slots]);
slots -= res;
return res;
}).join(' | ');
};
let cantrips = 3;
let spells = 1;
let slots = 2;
return `{{classTable,wide\n##### The ${classname}\n` +
`| Level | Proficiency | Features | Cantrips | Spells | --- Spell Slots Per Level --- |||||||||\n`+
`| ^| Bonus ^| ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th | 5th | 6th | 7th | 8th | 9th |\n`+
`|:-----:|:-----------:|:---------|:--------:|:------:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n${
return `{{${classes}\n##### The ${classname}\n` +
`| Level | Proficiency | Features | Cantrips | Spells | --- Spell Slots Per Spell Level ---|||||||||\n`+
`| ^| Bonus ^| ^| Known ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |\n`+
`|:-----:|:-----------:|:-------------|:--------:|:------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|\n${
_.map(levels, function(levelName, level){
const res = [
levelName,
`+${profBonus[level]}`,
getFeature(level),
cantrips,
spells,
drawSlots(slots)
_.pad(levelName, 5),
_.pad(`+${profBonus[level]}`, 2),
_.padEnd(_.sample(features), 21),
_.pad(cantrips.toString(), 8),
_.pad(spells.toString(), 6),
drawSlots(slots, 9, 2),
].join(' | ');
cantrips += _.random(0, 1);
@@ -92,24 +83,50 @@ module.exports = {
}).join('\n')}\n}}\n\n`;
},
half : function(){
half : function(classes){
const classname = _.sample(classnames);
let featureScore = 1;
return `<div class='classTable'>\n##### The ${classname}\n` +
`| Level | Proficiency Bonus | Features | ${_.sample(features)}|\n` +
`|:---:|:---:|:---|:---:|\n${
return `{{${classes}\n##### The ${classname}\n` +
`| Level | Proficiency Bonus | Features | ${_.pad(_.sample(features), 21)} |\n` +
`|:-----:|:-----------------:|:---------|:---------------------:|\n${
_.map(levels, function(levelName, level){
const res = [
levelName,
`+${profBonus[level]}`,
getFeature(level),
`+${featureScore}`
_.pad(levelName, 5),
_.pad(`+${profBonus[level]}`, 2),
_.padEnd(_.sample(features), 23),
_.pad(`+${featureScore}`, 21),
].join(' | ');
featureScore += _.random(0, 1);
return `| ${res} |`;
}).join('\n')}\n</div>\n\n`;
}).join('\n')}\n}}\n\n`;
},
third : function(classes){
const classname = _.sample(classnames);
let cantrips = 3;
let spells = 1;
let slots = 2;
return `{{${classes}\n##### ${classname} Spellcasting\n` +
`| Class | Cantrips | Spells |--- Spells Slots per Spell Level ---||||\n` +
`| Level ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |\n` +
`|:------:|:--------:|:-------:|:-------:|:-------:|:-------:|:-------:|\n${
_.map(levels, function(levelName, level){
const res = [
_.pad(levelName, 6),
_.pad(cantrips.toString(), 8),
_.pad(spells.toString(), 7),
drawSlots(slots, 4, 7),
].join(' | ');
cantrips += _.random(0, 1);
spells += _.random(0, 1);
slots += _.random(0, 1);
return `| ${res} |`;
}).join('\n')}\n}}\n\n`;
}
};

View File

@@ -7,6 +7,7 @@ const ClassFeatureGen = require('./classfeature.gen.js');
const CoverPageGen = require('./coverpage.gen.js');
const TableOfContentsGen = require('./tableOfContents.gen.js');
const dedent = require('dedent-tabs').default;
const watercolorGen = require('./watercolor.gen.js');
module.exports = [
@@ -43,23 +44,11 @@ module.exports = [
{{wide
Everything in here will be extra wide. Tables, text, everything!
Beware though, CSS columns can behave a bit weird sometimes. You may
have to rely on the automatic column-break rather than \`\column\` if
you mix columns and wide blocks on the same page.
have to manually place column breaks with \`\column\` to make the
surrounding text flow with this wide block the way you want.
}}
\n`
},
{
name : 'Image',
icon : 'fas fa-image',
gen : dedent`
![cat warrior](https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg) {width:325px}
Credit: Kyounghwan Kim`
},
{
name : 'Background Image',
icon : 'fas fa-tree',
gen : `![homebrew mug](http://i.imgur.com/hMna6G0.png) {position:absolute,top:50px,right:30px,width:280px}`
},
{
name : 'QR Code',
icon : 'fas fa-qrcode',
@@ -69,7 +58,6 @@ module.exports = [
`https://homebrewery.naturalcrit.com/share/${brew.shareId}` +
`&amp;size=100x100) {width:100px;mix-blend-mode:multiply}`;
}
},
{
name : 'Page Number',
@@ -135,6 +123,48 @@ module.exports = [
]
},
/*********************** IMAGES *******************/
{
groupName : 'Images',
icon : 'fas fa-images',
view : 'text',
snippets : [
{
name : 'Image',
icon : 'fas fa-image',
gen : dedent`
![cat warrior](https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg) {width:325px,mix-blend-mode:multiply}
{{artist,position:relative,top:-230px,left:-100px,margin-bottom:-30px
##### Cat Warrior
[Kyoung Hwan Kim](https://www.artstation.com/tahra)
}}`
},
{
name : 'Background Image',
icon : 'fas fa-tree',
gen : dedent`
![homebrew mug](http://i.imgur.com/hMna6G0.png) {position:absolute,top:50px,right:30px,width:280px}
{{artist,top:90px,right:30px
##### Homebrew Mug
[naturalcrit](https://homebrew.naturalcrit.com)
}}`
},
{
name : 'Watercolor Splatter',
icon : 'fas fa-fill-drip',
gen : watercolorGen,
},
{
name : 'Watermark',
icon : 'fas fa-id-card',
gen : dedent`
{{watermark Homebrewery}}\n`
},
]
},
/************************* PHB ********************/
@@ -211,6 +241,18 @@ module.exports = [
icon : 'fas fa-hat-wizard',
gen : MagicGen.item,
},
{
name : 'Artist Credit',
icon : 'fas fa-signature',
gen : function(){
return dedent`
{{artist,top:90px,right:30px
##### Starry Night
[Van Gogh](https://www.vangoghmuseum.nl/en)
}}
\n`;
},
},
]
},
@@ -226,12 +268,32 @@ module.exports = [
{
name : 'Class Table',
icon : 'fas fa-table',
gen : ClassTableGen.full,
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
},
{
name : 'Half Class Table',
name : 'Class Table (unframed)',
icon : 'fas fa-border-none',
gen : ClassTableGen.full('classTable,wide'),
},
{
name : '1/2 Class Table',
icon : 'fas fa-list-alt',
gen : ClassTableGen.half,
gen : ClassTableGen.half('classTable,decoration,frame'),
},
{
name : '1/2 Class Table (unframed)',
icon : 'fas fa-border-none',
gen : ClassTableGen.half('classTable'),
},
{
name : '1/3 Class Table',
icon : 'fas fa-border-all',
gen : ClassTableGen.third('classTable,frame'),
},
{
name : '1/3 Class Table (unframed)',
icon : 'fas fa-border-none',
gen : ClassTableGen.third('classTable'),
},
{
name : 'Table',
@@ -302,7 +364,7 @@ module.exports = [
/**************** PAGE *************/
{
groupName : 'Page',
groupName : 'Print',
icon : 'fas fa-print',
view : 'style',
snippets : [
@@ -310,8 +372,8 @@ module.exports = [
name : 'A4 Page Size',
icon : 'far fa-file',
gen : ['/* A4 Page Size */',
'.phb{',
' width : 210mm;',
'.page{',
' width : 210mm;',
' height : 296.8mm;',
'}',
''
@@ -322,10 +384,10 @@ module.exports = [
icon : 'far fa-file',
gen : ['/* Square Page Size */',
'.page {',
' width:5.25in;',
' height:5.25in;',
' padding:.5in;',
' columns:unset;',
' width : 125mm;',
' height : 125mm;',
' padding : 12.5mm;',
' columns : unset;',
'}',
''
].join('\n')
@@ -333,17 +395,20 @@ module.exports = [
{
name : 'Ink Friendly',
icon : 'fas fa-tint',
gen : ['/* Ink Friendly */',
'.pages *:is(.page,.monster,.note,.descriptive) {',
' background:white !important;',
' box-shadow:0px 0px 3px !important;',
'}',
'',
'.page .note:before {',
' box-shadow:0px 0px 3px;',
'}',
''
].join('\n')
gen : dedent`
/* Ink Friendly */
.pages *:is(.page,.monster,.note,.descriptive) {
background : white !important;
box-shadow : 0px 0px 3px !important;
}
.page .note:before {
box-shadow : 0px 0px 3px;
}
.page img {
visibility : hidden;
}`
},
]
},

View File

@@ -0,0 +1,5 @@
const _ = require('lodash');
module.exports = ()=>{
return `{{watercolor${_.random(1, 12)},top:20px,left:30px,width:300px,background-color:#BBAD82,opacity:80%}}\n\n`;
};

View File

@@ -6,7 +6,7 @@ const MonsterBlockGen = require('./monsterblock.gen.js');
const ClassFeatureGen = require('./classfeature.gen.js');
const CoverPageGen = require('./coverpage.gen.js');
const TableOfContentsGen = require('./tableOfContents.gen.js');
const dedent = require('dedent-tabs').default;
module.exports = [
@@ -184,6 +184,14 @@ module.exports = [
icon : 'far fa-file-word',
gen : CoverPageGen,
},
{
name : 'Artist Credit',
icon : 'fas fa-signature',
gen : '<div class=\'artist\' style=\'top:90px;right:30px;\'>\n' +
'##### Starry Night\n' +
'[Van Gogh](https://www.vangoghmuseum.nl/en)\n' +
'</div>\n'
},
]
},
@@ -277,7 +285,7 @@ module.exports = [
/**************** PRINT *************/
{
groupName : 'Page',
groupName : 'Print',
icon : 'fas fa-print',
view : 'style',
snippets : [
@@ -286,8 +294,8 @@ module.exports = [
icon : 'far fa-file',
gen : ['/* A4 Page Size */',
'.phb {',
' width : 210mm;',
' height : 296.8mm;',
' width : 210mm;',
' height : 296.8mm;',
'}'
].join('\n')
},
@@ -296,10 +304,10 @@ module.exports = [
icon : 'far fa-file',
gen : ['/* Square Page Size */',
'.phb {',
' width:5.25in;',
' height:5.25in;',
' padding:.5in;',
' columns:unset;',
' width : 125mm;',
' height : 125mm;',
' padding : 12.5mm;',
' columns : unset;',
'}',
''
].join('\n')
@@ -307,13 +315,16 @@ module.exports = [
{
name : 'Ink Friendly',
icon : 'fas fa-tint',
gen : ['/* Ink Friendly */',
'.phb, .phb blockquote, .phb hr+blockquote {',
' background : white;',
' box-shadow : 0px 0px 3px;',
'}',
''
].join('\n')
gen : dedent`
/* Ink Friendly */
.phb, .phb blockquote, .phb hr+blockquote {
background : white;
box-shadow : 0px 0px 3px;
}
.phb img {
visibility : hidden;
}`
},
]
},

View File

@@ -49,6 +49,7 @@ const Homebrew = createClass({
<Route path='/print/:id' component={(routeProps)=><PrintPage brew={this.props.brew} query={queryString.parse(routeProps.location.search)} />}/>
<Route path='/print' exact component={(routeProps)=><PrintPage query={queryString.parse(routeProps.location.search)} />}/>
<Route path='/changelog' exact component={()=><SharePage brew={this.props.brew} />}/>
<Route path='/v3_preview' exact component={()=><HomePage brew={this.props.brew} />}/>
<Route path='/' component={()=><HomePage brew={this.props.brew} />}/>
</Switch>
</div>

View File

@@ -196,11 +196,14 @@ const EditPage = createClass({
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
const brew = this.state.brew;
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
if(this.state.saveGoogle) {
if(transfer) {
const res = await request
.post('/api/newGoogle/')
.send(this.state.brew)
.send(brew)
.catch((err)=>{
console.log(err.status === 401
? 'Not signed in!'
@@ -211,7 +214,7 @@ const EditPage = createClass({
if(!res) { return; }
console.log('Deleting Local Copy');
await request.delete(`/api/${this.state.brew.editId}`)
await request.delete(`/api/${brew.editId}`)
.send()
.catch((err)=>{
console.log('Error deleting Local Copy');
@@ -221,8 +224,8 @@ const EditPage = createClass({
history.replaceState(null, null, `/edit/${this.savedBrew.googleId}${this.savedBrew.editId}`); //update URL to match doc ID
} else {
const res = await request
.put(`/api/updateGoogle/${this.state.brew.editId}`)
.send(this.state.brew)
.put(`/api/updateGoogle/${brew.editId}`)
.send(brew)
.catch((err)=>{
console.log(err.status === 401
? 'Not signed in!'
@@ -236,14 +239,14 @@ const EditPage = createClass({
} else {
if(transfer) {
const res = await request.post('/api')
.send(this.state.brew)
.send(brew)
.catch((err)=>{
console.log('Error creating Local Copy');
this.setState({ errors: err });
return;
});
await request.get(`/api/removeGoogle/${this.state.brew.googleId}${this.state.brew.editId}`)
await request.get(`/api/removeGoogle/${brew.googleId}${brew.editId}`)
.send()
.catch((err)=>{
console.log('Error Deleting Google Brew');
@@ -253,8 +256,8 @@ const EditPage = createClass({
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`); //update URL to match doc ID
} else {
const res = await request
.put(`/api/update/${this.state.brew.editId}`)
.send(this.state.brew)
.put(`/api/update/${brew.editId}`)
.send(brew)
.catch((err)=>{
console.log('Error Updating Local Brew');
this.setState({ errors: err });
@@ -322,7 +325,9 @@ const EditPage = createClass({
let errMsg = '';
try {
errMsg += `${this.state.errors.toString()}\n\n`;
errMsg += `\`\`\`\n${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
errMsg += `\`\`\`\n${this.state.errors.stack}\n`;
errMsg += `${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
console.log(errMsg);
} catch (e){}
if(this.state.errors.status == '401'){
@@ -344,6 +349,27 @@ const EditPage = createClass({
</Nav.item>;
}
if(this.state.errors.status == '403' && this.state.errors.response.body.errors[0].reason == 'insufficientPermissions'){
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={this.clearErrors}>
Looks like your Google credentials have
expired! Visit the log in page to sign out
and sign back in with Google
to save this to Google Drive!
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
<div className='confirm'>
Sign In
</div>
</a>
<div className='deny'>
Not Now
</div>
</div>
</Nav.item>;
}
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer'>

View File

@@ -1,6 +1,7 @@
require('./homePage.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const request = require('superagent');
const { Meta } = require('vitreum/headtags');
@@ -49,9 +50,9 @@ const HomePage = createClass({
this.refs.editor.update();
},
handleTextChange : function(text){
this.setState({
brew : { text: text }
});
this.setState((prevState)=>({
brew : _.merge({}, prevState.brew, { text: text })
}));
},
renderNavbar : function(){
return <Navbar ver={this.props.ver}>
@@ -81,7 +82,7 @@ const HomePage = createClass({
renderer={this.state.brew.renderer}
showEditButtons={false}
/>
<BrewRenderer text={this.state.brew.text} />
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer}/>
</SplitPane>
</div>

View File

@@ -5,7 +5,7 @@
position : absolute;
display : block;
right : 70px;
bottom : 70px;
bottom : 50px;
z-index : 100;
z-index : 5001;
padding : 1em;
@@ -23,7 +23,7 @@
position : absolute;
display : block;
right : 200px;
bottom : 90px;
bottom : 70px;
z-index : 100;
z-index : 5000;
padding : 0.8em;
@@ -40,4 +40,4 @@
right : 350px;
}
}
}
}

View File

@@ -0,0 +1,173 @@
```css
.page #example + table td {
border:1px dashed #00000030;
}
.page {
padding-bottom : 1.3cm;
}
```
# The Homebrewery *V3*
Welcome traveler from an antique land. Please sit and tell us of what you have seen. The unheard of monsters, who slither and bite. Tell us of the wondrous items and and artifacts you have found, their mysteries yet to be unlocked. Of the vexing vocations and surprising skills you have seen.
### Homebrew D&D made easy
The Homebrewery makes the creation and sharing of authentic looking Fifth-Edition homebrews easy. It uses [Markdown](https://help.github.com/articles/markdown-basics/) with a little CSS magic to make your brews come to life.
**Try it!** Simply edit the text on the left and watch it *update live* on the right. Note that not every button is visible on this demo page. Click New {{fas,fa-plus-square}} in the navbar above to start brewing with all the features!
### Editing and Sharing
When you create your own homebrew, you will be given a *edit url* and a *share url*.
Any changes you make while on the *edit url* will be automatically saved to the database within a few seconds. Anyone with the edit url will be able to make edits to your homebrew, so be careful about who you share it with.
Anyone with the *share url* will be able to access a read-only version of your homebrew.
### PDF Creation
PDF Printing works best in Google Chrome. If you are having quality/consistency issues, try using Chrome to print instead.
After clicking the "Print" item in the navbar a new page will open and a print dialog will pop-up.
* Set the **Destination** to "Save as PDF"
* Set **Paper Size** to "Letter"
* If you are printing on A4 paper, make sure to have the {{far,fa-file}} **A4 Pagesize** snippet in your brew
* In **Options** make sure "Background Images" is selected.
* Hit print and enjoy! You're done!
If you want to save ink or have a monochrome printer, add the {{fas,fa-tint}} **Ink Friendly** snippet to your brew before you print
<img src='https://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:50px;left:120px;width:180px' />
<div class='pageNumber'>1</div>
<div class='footnote'>PART 1 | FANCINESS</div>
\column
## New in V3.0.0
With the latest major update to *The Homebrewery* we've implemented an extended Markdown-like syntax for block and span elements, plus a few other changes, eliminating the need for HTML tags like `div` and `span` in most cases. No raw HTML tags should be needed in a brew, and going forward, raw HTML will no longer receive debugging support (*but can still be used if you insist*).
All brews made prior to the release of v3.0.0 will still render normally, and you may switch between the "legacy" brew renderer and the newer "V3" renderer via the {{fa,fa-info-circle}} **Properties** button on your brew. Much of the syntax and styling has changed in V3, so code in one version may be broken in the other.
Scroll down to the next page for a brief summary of the changes and new features available in V3!
#### New Things All The Time!
What's new in the latest update? Check out the full changelog [here](/changelog).
### Helping out
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/Naturalcrit) to help me keep the servers running.
This tool will **always** be free, never have ads, and I will never offer any "premium" features or whatever.
### Bugs, Issues, Suggestions?
Need help getting started or just the right look for your brew? Head to [r/Homebrewery](https://www.reddit.com/r/homebrewery/submit?selftext=true&title=%5BIssue%5D%20Describe%20Your%20Issue%20Here) and let us know!
Have an idea to make The Homebrewery better? Or did you find something that wasn't quite right? Check out the [GitHub Repo](https://github.com/naturalcrit/homebrewery/) to report technical issues.
### Legal Junk
The Homebrewery is licensed using the [MIT License](https://github.com/naturalcrit/homebrewery/blob/master/license). Which means you are free to use The Homebrewery codebase any way that you want, except for claiming that you made it yourself.
If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
#### Crediting Me
If you'd like to credit The Homebrewery in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
### More Resources
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/comments/3uwxx9/resources_open_to_the_community/).
\page
## Markdown+
The Homebrewery aims to make homebrewing as simple as possible, providing a live editor with Markdown syntax that is more human-readable and faster to write with than raw HTML.
In version 3.0.0, with a goal of adding maximum flexibility without users resorting to complex HTML to accomplish simple tasks, Homebrewery provides an extended verision of Markdown with additional syntax.
**You can enable V3 via the {{fa,fa-info-circle}} Properties button!**
### Curly Brackets
The biggest change in V3 is the replacement of `<span></span>` and `<div></div>` with `{{ }}` for a cleaner custom formatting. Inline spans and block elements can be created and given ID's and Classes, as well as css properties, each of which are comma separated with no spaces. Use double quotes if a value requires spaces. Spans and Blocks start the same:
#### Span
My favorite author is {{pen,#author,color:orange,font-family:"trebuchet ms" Brandon Sanderson}}. The orange text has a class of `pen`, an id of `author`, is colored orange, and given a new font. The first space outside of quotes marks the beginning of the content.
#### Block
{{purple,#book,text-align:center,background:#aa88aa55
My favorite book is Wheel of Time. This block has a class of `purple`, an id of `book`, and centered text with a colored background. The opening and closing brackets are on lines separate from the block contents.
}}
#### Injection
For any element not inside a span or block, you can *inject* attributes using the same syntax but with single brackets in a single line immediately after the element.
Inline elements like *italics* {color:#D35400} or images require the injection on the same line.
Block elements like headers require the injection to start on the line immediately following.
##### A Purple Header
{color:purple,text-align:center}
\* *this does not currently work for tables yet*
### Vertical Spacing
A blank line can be achieved with a run of one or more `:` alone on a line. More `:`'s will create more space.
::
Much nicer than `<br><br><br><br><br>`
### Definition Lists
V3 uses HTML *definition lists* to create "lists" with hanging indents.
**Senses** :: Here is some text that is long and overflows into a second line, creating a "hanging indent".
### Column Breaks
Column and page breaks with `\column` and `\page`.
\column
### Tables
Tables now allow column & row spanning between cells. This is included in some updated snippets, but a simplified example is given below.
A cell can be spanned across columns by grouping multiple pipe `|` characters at the end of a cell.
Row spanning is achieved by adding a `^` at the end of a cell just before the `|`.
These can be combined to span a cell across both columns and rows. Cells must have the same colspan if they are to be rowspan'd.
##### Example
| | Spanned Header ||
| Head A | Head B | Head C |
|:-------|:-------|:-------|
| 1A | 1B | 1C |
| 2A ^| 2B | 2C |
| 3A ^| 3B 3C ||
| 4A | 4B 4C^||
| 5A ^| 5B | 5C |
| 6A | 6B ^| 6C |
## Images
Images must be hosted online somewhere, like [Imgur](https://www.imgur.com). You use the address to that image to reference it in your brew\*. Images can be included using Markdown-style images.
Using *Curly Injection* you can assign an id, classes, or specific inline CSS properties to the image.
![alt-text](https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg) {width:100px,border:"2px solid",border-radius:10px}
\* *When using Imgur-hosted images, use the "direct link", which can be found when you click into your image in the Imgur interace.*
## Snippets
Homebrewery comes with a series of *code snippets* found at the top of the editor pane that make it easy to create brews as quickly as possible. Just set your cursor where you want the code to appear in the editor pane, choose a snippet, and make the adjustments you need.
## Style Editor Panel
{{fa,fa-paint-brush}} Technically released prior to v3 but still new to many users, check out the new **Style Editor** located on the right side of the Snippet bar. This editor accepts CSS for styling without requiring `<style>` tags-- anything that would have gone inside style tags before can now be placed here, and snippets that insert CSS styles are now located on that tab.
<div class='pageNumber'>2</div>
<div class='footnote'>PART 2 | BORING STUFF</div>

View File

@@ -61,7 +61,7 @@ const NewPage = createClass({
isSaving : false,
saveGoogle : (global.account && global.account.googleId ? true : false),
errors : [],
errors : null,
htmlErrors : Markdown.validate(this.props.brew.text)
};
},
@@ -138,6 +138,14 @@ const NewPage = createClass({
}));
},
clearErrors : function(){
this.setState({
errors : null,
isSaving : false
});
},
save : async function(){
this.setState({
isSaving : true
@@ -153,6 +161,8 @@ const NewPage = createClass({
brew.text = brew.text.slice(index + 5);
};
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
if(this.state.saveGoogle) {
const res = await request
.post('/api/newGoogle/')
@@ -161,7 +171,7 @@ const NewPage = createClass({
console.log(err.status === 401
? 'Not signed in!'
: 'Error Creating New Google Brew!');
this.setState({ isSaving: false });
this.setState({ isSaving: false, errors: err });
return;
});
@@ -191,12 +201,73 @@ const NewPage = createClass({
},
renderSaveButton : function(){
if(this.state.errors){
let errMsg = '';
try {
errMsg += `${this.state.errors.toString()}\n\n`;
errMsg += `\`\`\`\n${this.state.errors.stack}\n`;
errMsg += `${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
console.log(errMsg);
} catch (e){}
if(this.state.errors.status == '401'){
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={this.clearErrors}>
You must be signed in to a Google account
to save this to<br />Google Drive!<br />
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
<div className='confirm'>
Sign In
</div>
</a>
<div className='deny'>
Not Now
</div>
</div>
</Nav.item>;
}
if(this.state.errors.status == '403' && this.state.errors.response.body.errors[0].reason == 'insufficientPermissions'){
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={this.clearErrors}>
Looks like your Google credentials have
expired! Visit the log in page to sign out
and sign back in with Google
to save this to Google Drive!
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
<div className='confirm'>
Sign In
</div>
</a>
<div className='deny'>
Not Now
</div>
</div>
</Nav.item>;
}
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer'>
Looks like there was a problem saving. <br />
Report the issue <a target='_blank' rel='noopener noreferrer'
href={`https://github.com/naturalcrit/homebrewery/issues/new?body=${encodeURIComponent(errMsg)}`}>
here
</a>.
</div>
</Nav.item>;
}
if(this.state.isSaving){
return <Nav.item icon='fas fa-spinner fa-spin' className='saveButton'>
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
save...
</Nav.item>;
} else {
return <Nav.item icon='fas fa-save' className='saveButton' onClick={this.save}>
return <Nav.item icon='fas fa-save' className='save' onClick={this.save}>
save
</Nav.item>;
}

View File

@@ -1,10 +1,82 @@
.newPage{
.saveButton{
.navItem.save{
background-color: @orange;
&:hover{
background-color: @green;
}
&.error{
position : relative;
background-color : @red;
}
}
}
.errorContainer{
animation-name: glideDown;
animation-duration: 0.4s;
position : absolute;
top : 100%;
left : 50%;
z-index : 100000;
width : 140px;
padding : 3px;
color : white;
background-color : #333;
border : 3px solid #444;
border-radius : 5px;
transform : translate(-50% + 3px, 10px);
text-align : center;
font-size : 10px;
font-weight : 800;
text-transform : uppercase;
a{
color : @teal;
}
&:before {
content: "";
width: 0px;
height: 0px;
position: absolute;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid transparent;
border-bottom: 10px solid #444;
left: 53px;
top: -23px;
}
&:after {
content: "";
width: 0px;
height: 0px;
position: absolute;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid transparent;
border-bottom: 10px solid #333;
left: 53px;
top: -19px;
}
.deny {
width : 48%;
margin : 1px;
padding : 5px;
background-color : #333;
display : inline-block;
border-left : 1px solid #666;
.animate(background-color);
&:hover{
background-color : red;
}
}
.confirm {
width : 48%;
margin : 1px;
padding : 5px;
background-color : #333;
display : inline-block;
color : white;
.animate(background-color);
&:hover{
background-color : teal;
}
}
}
}

View File

@@ -37,20 +37,21 @@ const PrintPage = createClass({
renderPages : function(){
if(this.props.brew.renderer == 'legacy') {
return _.map(this.state.brewText.split('\\page'), (page, index)=>{
return _.map(this.state.brewText.split('\\page'), (pageText, index)=>{
return <div
className='phb page'
id={`p${index + 1}`}
dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(page) }}
dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }}
key={index} />;
});
} else {
return _.map(this.state.brewText.split(/^\\page/gm), (page, index)=>{
return <div
className='phb3 page'
id={`p${index + 1}`}
dangerouslySetInnerHTML={{ __html: Markdown.render(page) }}
key={index} />;
return _.map(this.state.brewText.split(/^\\page$/gm), (pageText, index)=>{
pageText += `\n\\column\n&nbsp;`; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
return (
<div className='page' id={`p${index + 1}`} key={index} >
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} />
</div>
);
});
}

View File

@@ -48,7 +48,7 @@ const BrewItem = createClass({
if(!this.props.brew.editId) return;
return <a onClick={this.deleteBrew}>
<i className='fas fa-trash-alt' />
<i className='fas fa-trash-alt' title='Delete' />
</a>;
},
@@ -61,7 +61,7 @@ const BrewItem = createClass({
}
return <a href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'>
<i className='fas fa-pencil-alt' />
<i className='fas fa-pencil-alt' title='Edit' />
</a>;
},
@@ -74,7 +74,7 @@ const BrewItem = createClass({
}
return <a href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
<i className='fas fa-share-alt' />
<i className='fas fa-share-alt' title='Share' />
</a>;
},
@@ -87,7 +87,7 @@ const BrewItem = createClass({
}
return <a href={`/download/${shareLink}`}>
<i className='fas fa-download' />
<i className='fas fa-download' title='Download' />
</a>;
},
@@ -99,31 +99,33 @@ const BrewItem = createClass({
</span>;
},
getTooltipData : function(){
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
let outputString = `Created: ${this.props.brew.createdAt ? moment(this.props.brew.createdAt).local().format(dateFormatString) : 'UNKNOWN'}\n`;
outputString += `Last updated: ${this.props.brew.updatedAt ? moment(this.props.brew.updatedAt).local().format(dateFormatString) : 'UNKNOWN'}`;
return outputString;
},
render : function(){
const brew = this.props.brew;
return <div className='brewItem' title={this.getTooltipData()}>
<h2>{brew.title}</h2>
<p className='description'>{brew.description}</p>
<hr />
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
return <div className='brewItem'>
<div className='text'>
<h2>{brew.title}</h2>
<p className='description'>{brew.description}</p>
</div>
<hr />
<div className='info'>
<span>
<i className='fas fa-user' /> {brew.authors.join(', ')}
</span>
<span>
<i className='fas fa-eye' /> {brew.views}
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
<i className='fas fa-eye'/> {brew.views}
</span>
{brew.pageCount &&
<span title={`Page count: ${brew.pageCount}`}>
<i className='far fa-file' /> {brew.pageCount}
</span>
}
<span>
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
</span>
{this.renderGoogleDriveIcon()}
<br />
<span title={`Authors:\n${brew.authors.join('\n')}`}>
<i className='fas fa-user'/> {brew.authors.join(', ')}
</span>
</div>
<div className='links'>

View File

@@ -10,24 +10,28 @@
min-height : 105px;
margin-right : 15px;
margin-bottom : 15px;
padding : 5px 15px 5px 8px;
padding : 5px 15px 2px 8px;
padding-right : 15px;
border : 1px solid #c9ad6a;
border-radius : 5px;
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
h4{
margin-bottom : 5px;
font-size : 2.2em;
.text {
min-height : 54px;
h4{
margin-bottom : 5px;
font-size : 2.2em;
}
}
.info{
position: absolute;
bottom: 0px;
position: initial;
bottom: 2px;
margin-bottom: 4px;
font-family : ScalySans;
font-size : 1.2em;
&>span{
display : float;
margin-right : 12px;
}
}

View File

@@ -31,8 +31,9 @@ const UserPage = createClass({
},
getInitialState : function() {
return {
sortType : 'alpha',
sortDir : 'asc'
sortType : 'alpha',
sortDir : 'asc',
filterString : ''
};
},
getUsernameWithS : function() {
@@ -44,7 +45,7 @@ const UserPage = createClass({
renderBrews : function(brews){
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
const sortedBrews = this.sortBrews(brews, this.state.sortType);
const sortedBrews = this.sortBrews(brews);
return _.map(sortedBrews, (brew, idx)=>{
return <BrewItem brew={brew} key={idx}/>;
@@ -52,6 +53,7 @@ const UserPage = createClass({
},
sortBrewOrder : function(brew){
if(!brew.title){brew.title = 'No Title';}
const mapping = {
'alpha' : _.deburr(brew.title.toLowerCase()),
'created' : moment(brew.createdAt).format(),
@@ -90,6 +92,26 @@ const UserPage = createClass({
</td>;
},
handleFilterTextChange : function(e){
this.setState({
filterString : e.target.value
});
return;
},
renderFilterOption : function(){
return <td>
<label>
<i className='fas fa-search'></i>
<input
type='search'
placeholder='search title/description'
onChange={this.handleFilterTextChange}
/>
</label>
</td>;
},
renderSortOptions : function(){
return <div className='sort-container'>
<table>
@@ -114,6 +136,7 @@ const UserPage = createClass({
{`${(this.state.sortDir == 'asc' ? '\u25B2 ASC' : '\u25BC DESC')}`}
</button>
</td>
{this.renderFilterOption()}
</tr>
</tbody>
</table>
@@ -121,7 +144,12 @@ const UserPage = createClass({
},
getSortedBrews : function(){
return _.groupBy(this.props.brews, (brew)=>{
const testString = _.deburr(this.state.filterString).toLowerCase();
const brewCollection = this.state.filterString ? _.filter(this.props.brews, (brew)=>{
return (_.deburr(brew.title).toLowerCase().includes(testString)) ||
(_.deburr(brew.description).toLowerCase().includes(testString));
}) : this.props.brews;
return _.groupBy(brewCollection, (brew)=>{
return (brew.published ? 'published' : 'private');
});
},

View File

@@ -34,8 +34,9 @@
font-family : 'Open Sans', sans-serif;
position : fixed;
top : 35px;
left : calc(50vw - 408px);
border : 2px solid #58180D;
width : 675px;
width : 800px;
background-color : #EEE5CE;
padding : 2px;
text-align : center;
@@ -52,6 +53,9 @@
vertical-align : middle;
tbody tr{
background-color: transparent !important;
i{
padding-right : 5px
}
button{
background-color : transparent;
color : #58180D;

340
package-lock.json generated
View File

@@ -1,11 +1,11 @@
{
"name": "homebrewery",
"version": "2.13.3",
"version": "2.13.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"version": "2.13.3",
"version": "2.13.4",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -15,7 +15,7 @@
"@babel/preset-react": "^7.14.5",
"body-parser": "^1.19.0",
"classnames": "^2.3.1",
"codemirror": "^5.62.2",
"codemirror": "^5.62.3",
"cookie-parser": "^1.4.5",
"create-react-class": "^15.7.0",
"dedent-tabs": "^0.9.0",
@@ -23,29 +23,29 @@
"express-async-handler": "^1.1.4",
"express-static-gzip": "2.1.1",
"fs-extra": "10.0.0",
"googleapis": "84.0.0",
"googleapis": "85.0.0",
"jwt-simple": "^0.5.6",
"less": "^3.13.1",
"lodash": "^4.17.21",
"marked": "2.1.3",
"marked": "3.0.2",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.29.1",
"mongoose": "^5.13.6",
"nanoid": "3.1.23",
"mongoose": "^5.13.7",
"nanoid": "3.1.25",
"nconf": "^0.11.3",
"prop-types": "15.7.2",
"query-string": "7.0.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-frame-component": "4.1.3",
"react-router-dom": "5.2.0",
"react-router-dom": "5.2.1",
"sanitize-filename": "1.6.3",
"superagent": "^6.1.0",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
},
"devDependencies": {
"eslint": "^7.32.0",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react": "^7.25.1",
"pico-check": "^2.1.3"
},
"engines": {
@@ -1665,11 +1665,14 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.6.tgz",
"integrity": "sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==",
"version": "7.15.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.3.tgz",
"integrity": "sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/runtime/node_modules/regenerator-runtime": {
@@ -3126,9 +3129,9 @@
}
},
"node_modules/codemirror": {
"version": "5.62.2",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.2.tgz",
"integrity": "sha512-tVFMUa4J3Q8JUd1KL9yQzQB0/BJt7ZYZujZmTPgo/54Lpuq3ez4C8x/ATUY/wv7b7X3AUq8o3Xd+2C5ZrCGWHw=="
"version": "5.62.3",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.3.tgz",
"integrity": "sha512-zZAyOfN8TU67ngqrxhOgtkSAGV9jSpN1snbl8elPtnh9Z5A11daR405+dhLzLnuXrwX0WCShWlybxPN3QC/9Pg=="
},
"node_modules/collection-visit": {
"version": "1.0.0",
@@ -3856,14 +3859,15 @@
}
},
"node_modules/eslint-plugin-react": {
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.24.0.tgz",
"integrity": "sha512-KJJIx2SYx7PBx3ONe/mEeMz4YE0Lcr7feJTCMyyKb/341NcjuAgim3Acgan89GfPv7nxXK2+0slu0CWXYM4x+Q==",
"version": "7.25.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.25.1.tgz",
"integrity": "sha512-P4j9K1dHoFXxDNP05AtixcJEvIT6ht8FhYKsrkY0MPCPaUMYijhpWwNiRDZVtA8KFuZOkGSeft6QwH8KuVpJug==",
"dev": true,
"dependencies": {
"array-includes": "^3.1.3",
"array.prototype.flatmap": "^1.2.4",
"doctrine": "^2.1.0",
"estraverse": "^5.2.0",
"has": "^1.0.3",
"jsx-ast-utils": "^2.4.1 || ^3.0.0",
"minimatch": "^3.0.4",
@@ -3893,6 +3897,15 @@
"node": ">=0.10.0"
}
},
"node_modules/eslint-plugin-react/node_modules/estraverse": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
"integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
"dev": true,
"engines": {
"node": ">=4.0"
}
},
"node_modules/eslint-plugin-react/node_modules/resolve": {
"version": "2.0.0-next.3",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz",
@@ -4781,9 +4794,9 @@
}
},
"node_modules/googleapis": {
"version": "84.0.0",
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-84.0.0.tgz",
"integrity": "sha512-5WWLwmraulw3p55lu0gNpLz2FME1gcuR7QxgmUdAVHMiVN4LEasYjJV9p36gxcf2TMe6bn6+PgQ/63+CvBEgoQ==",
"version": "85.0.0",
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-85.0.0.tgz",
"integrity": "sha512-9zFsCbxz/642PROcYJsg/CCm89U1qe15c0Wtv7bmZ8cWYLD1Jszc5z+xTNoXZxnomLbvQaHeKBCPh7RdAccYOA==",
"dependencies": {
"google-auth-library": "^7.0.2",
"googleapis-common": "^5.0.2"
@@ -5961,14 +5974,14 @@
}
},
"node_modules/marked": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz",
"integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-3.0.2.tgz",
"integrity": "sha512-TMJQQ79Z0e3rJYazY0tIoMsFzteUGw9fB3FD+gzuIT3zLuG9L9ckIvUfF51apdJkcqc208jJN2KbtPbOvXtbjA==",
"bin": {
"marked": "bin/marked"
},
"engines": {
"node": ">= 10"
"node": ">= 12"
}
},
"node_modules/markedLegacy": {
@@ -6185,15 +6198,6 @@
"node": ">=4"
}
},
"node_modules/mini-create-react-context": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz",
"integrity": "sha512-b0TytUgFSbgFJGzJqXPKCFCBWigAjpjo+Fl7Vf7ZbKRDptszpppKxXH6DRXEABZ/gcEQczeb0iZ7JvL8e8jjCA==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"tiny-warning": "^1.0.3"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@@ -6321,9 +6325,9 @@
}
},
"node_modules/mongoose": {
"version": "5.13.6",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.13.6.tgz",
"integrity": "sha512-IyswXkgxnnl+rpiU+lzXl5/BOEle2llDfuPBrN6K+Eb5vS6a/HN/A9zrdtOcSTb0tVoCZ0QN5PfDSwa/EEGBuQ==",
"version": "5.13.7",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.13.7.tgz",
"integrity": "sha512-ADIvftZ+KfoTALMZ0n8HvBlezFhcUd73hQaHQDwQ+3X+JZlqE47fUy9yhFZ2SjT+qzmuaCcIXCfhewIc38t2fQ==",
"dependencies": {
"@types/mongodb": "^3.5.27",
"bson": "^1.1.4",
@@ -6405,9 +6409,9 @@
"optional": true
},
"node_modules/nanoid": {
"version": "3.1.23",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
"integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==",
"version": "3.1.25",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz",
"integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -7287,12 +7291,42 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
"integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q=="
},
"node_modules/react-router": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
"node_modules/react-router-dom": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.1.tgz",
"integrity": "sha512-xhFFkBGVcIVPbWM2KEYzED+nuHQPmulVa7sqIs3ESxzYd1pYg8N8rxPnQ4T2o1zu/2QeDUWcaqST131SO1LR3w==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.1",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"peerDependencies": {
"react": ">=15"
}
},
"node_modules/react-router-dom/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"node_modules/react-router-dom/node_modules/path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"dependencies": {
"isarray": "0.0.1"
}
},
"node_modules/react-router-dom/node_modules/react-router": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
"integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
@@ -7302,75 +7336,22 @@
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
}
},
"node_modules/react-router-dom": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz",
"integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
}
},
"node_modules/react-router-dom/node_modules/prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
}
},
"node_modules/react-router-dom/node_modules/prop-types/node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
"peerDependencies": {
"react": ">=15"
}
},
"node_modules/react-router/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"node_modules/react-router/node_modules/path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"node_modules/react-router-dom/node_modules/react-router/node_modules/mini-create-react-context": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
"dependencies": {
"isarray": "0.0.1"
}
},
"node_modules/react-router/node_modules/prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
}
},
"node_modules/react-router/node_modules/prop-types/node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
"@babel/runtime": "^7.12.1",
"tiny-warning": "^1.0.3"
},
"bin": {
"loose-envify": "cli.js"
"peerDependencies": {
"prop-types": "^15.0.0",
"react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
"node_modules/read-only-stream": {
@@ -10616,9 +10597,9 @@
}
},
"@babel/runtime": {
"version": "7.9.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.6.tgz",
"integrity": "sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==",
"version": "7.15.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.3.tgz",
"integrity": "sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA==",
"requires": {
"regenerator-runtime": "^0.13.4"
},
@@ -11828,9 +11809,9 @@
}
},
"codemirror": {
"version": "5.62.2",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.2.tgz",
"integrity": "sha512-tVFMUa4J3Q8JUd1KL9yQzQB0/BJt7ZYZujZmTPgo/54Lpuq3ez4C8x/ATUY/wv7b7X3AUq8o3Xd+2C5ZrCGWHw=="
"version": "5.62.3",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.3.tgz",
"integrity": "sha512-zZAyOfN8TU67ngqrxhOgtkSAGV9jSpN1snbl8elPtnh9Z5A11daR405+dhLzLnuXrwX0WCShWlybxPN3QC/9Pg=="
},
"collection-visit": {
"version": "1.0.0",
@@ -12517,14 +12498,15 @@
}
},
"eslint-plugin-react": {
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.24.0.tgz",
"integrity": "sha512-KJJIx2SYx7PBx3ONe/mEeMz4YE0Lcr7feJTCMyyKb/341NcjuAgim3Acgan89GfPv7nxXK2+0slu0CWXYM4x+Q==",
"version": "7.25.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.25.1.tgz",
"integrity": "sha512-P4j9K1dHoFXxDNP05AtixcJEvIT6ht8FhYKsrkY0MPCPaUMYijhpWwNiRDZVtA8KFuZOkGSeft6QwH8KuVpJug==",
"dev": true,
"requires": {
"array-includes": "^3.1.3",
"array.prototype.flatmap": "^1.2.4",
"doctrine": "^2.1.0",
"estraverse": "^5.2.0",
"has": "^1.0.3",
"jsx-ast-utils": "^2.4.1 || ^3.0.0",
"minimatch": "^3.0.4",
@@ -12545,6 +12527,12 @@
"esutils": "^2.0.2"
}
},
"estraverse": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
"integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
"dev": true
},
"resolve": {
"version": "2.0.0-next.3",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz",
@@ -13167,9 +13155,9 @@
}
},
"googleapis": {
"version": "84.0.0",
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-84.0.0.tgz",
"integrity": "sha512-5WWLwmraulw3p55lu0gNpLz2FME1gcuR7QxgmUdAVHMiVN4LEasYjJV9p36gxcf2TMe6bn6+PgQ/63+CvBEgoQ==",
"version": "85.0.0",
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-85.0.0.tgz",
"integrity": "sha512-9zFsCbxz/642PROcYJsg/CCm89U1qe15c0Wtv7bmZ8cWYLD1Jszc5z+xTNoXZxnomLbvQaHeKBCPh7RdAccYOA==",
"requires": {
"google-auth-library": "^7.0.2",
"googleapis-common": "^5.0.2"
@@ -14071,9 +14059,9 @@
}
},
"marked": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz",
"integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA=="
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-3.0.2.tgz",
"integrity": "sha512-TMJQQ79Z0e3rJYazY0tIoMsFzteUGw9fB3FD+gzuIT3zLuG9L9ckIvUfF51apdJkcqc208jJN2KbtPbOvXtbjA=="
},
"markedLegacy": {
"version": "npm:marked@0.3.19",
@@ -14247,15 +14235,6 @@
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="
},
"mini-create-react-context": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz",
"integrity": "sha512-b0TytUgFSbgFJGzJqXPKCFCBWigAjpjo+Fl7Vf7ZbKRDptszpppKxXH6DRXEABZ/gcEQczeb0iZ7JvL8e8jjCA==",
"requires": {
"@babel/runtime": "^7.5.5",
"tiny-warning": "^1.0.3"
}
},
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@@ -14345,9 +14324,9 @@
}
},
"mongoose": {
"version": "5.13.6",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.13.6.tgz",
"integrity": "sha512-IyswXkgxnnl+rpiU+lzXl5/BOEle2llDfuPBrN6K+Eb5vS6a/HN/A9zrdtOcSTb0tVoCZ0QN5PfDSwa/EEGBuQ==",
"version": "5.13.7",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.13.7.tgz",
"integrity": "sha512-ADIvftZ+KfoTALMZ0n8HvBlezFhcUd73hQaHQDwQ+3X+JZlqE47fUy9yhFZ2SjT+qzmuaCcIXCfhewIc38t2fQ==",
"requires": {
"@types/mongodb": "^3.5.27",
"bson": "^1.1.4",
@@ -14420,9 +14399,9 @@
"optional": true
},
"nanoid": {
"version": "3.1.23",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
"integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw=="
"version": "3.1.25",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz",
"integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q=="
},
"nanomatch": {
"version": "1.2.13",
@@ -15108,19 +15087,16 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
"integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q=="
},
"react-router": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
"react-router-dom": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.1.tgz",
"integrity": "sha512-xhFFkBGVcIVPbWM2KEYzED+nuHQPmulVa7sqIs3ESxzYd1pYg8N8rxPnQ4T2o1zu/2QeDUWcaqST131SO1LR3w==",
"requires": {
"@babel/runtime": "^7.1.2",
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"react-router": "5.2.1",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
@@ -15138,58 +15114,30 @@
"isarray": "0.0.1"
}
},
"prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"react-router": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
"integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"dependencies": {
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"mini-create-react-context": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
}
}
}
}
},
"react-router-dom": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz",
"integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"dependencies": {
"prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
},
"dependencies": {
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
"@babel/runtime": "^7.12.1",
"tiny-warning": "^1.0.3"
}
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown",
"version": "2.13.3",
"version": "2.13.4",
"engines": {
"node": "14.15.x"
},
@@ -46,7 +46,7 @@
"@babel/preset-react": "^7.14.5",
"body-parser": "^1.19.0",
"classnames": "^2.3.1",
"codemirror": "^5.62.2",
"codemirror": "^5.62.3",
"cookie-parser": "^1.4.5",
"create-react-class": "^15.7.0",
"dedent-tabs": "^0.9.0",
@@ -54,29 +54,29 @@
"express-async-handler": "^1.1.4",
"express-static-gzip": "2.1.1",
"fs-extra": "10.0.0",
"googleapis": "84.0.0",
"googleapis": "85.0.0",
"jwt-simple": "^0.5.6",
"less": "^3.13.1",
"lodash": "^4.17.21",
"marked": "2.1.3",
"marked": "3.0.2",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.29.1",
"mongoose": "^5.13.6",
"nanoid": "3.1.23",
"mongoose": "^5.13.7",
"nanoid": "3.1.25",
"nconf": "^0.11.3",
"prop-types": "15.7.2",
"query-string": "7.0.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-frame-component": "4.1.3",
"react-router-dom": "5.2.0",
"react-router-dom": "5.2.1",
"sanitize-filename": "1.6.3",
"superagent": "^6.1.0",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
},
"devDependencies": {
"eslint": "^7.32.0",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react": "^7.25.1",
"pico-check": "^2.1.3"
}
}

View File

@@ -32,11 +32,7 @@ const getBrewFromId = asyncHandler(async (id, accessType)=>{
if(accessType == 'raw') {
return brew;
}
if(brew.text.startsWith('```css')) {
const index = brew.text.indexOf('```\n\n');
brew.style = brew.text.slice(7, index - 1);
brew.text = brew.text.slice(index + 5);
}
splitTextAndStyle(brew);
return brew;
});
@@ -49,6 +45,15 @@ const sanitizeBrew = (brew, full=false)=>{
return brew;
};
const splitTextAndStyle = (brew)=>{
brew.text = brew.text.replaceAll('\r\n', '\n');
if(brew.text.startsWith('```css')) {
const index = brew.text.indexOf('```\n\n');
brew.style = brew.text.slice(7, index - 1);
brew.text = brew.text.slice(index + 5);
}
};
app.use('/', serveCompressedStaticAssets(`${__dirname}/build`));
process.chdir(__dirname);
@@ -94,9 +99,10 @@ app.use((req, res, next)=>{
app.use(homebrewApi);
app.use(require('./server/admin.api.js'));
const HomebrewModel = require('./server/homebrew.model.js').model;
const welcomeText = require('fs').readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
const changelogText = require('fs').readFileSync('./changelog.md', 'utf8');
const HomebrewModel = require('./server/homebrew.model.js').model;
const welcomeText = require('fs').readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
const welcomeTextV3 = require('fs').readFileSync('./client/homebrew/pages/homePage/welcome_msg_v3.md', 'utf8');
const changelogText = require('fs').readFileSync('./changelog.md', 'utf8');
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
@@ -114,11 +120,23 @@ app.get('/', async (req, res, next)=>{
return next();
});
//Home page v3
app.get('/v3_preview', async (req, res, next)=>{
const brew = {
text : welcomeTextV3,
renderer : 'V3'
};
splitTextAndStyle(brew);
req.brew = brew;
return next();
});
//Changelog page
app.get('/changelog', async (req, res, next)=>{
const brew = {
title : 'Changelog',
text : changelogText
title : 'Changelog',
text : changelogText,
renderer : 'V3'
};
req.brew = brew;
return next();

View File

@@ -17,7 +17,7 @@ GoogleActions = {
if(!account || !account.googleId){ // If not signed into Google
const err = new Error('Not Signed In');
err.status = 401;
throw err;
throw (err);
}
const oAuth2Client = new google.auth.OAuth2(
@@ -60,6 +60,7 @@ GoogleActions = {
.catch((err)=>{
console.log('Error searching Google Drive Folders');
console.error(err);
throw (err);
});
let folderId;
@@ -69,8 +70,9 @@ GoogleActions = {
resource : fileMetadata
})
.catch((err)=>{
console.log('Error creating google app folder');
console.log('Error creating Google Drive folder');
console.error(err);
throw (err);
});
folderId = obj.data.id;
@@ -99,7 +101,9 @@ GoogleActions = {
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
})
.catch((err)=>{
return console.error(`Error Listing Google Brews: ${err}`);
console.log(`Error Listing Google Brews`);
console.error(err);
throw (err);
//TODO: Should break out here, but continues on for some reason.
});
@@ -109,24 +113,23 @@ GoogleActions = {
const brews = obj.data.files.map((file)=>{
return {
text : '',
shareId : file.properties.shareId,
editId : file.properties.editId,
createdAt : file.createdTime,
updatedAt : file.modifiedTime,
gDrive : true,
googleId : file.id,
title : file.properties.title,
description : file.description,
text : '',
shareId : file.properties.shareId,
editId : file.properties.editId,
createdAt : file.createdTime,
updatedAt : file.modifiedTime,
gDrive : true,
googleId : file.id,
pageCount : file.properties.pageCount,
title : file.properties.title,
description : file.description,
views : file.properties.views,
tags : '',
published : file.properties.published ? file.properties.published == 'true' : false,
authors : [req.account.username], //TODO: properly save and load authors to google drive
systems : []
};
});
tags : '',
published : file.properties.published ? file.properties.published == 'true' : false,
authors : [req.account.username], //TODO: properly save and load authors to google drive
systems : []
};
});
return brews;
},
@@ -136,7 +139,7 @@ GoogleActions = {
const result = await drive.files.get({ fileId: id })
.catch((err)=>{
console.log('error checking file exists...');
console.log(err);
console.error(err);
return false;
});
@@ -151,23 +154,27 @@ GoogleActions = {
if(await GoogleActions.existsGoogleBrew(auth, brew.googleId) == true) {
await drive.files.update({
fileId : brew.googleId,
resource : { name : `${brew.title}.txt`,
description : `${brew.description}`,
properties : { title : brew.title,
published : brew.published,
lastViewed : brew.lastViewed,
views : brew.views,
version : brew.version,
renderer : brew.renderer,
tags : brew.tags,
systems : brew.systems.join() }
},
media : { mimeType : 'text/plain',
body : brew.text }
resource : {
name : `${brew.title}.txt`,
description : `${brew.description}`,
properties : {
title : brew.title,
published : brew.published,
version : brew.version,
renderer : brew.renderer,
tags : brew.tags,
systems : brew.systems.join()
}
},
media : {
mimeType : 'text/plain',
body : brew.text
}
})
.catch((err)=>{
console.log('Error saving to google');
console.error(err);
throw (err);
//return res.status(500).send('Error while saving');
});
}
@@ -190,11 +197,12 @@ GoogleActions = {
'description' : `${brew.description}`,
'parents' : [folderId],
'properties' : { //AppProperties is not accessible
'shareId' : nanoid(12),
'editId' : nanoid(12),
'title' : brew.title,
'views' : '0',
'renderer' : brew.renderer || 'legacy'
'shareId' : nanoid(12),
'editId' : nanoid(12),
'title' : brew.title,
'views' : '0',
'pageCount' : brew.pageCount,
'renderer' : brew.renderer || 'legacy'
}
};
@@ -203,8 +211,9 @@ GoogleActions = {
media : media
})
.catch((err)=>{
console.log('Error while creating new Google brew');
console.error(err);
return res.status(500).send('Error while creating google brew');
throw (err);
});
if(!obj) return;
@@ -228,6 +237,7 @@ GoogleActions = {
updatedAt : new Date(),
gDrive : true,
googleId : obj.data.id,
pageCount : fileMetadata.properties.pageCount,
title : brew.title,
description : brew.description,
@@ -299,6 +309,7 @@ GoogleActions = {
createdAt : obj.data.createdTime,
updatedAt : obj.data.modifiedTime,
lastViewed : obj.data.properties.lastViewed,
pageCount : obj.data.properties.pageCount,
views : parseInt(obj.data.properties.views) || 0, //brews with no view parameter will return undefined
version : parseInt(obj.data.properties.version) || 0,
renderer : obj.data.properties.renderer ? obj.data.properties.renderer : 'legacy',
@@ -359,8 +370,13 @@ GoogleActions = {
await drive.files.update({
fileId : brew.googleId,
resource : { properties : { views : brew.views + 1,
lastViewed : new Date() } }
resource : {
modifiedTime : brew.updatedAt,
properties : {
views : brew.views + 1,
lastViewed : new Date()
}
}
})
.catch((err)=>{
console.log('Error updating Google views');

View File

@@ -19,6 +19,15 @@ const getGoodBrewTitle = (text)=>{
.slice(0, MAX_TITLE_LENGTH);
};
const excludePropsFromUpdate = (brew)=>{
// Remove undesired properties
const propsToExclude = ['views', 'lastViewed'];
for (const prop of propsToExclude) {
delete brew[prop];
};
return brew;
};
const mergeBrewText = (text, style)=>{
if(typeof style !== 'undefined') {
text = `\`\`\`css\n` +
@@ -64,7 +73,8 @@ const newBrew = (req, res)=>{
const updateBrew = (req, res)=>{
HomebrewModel.get({ editId: req.params.id })
.then((brew)=>{
brew = _.merge(brew, req.body);
const updateBrew = excludePropsFromUpdate(req.body);
brew = _.merge(brew, updateBrew);
brew.text = mergeBrewText(brew.text, brew.style);
// Compress brew text to binary before saving
@@ -141,9 +151,12 @@ const newGoogleBrew = async (req, res, next)=>{
req.body = brew;
const newBrew = await GoogleActions.newGoogleBrew(oAuth2Client, brew);
return res.status(200).send(newBrew);
try {
const newBrew = await GoogleActions.newGoogleBrew(oAuth2Client, brew);
return res.status(200).send(newBrew);
} catch (err) {
return res.status(err.response.status).send(err);
}
};
const updateGoogleBrew = async (req, res, next)=>{
@@ -151,12 +164,15 @@ const updateGoogleBrew = async (req, res, next)=>{
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
const brew = req.body;
const brew = excludePropsFromUpdate(req.body);
brew.text = mergeBrewText(brew.text, brew.style);
const updatedBrew = await GoogleActions.updateGoogleBrew(oAuth2Client, brew);
return res.status(200).send(updatedBrew);
try {
const updatedBrew = await GoogleActions.updateGoogleBrew(oAuth2Client, brew);
return res.status(200).send(updatedBrew);
} catch (err) {
return res.status(err.response.status).send(err);
}
};
router.post('/api', newBrew);

View File

@@ -4,11 +4,12 @@ const _ = require('lodash');
const zlib = require('zlib');
const HomebrewSchema = mongoose.Schema({
shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
editId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
title : { type: String, default: '' },
text : { type: String, default: '' },
textBin : { type: Buffer },
shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
editId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
title : { type: String, default: '' },
text : { type: String, default: '' },
textBin : { type: Buffer },
pageCount : { type: Number, default: 1 },
description : { type: String, default: '' },
tags : { type: String, default: '' },

View File

@@ -47,12 +47,6 @@ const CodeEditor = createClass({
indentWithTabs : true,
tabSize : 2,
extraKeys : {
'Ctrl-B' : this.makeBold,
'Cmd-B' : this.makeBold,
'Ctrl-I' : this.makeItalic,
'Cmd-I' : this.makeItalic,
'Ctrl-M' : this.makeSpan,
'Cmd-M' : this.makeSpan,
'Ctrl-B' : this.makeBold,
'Cmd-B' : this.makeBold,
'Ctrl-I' : this.makeItalic,
@@ -60,7 +54,7 @@ const CodeEditor = createClass({
'Ctrl-M' : this.makeSpan,
'Cmd-M' : this.makeSpan,
'Ctrl-/' : this.makeComment,
'Cmd-/' : this.makeComment,
'Cmd-/' : this.makeComment
}
});

View File

@@ -19,7 +19,7 @@ renderer.paragraph = function(text){
let match;
if(text.startsWith('<div') || text.startsWith('</div'))
return `${text}`;
else if(match = text.match(/(^|^.*?\n)<span class="inline(.*?<\/span>)$/)) {
else if(match = text.match(/(^|^.*?\n)<span class="inline-block(.*?<\/span>)$/)) {
return `${match[1].trim() ? `<p>${match[1]}</p>` : ''}<span class="inline-block${match[2]}`;
} else
return `<p>${text}</p>\n`;
@@ -65,13 +65,13 @@ const mustacheSpans = {
raw : raw, // Text to consume from the source
text : text, // Additional custom properties
tags : tags,
tokens : this.inlineTokens(text) // inlineTokens to process **bold**, *italics*, etc.
tokens : this.lexer.inlineTokens(text) // inlineTokens to process **bold**, *italics*, etc.
};
}
}
},
renderer(token) {
return `<span class="inline${token.tags}>${this.parseInline(token.tokens)}</span>`; // parseInline to turn child tokens into HTML
return `<span class="inline-block${token.tags}>${this.parser.parseInline(token.tokens)}</span>`; // parseInline to turn child tokens into HTML
}
};
@@ -114,13 +114,13 @@ const mustacheDivs = {
raw : raw, // Text to consume from the source
text : text, // Additional custom properties
tags : tags,
tokens : this.inline(this.blockTokens(text))
tokens : this.lexer.blockTokens(text)
};
}
}
},
renderer(token) {
return `<div class="block${token.tags}>${this.parse(token.tokens)}</div>`; // parseInline to turn child tokens into HTML
return `<div class="block${token.tags}>${this.parser.parse(token.tokens)}</div>`; // parseInline to turn child tokens into HTML
}
};
@@ -149,7 +149,7 @@ const mustacheInjectInline = {
},
renderer(token) {
token.type = token.originalType;
const text = this.parseInline([token]);
const text = this.parser.parseInline([token]);
const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text);
if(openingTag) {
return `${openingTag[1]} class="${token.tags}${openingTag[2]}`;
@@ -182,7 +182,7 @@ const mustacheInjectBlock = {
},
renderer(token) {
token.type = token.originalType;
const text = this.parse([token]);
const text = this.parser.parse([token]);
const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text);
if(openingTag) {
return `${openingTag[1]} class="${token.tags}${openingTag[2]}`;
@@ -211,8 +211,8 @@ const definitionLists = {
const definitions = [];
while (match = regex.exec(src)) {
definitions.push({
dt : this.inlineTokens(match[1].trim()),
dd : this.inlineTokens(match[2].trim())
dt : this.lexer.inlineTokens(match[1].trim()),
dd : this.lexer.inlineTokens(match[2].trim())
});
endIndex = regex.lastIndex;
}
@@ -227,8 +227,8 @@ const definitionLists = {
renderer(token) {
return `<dl>
${token.definitions.reduce((html, def)=>{
return `${html}<dt>${this.parseInline(def.dt)}</dt>`
+ `<dd>${this.parseInline(def.dd)}</dd>\n`;
return `${html}<dt>${this.parser.parseInline(def.dt)}</dt>`
+ `<dd>${this.parser.parseInline(def.dd)}</dd>\n`;
}, '')}
</dl>`;
}
@@ -302,7 +302,7 @@ const spanTable = {
row = item.header[j];
for (k = 0; k < row.length; k++) {
row[k].tokens = [];
this.inlineTokens(row[k].text, row[k].tokens);
this.lexer.inlineTokens(row[k].text, row[k].tokens);
}
}
@@ -312,7 +312,7 @@ const spanTable = {
row = item.rows[j];
for (k = 0; k < row.length; k++) {
row[k].tokens = [];
this.inlineTokens(row[k].text, row[k].tokens);
this.lexer.inlineTokens(row[k].text, row[k].tokens);
}
}
return item;
@@ -329,7 +329,7 @@ const spanTable = {
output += `<tr>`;
for (j = 0; j < row.length; j++) {
cell = row[j];
text = this.parseInline(cell.tokens);
text = this.parser.parseInline(cell.tokens);
output += getTableCell(text, cell, 'th', token.align[col]);
col += cell.colspan;
}
@@ -344,7 +344,7 @@ const spanTable = {
output += `<tr>`;
for (j = 0; j < row.length; j++) {
cell = row[j];
text = this.parseInline(cell.tokens);
text = this.parser.parseInline(cell.tokens);
output += getTableCell(text, cell, 'td', token.align[col]);
col += cell.colspan;
}
@@ -369,45 +369,28 @@ const getTableCell = (text, cell, type, align)=>{
};
const splitCells = (tableRow, count, prevRow = [])=>{
// trim any excessive pipes at start of row
tableRow = tableRow.replace(/^\|+(?=\|)/, '')
.replace(/(\|+)/g, (match, p1, offset, str)=>{
let escaped = false,
curr = offset;
while (--curr >= 0 && str[curr] === '\\') escaped = !escaped;
if(escaped) {
// odd number of slashes means | is escaped
// so we leave it and the slashes alone
return p1;
} else {
// add space before unescaped | to distinguish it from an escaped pipe
return ` ${p1}`;
}
});
const cells = [...tableRow.matchAll(/(?:[^|\\]|\\.?)+(?:\|+|$)/g)].map((x)=>x[0]);
const cells = tableRow.split(/(?: \||(?<=\|)\|)(?=[^\|]|$)/g);
let i = 0;
// First/last cell in a row cannot be empty if it has no leading/trailing pipe
if(!cells[0].trim()) { cells.shift(); }
if(!cells[cells.length - 1].trim()) { cells.pop(); }
// Remove first/last cell in a row if whitespace only and no leading/trailing pipe
if(!cells[0]?.trim()) { cells.shift(); }
if(!cells[cells.length - 1]?.trim()) { cells.pop(); }
let numCols = 0;
let i, j, trimmedCell, prevCell, prevCols;
for (; i < cells.length; i++) {
const trimmedCell = cells[i].split(/ \|+$/)[0];
for (i = 0; i < cells.length; i++) {
trimmedCell = cells[i].split(/\|+$/)[0];
cells[i] = {
rowspan : 1,
colspan : Math.max(cells[i].length - trimmedCell.length, 1),
text : trimmedCell.trim().replace(/\\\|/g, '|')
// trim whitespace and display escaped pipes as normal character
// display escaped pipes as normal character
};
// Handle Rowspan
if(trimmedCell.slice(-1) == '^' && prevRow.length) {
// Find matching cell in previous row
let prevCols = 0;
let j, prevCell;
prevCols = 0;
for (j = 0; j < prevRow.length; j++) {
prevCell = prevRow[j];
if((prevCols == numCols) && (prevCell.colspan == cells[i].colspan)) {
@@ -544,7 +527,7 @@ const processStyleTags = (string)=>{
module.exports = {
marked : Markdown,
render : (rawBrewText)=>{
rawBrewText = rawBrewText.replace(/^\\column$/gm, `<div class='columnSplit'></div>`)
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
return Markdown(
sanatizeScriptTags(rawBrewText),

View File

@@ -2,12 +2,14 @@
@import (less) './themes/assets/assets.less';
//Colors
@background : #EEE5CE;
@noteGreen : #e0e5c1;
@headerUnderline : #c9ad6a;
@horizontalRule : #9c2b1b;
@headerText : #58180D;
@monsterStatBackground : #EEDBAB;
@background : #EEE5CE; // Light parchment
@noteGreen : #e0e5c1; // Pastel green
@headerUnderline : #c9ad6a; // Gold
@horizontalRule : #9c2b1b; // Maroon
@headerText : #58180D; // Dark maroon
@monsterStatBackground : #EEDBAB; // Light orange parchment
@captionText : #766649; // Brown
@watercolorStain : #BBAD82; // Light brown
@page { margin: 0; }
body {
counter-reset : phb-page-numbers;
@@ -33,9 +35,9 @@ body {
letter-spacing : -0.02em;
}
}
.useColumns(@multiplier : 1){
.useColumns(@multiplier : 1, @fillMode: balance){
column-count : 2;
column-fill : auto;
column-fill : @fillMode;
column-gap : 0.9cm;
column-width : 8cm * @multiplier;
-webkit-column-count : 2;
@@ -45,6 +47,11 @@ body {
-webkit-column-gap : 0.9cm;
-moz-column-gap : 0.9cm;
}
.columnWrapper{
max-height : 100%;
column-span : all;
columns : inherit;
}
.page{
.useColumns();
counter-increment : phb-page-numbers;
@@ -54,9 +61,9 @@ body {
overflow : hidden;
height : 279.4mm;
width : 215.9mm;
padding : 1.4cm 1.9cm 1.7cm;
background-color : @background;
background-image : @backgroundImage;
padding : 1.4cm 1.9cm 1.7cm;
font-family : BookInsanityRemake;
font-size : 0.34cm;
text-rendering : optimizeLegibility;
@@ -67,10 +74,13 @@ body {
// *****************************/
p{
overflow-wrap : break-word; //TODO: MAKE ALL MARGINS TOP-ONLY. USE * + * STYLE SELECTORS
margin-bottom : 0.8em;
display : block;
line-height : 1.3em;
&+* {
margin-top : 0.27cm;
}
&+p{
margin-top : -0.8em;
margin-top : 0;
}
}
ul{
@@ -120,50 +130,49 @@ body {
color : @headerText;
}
h1{
margin-bottom : 0.18cm;
margin-bottom : 0.18cm; //Margin-bottom only because this is WIDE
column-span : all;
font-size : 0.89cm;
-webkit-column-span : all;
-moz-column-span : all;
&+p::first-letter{
float : left;
font-family : SolberaImitationRemake;
line-height : 0.8em;
font-size: 3.5cm;
padding-left: 40px;
margin-left: -40px;
padding-top:10px;
margin-top:-8px;
padding-bottom:10px;
margin-bottom:-20px;
background-image: linear-gradient(-45deg, #322814, #998250, #322814);
background-clip: text;
-webkit-background-clip: text;
color: rgba(0, 0, 0, 0);
float : left;
font-family : SolberaImitationRemake;
line-height : 1em;
font-size : 3.5cm;
padding-left : 40px; //Allow background color to extend into margins
margin-left : -40px;
margin-top :-0.3cm;
padding-bottom :2px;
margin-bottom :-20px;
background-image : linear-gradient(-45deg, #322814, #998250, #322814);
background-clip : text;
-webkit-background-clip : text;
color : rgba(0, 0, 0, 0);
}
&+p::first-line{
font-variant : small-caps;
}
}
h2{
margin-top : 0px;
margin-bottom : 0.05cm;
//margin-top : 0px; //Font is misaligned. Shift up slightly
//margin-bottom : 0.05cm;
font-size : 0.75cm;
}
h3{
margin-top : -0.1cm;
margin-bottom : 0.1cm;
//margin-top : -0.1cm; //Font is misaligned. Shift up slightly
//margin-bottom : 0.1cm;
font-size : 0.575cm;
border-bottom : 2px solid @headerUnderline;
}
h4{
margin-top : -0.02cm;
margin-bottom : 0.02cm;
//margin-top : -0.02cm; //Font is misaligned. Shift up slightly
//margin-bottom : 0.02cm;
font-size : 0.458cm;
}
h5{
margin-top : -0.02cm;
margin-bottom : 0.02cm;
//margin-top : -0.02cm; //Font is misaligned. Shift up slightly
//margin-bottom : 0.02cm;
font-family : ScalySansSmallCapsRemake;
font-size : 0.423cm;
font-weight : 900;
@@ -174,7 +183,9 @@ body {
table{
.useSansSerif();
width : 100%;
margin-bottom : 1em;
& + * {
margin-top : 1em;
}
thead{
display: table-row-group;
font-weight : 800;
@@ -205,7 +216,6 @@ body {
border-width : 11px;
border-image : @noteBorderImage 12;
border-image-outset : 9px 0px;
box-shadow : 1px 4px 14px #888;
position : absolute;
width : 100%;
height : 100%;
@@ -218,6 +228,7 @@ body {
margin-left : -0.1em;
margin-right : -0.1em;
background-color : @noteGreen;
filter : drop-shadow(1px 4px 6px #888);
padding : 0.5em 0.6em;
& + * {
margin-top : 1.3em;
@@ -238,7 +249,7 @@ body {
// ************************************/
.descriptive{
.useSansSerif();
display : block-inline;
display : inline-block;
margin-top : 1.4em;
background-color : #faf7ea;
font-family : ScalySansRemake;
@@ -246,7 +257,7 @@ body {
border-width : 7px;
border-image : @descriptiveBoxImage 12 stretch;
border-image-outset : 4px;
box-shadow : 0px 0px 6px #faf7ea;
filter : drop-shadow(0 0 3px #faf7ea);
padding : 0.1em;
& + * {
margin-top : 1.4em;
@@ -263,6 +274,93 @@ body {
margin-bottom : 0em;
}
}
//*****************************
// * Images Snippets
// *****************************/
/* Arist Credit */
.artist {
position : absolute;
text-align : center;
font-family : WalterTurncoat;
font-size : 0.27cm;
color : @captionText;
p, p + p {
margin : unset;
text-indent : unset;
line-height : 1em;
}
h5 {
font-size : 1.3em;
font-family : WalterTurncoat;
}
a{
color : inherit;
text-decoration : unset;
&:hover {
text-decoration : underline;
}
}
}
/* Watermark */
.watermark {
display : grid !important;
place-items : center;
justify-content : center;
position : absolute;
top : 0;
left : 0;
width : 100%;
height : 100%;
font-size : 120px;
text-transform : uppercase;
color : black;
mix-blend-mode : overlay;
opacity : 30%;
transform : rotate(-45deg);
z-index : 500;
p {
margin-bottom : none;
}
}
/* Watercolor */
[class*="watercolor"] {
position : absolute;
width : 2000px; /* dimensions need to be real big so the user can set */
height : 2000px; /* height or width and the image will maintain aspect ratio */
-webkit-mask-image : var(--wc);
-webkit-mask-size : contain;
-webkit-mask-repeat : no-repeat;
mask-image : var(--wc);
mask-size : contain;
mask-repeat : no-repeat;
background-size : cover;
background-color : @watercolorStain; /*default color*/
--wc : @watercolor1; /*default image*/
z-index : -2;
}
.watercolor1 { --wc : @watercolor1; }
.watercolor2 { --wc : @watercolor2; }
.watercolor3 { --wc : @watercolor3; }
.watercolor4 { --wc : @watercolor4; }
.watercolor5 { --wc : @watercolor5; }
.watercolor6 { --wc : @watercolor6; }
.watercolor7 { --wc : @watercolor7; }
.watercolor8 { --wc : @watercolor8; }
.watercolor9 { --wc : @watercolor9; }
.watercolor10 { --wc : @watercolor10; }
.watercolor11 { --wc : @watercolor11; }
.watercolor12 { --wc : @watercolor12; }
img {
z-index: 2 !important;
filter : drop-shadow(0px 6px 6px rgba(0,0,0,.4));
position: absolute;
}
//*****************************
// * MONSTER STAT BLOCK
// *****************************/
@@ -277,7 +375,7 @@ body {
border-image-outset : 0px 2px;
background-blend-mode : overlay;
background-attachment : fixed;
box-shadow : 1px 4px 14px #888;
filter : drop-shadow(1px 4px 6px #888);
padding : 4px 2px;
margin : 0px -6px 1em;
}
@@ -331,16 +429,19 @@ body {
dl {
color : @headerText;
}
hr:last-of-type~dl{
color : inherit; // After the HRs, hanging indents remain black.
}
// Monster Ability table
hr + table:first-of-type{
margin : 0;
column-span : 1;
column-span : none;
color : @headerText;
background-color : transparent;
border-style : none;
border-image : none;
-webkit-column-span : 1;
-webkit-column-span : none;
tr {
background-color : transparent;
}
@@ -352,7 +453,7 @@ body {
//Full Width
.monster.wide{
.useColumns(0.96);
.useColumns(0.96, @fillMode: balance);
}
//*****************************
@@ -449,6 +550,9 @@ body {
break-after : always;
-moz-column-break-after : always;
break-before : column;
&+* {
margin-top: 0;
}
}
//Avoid breaking up
blockquote,table{
@@ -509,24 +613,57 @@ body {
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
display : block;
margin-bottom : 0.34cm;
&+* {
margin-top : 0;
}
}
//*****************************
// * CLASS TABLE
// *****************************/
.page .classTable{
margin-top : 25px;
margin-bottom : 40px;
border-collapse : separate;
background-color : white;
border : initial;
border-style : solid;
border-image-outset : 25px 17px;
border-image-repeat : stretch;
border-image-slice : 150 200 150 200;
border-image-source : @frameBorderImage;
border-image-width : 47px;
h5{
margin-bottom : 10px;
th[colspan]:not([rowspan]) {
white-space : nowrap;
}
&.frame {
margin-top : 0.66cm;
margin-bottom : 1.05cm;
margin-left : -0.1cm;
margin-right : -0.1cm;
border-collapse : separate;
background-color : white;
border : initial;
border-style : solid;
border-image-outset : 0.55cm 0.3cm;
border-image-repeat : stretch;
border-image-slice : 200;
border-image-source : @frameBorderImage;
border-image-width : 47px;
}
&.decoration {
transform-style : preserve-3d;
}
&.decoration::before {
content :'';
position : absolute;
background-image : @classTableDecoration;
background-size : contain;
background-repeat : space;
width : 7.75cm;
height : calc(100% + 3.3cm);
top : 50%;
left : 50%;
transform : translateY(-50%) translateX(-50%) translateZ(-1px);
filter : drop-shadow(0px 0px 1px #C8C5C080)
}
&.decoration.wide::before {
width : calc(100% + 3.3cm);
height : 7.75cm;
top : calc(50% + 0.4cm);
}
h5 + table{
margin-top : 0.2cm;
}
}
//*****************************
@@ -598,7 +735,7 @@ body {
}
}
&.wide{
.useColumns(0.96);
.useColumns(0.96, @fillMode: balance);
}
}
@@ -613,7 +750,6 @@ body {
.inline-block {
display : inline-block;
text-indent : initial;
line-height : 1.3em;
}
div {
column-gap : 0.5cm; //Default spacing if a div uses multicolumns
@@ -628,12 +764,18 @@ body {
line-height : 1.3em;
padding-left : 1em;
text-indent : -1em;
& + * {
margin-top : 0.28cm;
}
& + dl {
margin-top : 0;
}
}
dl + p {
margin-top: 0.5em;
dl + * {
margin-top : 0.17cm;
}
p + dl {
margin-top: -0.5em;
margin-top: 0.17cm;
}
dt {
display : inline;

View File

@@ -2,12 +2,13 @@
@import (less) './themes/assets/assets.less';
@import (less) './themes/phb.depricated.less';
//Colors
@background : #EEE5CE;
@noteGreen : #e0e5c1;
@headerUnderline : #c9ad6a;
@horizontalRule : #9c2b1b;
@headerText : #58180D;
@monsterStatBackground : #FDF1DC;
@background : #EEE5CE; // Light parchment
@noteGreen : #e0e5c1; // Pastel green
@headerUnderline : #c9ad6a; // Gold
@horizontalRule : #9c2b1b; // Maroon
@headerText : #58180D; // Dark maroon
@monsterStatBackground : #FDF1DC; // Lighter parchment
@captionText : #766649; // Brown
@page { margin: 0; }
body {
counter-reset : phb-page-numbers;
@@ -230,11 +231,11 @@ body {
// Monster Ability table
hr+table{
margin : 0;
column-span : 1;
column-span : none;
background-color : transparent;
border-style : none;
border-image : none;
-webkit-column-span : 1;
-webkit-column-span : none;
tbody{
tr:nth-child(odd), tr:nth-child(even){
background-color : transparent;
@@ -415,7 +416,7 @@ body {
// * DESCRIPTIVE TEXT BOX
// ************************************/
.phb .descriptive{
display : block-inline;
display : inline-block;
margin-bottom : 1em;
background-color : #faf7ea;
font-family : ScalySans;
@@ -445,6 +446,35 @@ body {
.phb pre+.descriptive{
margin-top : 8px;
}
//*****************************
// * ARTIST CREDIT BLOCK
// *****************************/
.phb {
.artist {
position : absolute;
text-align : center;
font-family : WalterTurncoat;
font-size : 0.27cm;
color : @captionText;
p, p + p {
margin : unset;
text-indent : unset;
line-height : 1em;
}
h5 {
font-size : 1.3em;
font-family : WalterTurncoat;
}
a{
color : inherit;
text-decoration : unset;
&:hover {
text-decoration : underline;
}
}
}
}
//*****************************
// * TABLE OF CONTENTS
// *****************************/

View File

@@ -1,3 +1,4 @@
// PHB
@footerAccentImage : data-uri('./themes/assets/footerAccent.png');
@frameBorderImage : data-uri('./themes/assets/frameBorder.png');
@backgroundImage : data-uri('./themes/assets/parchmentBackground.jpg');
@@ -8,3 +9,18 @@
@monsterBlockBackground : data-uri('./themes/assets/parchmentBackgroundGrayscale.jpg');
@monsterBorderImage : data-uri('./themes/assets/monsterBorderFancy.png');
@codeBorderImage : data-uri('./themes/assets/codeBorder.png');
@classTableDecoration : data-uri('./themes/assets/classTableDecoration.png');
// Watercolor Images
@watercolor1 : data-uri('./themes/assets/watercolor/watercolor1.png');
@watercolor2 : data-uri('./themes/assets/watercolor/watercolor2.png');
@watercolor3 : data-uri('./themes/assets/watercolor/watercolor3.png');
@watercolor4 : data-uri('./themes/assets/watercolor/watercolor4.png');
@watercolor5 : data-uri('./themes/assets/watercolor/watercolor5.png');
@watercolor6 : data-uri('./themes/assets/watercolor/watercolor6.png');
@watercolor7 : data-uri('./themes/assets/watercolor/watercolor7.png');
@watercolor8 : data-uri('./themes/assets/watercolor/watercolor8.png');
@watercolor9 : data-uri('./themes/assets/watercolor/watercolor9.png');
@watercolor10 : data-uri('./themes/assets/watercolor/watercolor10.png');
@watercolor11 : data-uri('./themes/assets/watercolor/watercolor11.png');
@watercolor12 : data-uri('./themes/assets/watercolor/watercolor12.png');

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

View File

@@ -37,6 +37,12 @@
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: WalterTurncoat;
src: url('../fonts/5e legacy/WalterTurncoat-Regular.woff2');
font-weight: normal;
font-style: normal;
}
/* Headers */
@font-face {

Binary file not shown.

View File

@@ -55,6 +55,12 @@
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: WalterTurncoat;
src: url('../fonts/5e/WalterTurncoat-Regular.woff2');
font-weight: normal;
font-style: normal;
}
/* Headers */
@font-face {