0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-29 06:52:41 +00:00

Merge branch 'master' into newTheme-UnearthedArcana

This commit is contained in:
G.Ambatte
2024-02-25 11:55:40 +13:00
committed by GitHub
47 changed files with 3266 additions and 1537 deletions

View File

@@ -75,11 +75,188 @@ pre {
.page {
padding-bottom: 1.5cm;
}
.varSyntaxTable th:first-of-type {
width:6cm;
}
```
## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
### Wednesday 21/2/2024 - v3.11.0
{{taskList
##### Gazook89
* [x] Brew view count no longer increases when viewed by owner
Fixes issue [#3037](https://github.com/naturalcrit/homebrewery/issues/3037)
* [x] Small tweak to PHB H3 sizing
Fixes issue [#2989](https://github.com/naturalcrit/homebrewery/issues/2989)
* [x] Add **Fold/Unfold All** {{fas,fa-compress-alt}} / {{fas,fa-expand-alt}} buttons to editor bar
Fixes issue [#2965](https://github.com/naturalcrit/homebrewery/issues/2965)
##### G-Ambatte
* [x] Share link added to Editor Access error page
Fixes issue [#3086](https://github.com/naturalcrit/homebrewery/issues/3086)
* [x] Add Darkbrewery theme to Editor theme selector {{fas,fa-palette}}
Fixes issue [#3034](https://github.com/naturalcrit/homebrewery/issues/3034)
* [x] Fix Firefox prints with alternating blank pages
Fixes issue [#3115](https://github.com/naturalcrit/homebrewery/issues/3115)
* [x] Admin page working again
Fixes issue [#2657](https://github.com/naturalcrit/homebrewery/issues/2657)
##### 5e-Cleric
* [x] Fix indenting issue with Monster Blocks and italics in Class Feature
Fixes issues [#527](https://github.com/naturalcrit/homebrewery/issues/527),
[#3247](https://github.com/naturalcrit/homebrewery/issues/3247)
* [x] Allow CSS vars in curly syntax to be formatted as strings using single quotes
`{{--customVar:"'a string'"}}`
Fixes issue [#3066](https://github.com/naturalcrit/homebrewery/issues/3066)
* [x] Add *Elderberry Inn* icons {{ei,action}} `{{ei,icon-name}}`
Fixes issue [#3171](https://github.com/naturalcrit/homebrewery/issues/3171)
* [x] New {{openSans **{{fas,fa-keyboard}} FONTS** }} snippets!
Fixes issue [#3171](https://github.com/naturalcrit/homebrewery/issues/3171)
* [x] New page now opens in a new tab
##### abquintic (new contributor!)
* [x] Add ^super^ `^abc^` and ^^sub^^ `^^abc^^` syntax.
Fixes issue [#2171](https://github.com/naturalcrit/homebrewery/issues/2171)
* [x] Add HTML tag assignment to curly syntax `{{tag=value}}`
Fixes issue [1488](https://github.com/naturalcrit/homebrewery/issues/1488)
* [x] {{openSans **Brew → Clone to New**}} now clones tags
Fixes issue [1488](https://github.com/naturalcrit/homebrewery/issues/1488)
##### calculuschild
* [x] Better error messages for "Out of Google Drive Storage" and "Not logged in to edit"
Fixes issues [2510](https://github.com/naturalcrit/homebrewery/issues/2510),
[2975](https://github.com/naturalcrit/homebrewery/issues/2975)
* [x] New Variables syntax. See below for details.
}}
{{wide
### Brew Variable Syntax
You may already be familiar with `[link](url)` and `![image](url)` syntax. We have expanded this to include a third `$[variable](text)` syntax. All three of these syntaxes now share a common set of features:
{{varSyntaxTable
| syntax | description |
|:-------|-------------|
| `[var]:content` | Assigns a variable (must start on a line by itself, and ends at the next blank line) |
| `[var](content)` | Assigns a variable and outputs it (can be inline) |
| `[var]` | Outputs the variable contents as a link, if formatted as a valid link |
| `![var]` | Outputs as an image, if formatted as a valid image |
| `$[var]` | Outputs as Markdown |
| `$[var1 + var2 - 2 * var3]` | Performs math operations and outputs result if all variables are valid numbers |
}}
}}
{{wide,margin-top:0,margin-bottom:0
### Examples
}}
{{wide,columns:2,margin-top:0,margin-bottom:0
```
[first]: Bob
[last]: Jones
My name is $[first] $[last].
```
\column
[first]: Bob
[last]: Jones
My name is $[first] $[last].
}}
{{wide,columns:2,margin-top:0,margin-bottom:0
```
[myTable]:
| h1 | h2 |
|----|----|
| c1 | c2 |
Here is my table:
$[myTable]
```
\column
[myTable]:
| h1 | h2 |
|----|----|
| c1 | c2 |
Here is my table:
$[myTable]
}}
{{wide,columns:2,margin-top:0,margin-bottom:0
```
There are $[TableNum] tables total.
#### Table $[TableNum](1): Horses
#### Table $[TableNum]($[TableNum + 1]): Cows
```
\column
There are $[TableNum] tables in this document. *(note: final value of `$[TableNum]` gets hoisted up if available)*
#### Table $[TableNum](1): Horses
#### Table $[TableNum]($[TableNum + 1]): Cows
}}
\page
### Friday 13/10/2023 - v3.10.0
{{taskList

View File

@@ -1,9 +1,8 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
require('./brewRenderer.less');
const React = require('react');
const createClass = require('create-react-class');
const { useState, useRef, useEffect } = React;
const _ = require('lodash');
const cx = require('classnames');
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
const Markdown = require('naturalcrit/markdown.js');
@@ -13,244 +12,226 @@ const ErrorBar = require('./errorBar/errorBar.jsx');
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
const NotificationPopup = require('./notificationPopup/notificationPopup.jsx');
const Frame = require('react-frame-component').default;
const dedent = require('dedent-tabs').default;
const Themes = require('themes/themes.json');
const PAGE_HEIGHT = 1056;
const PPR_THRESHOLD = 50;
const BrewRenderer = createClass({
displayName : 'BrewRenderer',
getDefaultProps : function() {
return {
text : '',
style : '',
renderer : 'legacy',
theme : '5ePHB',
lang : '',
errors : []
};
},
getInitialState : function() {
let pages;
if(this.props.renderer == 'legacy') {
pages = this.props.text.split('\\page');
} else {
pages = this.props.text.split(/^\\page$/gm);
}
const INITIAL_CONTENT = dedent`
<!DOCTYPE html><html><head>
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link href='/homebrew/bundle.css' rel='stylesheet' />
<base target=_blank>
</head><body style='overflow: hidden'><div></div></body></html>`;
return {
viewablePageNumber : 0,
height : 0,
isMounted : false,
//v=====----------------------< Brew Page Component >---------------------=====v//
const BrewPage = (props)=>{
props = {
contents : '',
index : 0,
...props
};
return <div className={props.className} id={`p${props.index + 1}`} >
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: props.contents }} />
</div>;
};
pages : pages,
usePPR : pages.length >= PPR_THRESHOLD,
visibility : 'hidden',
initialContent : `<!DOCTYPE html><html><head>
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link href='/homebrew/bundle.css' rel='stylesheet' />
<base target=_blank>
</head><body style='overflow: hidden'><div></div></body></html>`
};
},
height : 0,
lastRender : <div></div>,
componentWillUnmount : function() {
window.removeEventListener('resize', this.updateSize);
},
//v=====--------------------< Brew Renderer Component >-------------------=====v//
const renderedPages = [];
let rawPages = [];
componentDidUpdate : function(prevProps) {
if(prevProps.text !== this.props.text) {
let pages;
if(this.props.renderer == 'legacy') {
pages = this.props.text.split('\\page');
} else {
pages = this.props.text.split(/^\\page$/gm);
}
this.setState({
pages : pages,
usePPR : pages.length >= PPR_THRESHOLD
});
}
},
const BrewRenderer = (props)=>{
props = {
text : '',
style : '',
renderer : 'legacy',
theme : '5ePHB',
lang : '',
errors : [],
currentEditorPage : 0,
...props
};
updateSize : function() {
this.setState({
height : this.refs.main.parentNode.clientHeight,
});
},
const [state, setState] = useState({
viewablePageNumber : 0,
height : PAGE_HEIGHT,
isMounted : false,
visibility : 'hidden',
});
handleScroll : function(e){
const target = e.target;
this.setState((prevState)=>({
viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * prevState.pages.length)
const mainRef = useRef(null);
if(props.renderer == 'legacy') {
rawPages = props.text.split('\\page');
} else {
rawPages = props.text.split(/^\\page$/gm);
}
useEffect(()=>{ // Unmounting steps
return ()=>{window.removeEventListener('resize', updateSize);};
}, []);
const updateSize = ()=>{
setState((prevState)=>({
...prevState,
height : mainRef.current.parentNode.clientHeight,
}));
},
};
shouldRender : function(pageText, index){
if(!this.state.isMounted) return false;
const handleScroll = (e)=>{
const target = e.target;
setState((prevState)=>({
...prevState,
viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * rawPages.length)
}));
};
const viewIndex = this.state.viewablePageNumber;
if(index == viewIndex - 3) return true;
if(index == viewIndex - 2) return true;
if(index == viewIndex - 1) return true;
if(index == viewIndex) return true;
if(index == viewIndex + 1) return true;
if(index == viewIndex + 2) return true;
if(index == viewIndex + 3) return true;
const isInView = (index)=>{
if(!state.isMounted)
return false;
//Check for style tages
if(pageText.indexOf('<style>') !== -1) return true;
if(index == props.currentEditorPage) //Already rendered before this step
return false;
if(Math.abs(index - state.viewablePageNumber) <= 3)
return true;
return false;
},
};
sanitizeScriptTags : function(content) {
const sanitizeScriptTags = (content)=>{
return content
.replace(/<script/ig, '&lt;script')
.replace(/<\/script>/ig, '&lt;/script&gt;');
},
?.replace(/<script/ig, '&lt;script')
.replace(/<\/script>/ig, '&lt;/script&gt;')
|| '';
};
renderPageInfo : function(){
return <div className='pageInfo' ref='main'>
const renderPageInfo = ()=>{
return <div className='pageInfo' ref={mainRef}>
<div>
{this.props.renderer}
{props.renderer}
</div>
<div>
{this.state.viewablePageNumber + 1} / {this.state.pages.length}
{state.viewablePageNumber + 1} / {rawPages.length}
</div>
</div>;
},
};
renderPPRmsg : function(){
if(!this.state.usePPR) return;
return <div className='ppr_msg'>
Partial Page Renderer is enabled, because your brew is so large. May affect rendering.
</div>;
},
renderDummyPage : function(index){
const renderDummyPage = (index)=>{
return <div className='phb page' id={`p${index + 1}`} key={index}>
<i className='fas fa-spinner fa-spin' />
</div>;
},
};
renderStyle : function() {
if(!this.props.style) return;
const cleanStyle = this.sanitizeScriptTags(this.props.style);
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.sanitizeScriptTags(this.props.style)}\n} </style>` }} />;
const renderStyle = ()=>{
if(!props.style) return;
const cleanStyle = sanitizeScriptTags(props.style);
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${sanitizeScriptTags(props.style)}\n} </style>` }} />;
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${cleanStyle} </style>` }} />;
},
};
renderPage : function(pageText, index){
let cleanPageText = this.sanitizeScriptTags(pageText);
if(this.props.renderer == 'legacy')
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(cleanPageText) }} key={index} />;
else {
const renderPage = (pageText, index)=>{
let cleanPageText = sanitizeScriptTags(pageText);
if(props.renderer == 'legacy') {
const html = MarkdownLegacy.render(cleanPageText);
return <BrewPage className='page phb' index={index} key={index} contents={html} />;
} else {
cleanPageText += `\n\n&nbsp;\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(cleanPageText) }} />
</div>
);
const html = Markdown.render(cleanPageText, index);
return <BrewPage className='page' index={index} key={index} contents={html} />;
}
},
};
renderPages : function(){
if(this.state.usePPR){
return _.map(this.state.pages, (page, index)=>{
if(this.shouldRender(page, index) && typeof window !== 'undefined'){
return this.renderPage(page, index);
} else {
return this.renderDummyPage(index);
}
});
}
if(this.props.errors && this.props.errors.length) return this.lastRender;
this.lastRender = _.map(this.state.pages, (page, index)=>{
if(typeof window !== 'undefined') {
return this.renderPage(page, index);
} else {
return this.renderDummyPage(index);
const renderPages = ()=>{
if(props.errors && props.errors.length)
return renderedPages;
if(rawPages.length != renderedPages.length) // Re-render all pages when page count changes
renderedPages.length = 0;
// Render currently-edited page first so cross-page effects (variables, links) can propagate out first
renderedPages[props.currentEditorPage] = renderPage(rawPages[props.currentEditorPage], props.currentEditorPage);
_.forEach(rawPages, (page, index)=>{
if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){
renderedPages[index] = renderPage(page, index); // Render any page not yet rendered, but only re-render those in PPR range
}
});
return this.lastRender;
},
return renderedPages;
};
frameDidMount : function(){ //This triggers when iFrame finishes internal "componentDidMount"
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
this.updateSize();
window.addEventListener('resize', this.updateSize);
this.renderPages(); //Make sure page is renderable before showing
this.setState({
updateSize();
window.addEventListener('resize', updateSize);
renderPages(); //Make sure page is renderable before showing
setState((prevState)=>({
...prevState,
isMounted : true,
visibility : 'visible'
});
}));
}, 100);
},
};
emitClick : function(){
// console.log('iFrame clicked');
const emitClick = ()=>{ // Allow clicks inside iFrame to interact with dropdowns, etc. from outside
if(!window || !document) return;
document.dispatchEvent(new MouseEvent('click'));
},
};
render : function(){
//render in iFrame so broken code doesn't crash the site.
//Also render dummy page while iframe is mounting.
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
const themePath = this.props.theme ?? '5ePHB';
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
return (
<React.Fragment>
{!this.state.isMounted
? <div className='brewRenderer' onScroll={this.handleScroll}>
<div className='pages' ref='pages'>
{this.renderDummyPage(1)}
</div>
const rendererPath = props.renderer == 'V3' ? 'V3' : 'Legacy';
const themePath = props.theme ?? '5ePHB';
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
return (
<>
{/*render dummy page while iFrame is mounting.*/}
{!state.isMounted
? <div className='brewRenderer' onScroll={handleScroll}>
<div className='pages'>
{renderDummyPage(1)}
</div>
: null}
</div>
: null}
<Frame id='BrewRenderer' initialContent={this.state.initialContent}
style={{ width: '100%', height: '100%', visibility: this.state.visibility }}
contentDidMount={this.frameDidMount}
onClick={()=>{this.emitClick();}}
>
<div className={'brewRenderer'}
onScroll={this.handleScroll}
style={{ height: this.state.height }}>
{/*render in iFrame so broken code doesn't crash the site.*/}
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
style={{ width: '100%', height: '100%', visibility: state.visibility }}
contentDidMount={frameDidMount}
onClick={()=>{emitClick();}}
>
<div className={'brewRenderer'}
onScroll={handleScroll}
style={{ height: state.height }}>
<ErrorBar errors={this.props.errors} />
<div className='popups'>
<RenderWarnings />
<NotificationPopup />
</div>
<link href={`/themes/${rendererPath}/Blank/style.css`} rel='stylesheet'/>
{baseThemePath &&
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
}
<link href={`/themes/${rendererPath}/${themePath}/style.css`} rel='stylesheet'/>
{/* Apply CSS from Style tab and render pages from Markdown tab */}
{this.state.isMounted
&&
<>
{this.renderStyle()}
<div className='pages' ref='pages' lang={`${this.props.lang || 'en'}`}>
{this.renderPages()}
</div>
</>
}
<ErrorBar errors={props.errors} />
<div className='popups'>
<RenderWarnings />
<NotificationPopup />
</div>
</Frame>
{this.renderPageInfo()}
{this.renderPPRmsg()}
</React.Fragment>
);
}
});
<link href={`/themes/${rendererPath}/Blank/style.css`} rel='stylesheet'/>
{baseThemePath &&
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
}
<link href={`/themes/${rendererPath}/${themePath}/style.css`} rel='stylesheet'/>
{/* Apply CSS from Style tab and render pages from Markdown tab */}
{state.isMounted
&&
<>
{renderStyle()}
<div className='pages' lang={`${props.lang || 'en'}`}>
{renderPages()}
</div>
</>
}
</div>
</Frame>
{renderPageInfo()}
</>
);
};
module.exports = BrewRenderer;

View File

@@ -1,46 +1,44 @@
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
.brewRenderer{
.brewRenderer {
will-change : transform;
overflow-y : scroll;
.pages{
:where(.pages) {
margin : 30px 0px;
&>.page{
& > :where(.page) {
width : 215.9mm;
height : 279.4mm;
margin-right : auto;
margin-bottom : 30px;
margin-left : auto;
box-shadow : 1px 4px 14px #000;
box-shadow : 1px 4px 14px #000000;
}
}
}
.pane{
position : relative;
}
.pageInfo{
.pane { position : relative; }
.pageInfo {
position : absolute;
right : 17px;
bottom : 0;
z-index : 1000;
background-color : #333;
font-size : 10px;
font-weight : 800;
color : white;
background-color : #333333;
div {
display: inline-block;
display : inline-block;
padding : 8px 10px;
&:not(:last-child){
border-right: 1px solid #666;
}
&:not(:last-child) { border-right : 1px solid #666666; }
}
}
.ppr_msg{
.ppr_msg {
position : absolute;
left : 0px;
bottom : 0;
left : 0px;
z-index : 1000;
padding : 8px 10px;
background-color : #333;
font-size : 10px;
font-weight : 800;
color : white;
background-color : #333333;
}

View File

@@ -25,13 +25,10 @@ const NotificationPopup = createClass({
return (
<>
<li key='psa'>
<em>Broken default logo on <b>CoverPage</b> </em> <br />
If you have used the Cover Page snippet and notice the Naturalcrit
logo is showing as a broken image, this is due to some small tweaks
of this BETA feature. To fix the logo in your cover page, rename
the image link <b>"/assets/naturalCritLogoRed.svg"</b>. Remember
that any snippet marked "BETA" may have a similar change in the
future as we encounter any bugs or reworks.
<em>Don't store IMAGES in Google Drive</em><br />
Google Drive is not an image service, and will block images from being used
in brews if they get more views than expected. Google has confirmed they won't fix
this, so we recommend you look for another image hosting service such as imgur, ImgBB or Google Photos.
</li>
<li key='googleDriveFolder'>

View File

@@ -160,9 +160,27 @@ const Editor = createClass({
}
}
// Superscript
if(line.includes('\^')) {
const regex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/g;
let match;
while ((match = regex.exec(line)) != null) {
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) - 1 }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length + 1 }, { className: 'superscript' });
}
}
// Subscript
if(line.includes('^^')) {
const regex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/g;
let match;
while ((match = regex.exec(line)) != null) {
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) - 2 }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length + 2 }, { className: 'subscript' });
}
}
// Highlight injectors {style}
if(line.includes('{') && line.includes('}')){
const regex = /(?:^|[^{\n])({(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\2})/gm;
const regex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gm;
let match;
while ((match = regex.exec(line)) != null) {
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' });
@@ -170,7 +188,7 @@ const Editor = createClass({
}
// Highlight inline spans {{content}}
if(line.includes('{{') && line.includes('}}')){
const regex = /{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *|}}/g;
const regex = /{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *|}}/g;
let match;
let blockCount = 0;
while ((match = regex.exec(line)) != null) {
@@ -189,7 +207,7 @@ const Editor = createClass({
// Highlight block divs {{\n Content \n}}
let endCh = line.length+1;
const match = line.match(/^ *{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *$|^ *}}$/);
const match = line.match(/^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *$|^ *}}$/);
if(match)
endCh = match.index+match[0].length;
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
@@ -341,6 +359,14 @@ const Editor = createClass({
return this.refs.codeEditor?.undo();
},
foldCode : function(){
return this.refs.codeEditor?.foldAllCode();
},
unfoldCode : function(){
return this.refs.codeEditor?.unfoldAllCode();
},
render : function(){
return (
<div className='editor' ref='main'>
@@ -354,6 +380,8 @@ const Editor = createClass({
theme={this.props.brew.theme}
undo={this.undo}
redo={this.redo}
foldCode={this.foldCode}
unfoldCode={this.unfoldCode}
historySize={this.historySize()}
currentEditorTheme={this.state.editorTheme}
updateEditorTheme={this.updateEditorTheme}

View File

@@ -43,6 +43,18 @@
font-weight : bold;
color : green;
}
.superscript:not(.cm-comment) {
font-weight : bold;
color : goldenrod;
vertical-align : super;
font-size : 0.9em;
}
.subscript:not(.cm-comment) {
font-weight : bold;
color : rgb(123, 123, 15);
vertical-align : sub;
font-size : 0.9em;
}
}
.brewJump {

View File

@@ -2,7 +2,7 @@
.metadataEditor{
position : absolute;
z-index : 10000;
z-index : 5;
box-sizing : border-box;
width : 100%;
padding : 25px;

View File

@@ -37,6 +37,8 @@ const Snippetbar = createClass({
undo : ()=>{},
redo : ()=>{},
historySize : ()=>{},
foldCode : ()=>{},
unfoldCode : ()=>{},
updateEditorTheme : ()=>{},
cursorPos : {}
};
@@ -72,6 +74,7 @@ const Snippetbar = createClass({
}
},
mergeCustomizer : function(valueA, valueB, key) {
if(key == 'snippets') {
const result = _.reverse(_.unionBy(_.reverse(valueB), _.reverse(valueA), 'name')); // Join snippets together, with preference for the current theme over the base theme
@@ -100,10 +103,12 @@ const Snippetbar = createClass({
this.props.onInject(injectedText);
},
toggleThemeSelector : function(){
this.setState({
themeSelector : !this.state.themeSelector
});
toggleThemeSelector : function(e){
if(e.target.tagName != 'SELECT'){
this.setState({
themeSelector : !this.state.themeSelector
});
}
},
changeTheme : function(e){
@@ -117,7 +122,7 @@ const Snippetbar = createClass({
renderThemeSelector : function(){
return <div className='themeSelector'>
<select value={this.props.currentEditorTheme} onChange={this.changeTheme} onMouseDown={(this.changeTheme)}>
<select value={this.props.currentEditorTheme} onChange={this.changeTheme} >
{EditorThemes.map((theme, key)=>{
return <option key={key} value={theme}>{theme}</option>;
})}
@@ -144,6 +149,22 @@ const Snippetbar = createClass({
renderEditorButtons : function(){
if(!this.props.showEditButtons) return;
let foldButtons;
if(this.props.view == 'text'){
foldButtons =
<>
<div className={`editorTool foldAll ${this.props.foldCode ? 'active' : ''}`}
onClick={this.props.foldCode} >
<i className='fas fa-compress-alt' />
</div>
<div className={`editorTool unfoldAll ${this.props.unfoldCode ? 'active' : ''}`}
onClick={this.props.unfoldCode} >
<i className='fas fa-expand-alt' />
</div>
</>
}
return <div className='editors'>
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
onClick={this.props.undo} >
@@ -154,11 +175,13 @@ const Snippetbar = createClass({
<i className='fas fa-redo' />
</div>
<div className='divider'></div>
{foldButtons}
<div className={`editorTool editorTheme ${this.state.themeSelector ? 'active' : ''}`}
onClick={this.toggleThemeSelector} >
<i className='fas fa-palette' />
{this.state.themeSelector && this.renderThemeSelector()}
</div>
{this.state.themeSelector && this.renderThemeSelector()}
<div className='divider'></div>
<div className={cx('text', { selected: this.props.view === 'text' })}
onClick={()=>this.props.onViewChange('text')}>
@@ -209,7 +232,7 @@ const SnippetGroup = createClass({
return _.map(snippets, (snippet)=>{
return <div className='snippet' key={snippet.name} onClick={(e)=>this.handleSnippetClick(e, snippet)}>
<i className={snippet.icon} />
<span className='name'>{snippet.name}</span>
<span className='name'title={snippet.name}>{snippet.name}</span>
{snippet.experimental && <span className='beta'>beta</span>}
{snippet.subsnippets && <>
<i className='fas fa-caret-right'></i>

View File

@@ -1,162 +1,190 @@
@import (less) './client/icons/customIcons.less';
.snippetBar{
@import (less) '././././themes/fonts/5e/fonts.less';
.snippetBar {
@menuHeight : 25px;
position : relative;
height : @menuHeight;
background-color : #ddd;
.editors{
color : black;
background-color : #DDDDDD;
.editors {
position : absolute;
display : flex;
top : 0px;
right : 0px;
height : @menuHeight;
width : 125px;
display : flex;
justify-content : space-between;
&>div{
height : @menuHeight;
height : @menuHeight;
& > div {
width : @menuHeight;
cursor : pointer;
height : @menuHeight;
line-height : @menuHeight;
text-align : center;
&:hover,&.selected{
background-color : #999;
}
&.text{
cursor : pointer;
&:hover,&.selected { background-color : #999999; }
&.text {
.tooltipLeft('Brew Editor');
}
&.style{
&.style {
.tooltipLeft('Style Editor');
}
&.meta{
&.meta {
.tooltipLeft('Properties');
}
&.undo{
&.undo {
.tooltipLeft('Undo');
font-size : 0.75em;
color : grey;
&.active{
color : black;
}
&.active { color : inherit; }
}
&.redo{
&.redo {
.tooltipLeft('Redo');
font-size : 0.75em;
color : grey;
&.active{
color : black;
}
&.active { color : inherit; }
}
&.editorTheme{
&.foldAll {
.tooltipLeft('Fold All');
font-size : 0.75em;
color : inherit;
}
&.unfoldAll {
.tooltipLeft('Unfold All');
font-size : 0.75em;
color : inherit;
}
&.editorTheme {
.tooltipLeft('Editor Themes');
font-size : 0.75em;
color : black;
&.active{
color : white;
background-color: black;
&.active {
position : relative;
background-color : #999999;
}
}
&.divider {
background: linear-gradient(#000, #000) no-repeat center/1px 100%;
width: 5px;
&:hover{
background-color: inherit;
}
width : 5px;
background : linear-gradient(currentColor, currentColor) no-repeat center/1px 100%;
&:hover { background-color : inherit; }
}
}
.themeSelector{
position: absolute;
left: -65px;
top: 30px;
z-index: 999;
width: 170px;
background-color: black;
border-radius: 5px;
.themeSelector {
position : absolute;
top : 25px;
right : 0;
z-index : 10;
display : flex;
align-items : center;
justify-content : center;
width : 170px;
height : inherit;
background-color : inherit;
}
}
.snippetBarButton{
height : @menuHeight;
line-height : @menuHeight;
.snippetBarButton {
display : inline-block;
height : @menuHeight;
padding : 0px 5px;
font-weight : 800;
font-size : 0.625em;
font-weight : 800;
line-height : @menuHeight;
text-transform : uppercase;
cursor : pointer;
&:hover, &.selected{
background-color : #999;
}
i{
vertical-align : middle;
&:hover, &.selected { background-color : #999999; }
i {
margin-right : 3px;
font-size : 1.4em;
vertical-align : middle;
}
}
.toggleMeta{
position : absolute;
top : 0px;
right : 0px;
border-left : 1px solid black;
.tooltipLeft("Edit Brew Properties");
.toggleMeta {
position : absolute;
top : 0px;
right : 0px;
border-left : 1px solid black;
.tooltipLeft('Edit Brew Properties');
}
.snippetGroup{
border-right : 1px solid black;
&:hover{
&>.dropdown{
visibility : visible;
}
.snippetGroup {
border-right : 1px solid currentColor;
&:hover {
& > .dropdown { visibility : visible; }
}
.dropdown{
.dropdown {
position : absolute;
top : 100%;
visibility : hidden;
z-index : 1000;
margin-left : -5px;
padding : 0px;
background-color : #ddd;
.snippet{
position: relative;
.animate(background-color);
margin-left : -5px;
visibility : hidden;
background-color : #DDDDDD;
.snippet {
position : relative;
display : flex;
align-items : center;
min-width : max-content;
padding : 5px;
cursor : pointer;
font-size : 10px;
i{
cursor : pointer;
.animate(background-color);
i {
height : 1.2em;
margin-right : 8px;
font-size : 1.2em;
height : 1.2em;
&~i{
margin-right: 0;
margin-left: 5px;
& ~ i {
margin-right : 0;
margin-left : 5px;
}
/* Fonts */
&.font {
height : auto;
&::before {
font-size : 1.4em;
content : 'ABC';
}
&.OpenSans {font-family : 'OpenSans';}
&.CodeBold {font-family : 'CodeBold';}
&.CodeLight {font-family : 'CodeLight';}
&.ScalySansRemake {font-family : 'ScalySansRemake';}
&.BookInsanityRemake {font-family : 'BookInsanityRemake';}
&.MrEavesRemake {font-family : 'MrEavesRemake';}
&.SolberaImitationRemake {font-family : 'SolberaImitationRemake';}
&.ScalySansSmallCapsRemake {font-family : 'ScalySansSmallCapsRemake';}
&.WalterTurncoat {font-family : 'WalterTurncoat';}
&.Lato {font-family : 'Lato';}
&.Courier {font-family : 'Courier';}
&.NodestoCapsCondensed {font-family : 'NodestoCapsCondensed';}
&.Overpass {font-family : 'Overpass';}
&.Davek {font-family : 'Davek';}
&.Iokharic {font-family : 'Iokharic';}
&.Rellanic {font-family : 'Rellanic';}
&.TimesNewRoman {font-family : 'Times New Roman';}
}
}
.name {
margin-right : auto;
}
.name { margin-right : auto; }
.beta {
color : white;
padding : 4px 6px;
line-height : 1em;
margin-left : 5px;
align-self : center;
padding : 4px 6px;
margin-left : 5px;
font-family : monospace;
line-height : 1em;
color : white;
background : grey;
border-radius : 12px;
font-family : monospace;
}
&:hover{
background-color : #999;
&>.dropdown{
&:hover {
background-color : #999999;
& > .dropdown {
visibility : visible;
&.side {
left: 100%;
top: 0%;
margin-left:0;
box-shadow: -1px 1px 2px 0px #999;
top : 0%;
left : 100%;
margin-left : 0;
box-shadow : -1px 1px 2px 0px #999999;
}
}
}
}
}
}
}
}

View File

@@ -21,10 +21,11 @@ const ErrorNavItem = createClass({
this.props.parent.setState(state);
};
const error = this.props.error;
const response = error.response;
const status = response.status;
const message = response.body?.message;
const error = this.props.error;
const response = error.response;
const status = response.status;
const HBErrorCode = response.body?.HBErrorCode;
const message = response.body?.message;
let errMsg = '';
try {
errMsg += `${error.toString()}\n\n`;
@@ -40,7 +41,9 @@ const ErrorNavItem = createClass({
{message ?? 'Conflict: please refresh to get latest changes'}
</div>
</Nav.item>;
} else if(status === 412) {
}
if(status === 412) {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
@@ -48,6 +51,36 @@ const ErrorNavItem = createClass({
</div>
</Nav.item>;
}
if(HBErrorCode === '04') {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
You are no longer signed in as an author of
this brew! Were you signed out from a different
window? Visit our log in page, then try again!
<br></br>
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
<div className='confirm'>
Sign In
</div>
</a>
<div className='deny'>
Not Now
</div>
</div>
</Nav.item>;
}
if(response.body?.errors?.[0].reason == 'storageQuotaExceeded') {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
Can't save because your Google Drive seems to be full!
</div>
</Nav.item>;
}
if(response.req.url.match(/^\/api.*Google.*$/m)){
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
@@ -57,6 +90,7 @@ const ErrorNavItem = createClass({
expired! Visit our log in page to sign out
and sign back in with Google,
then try saving again!
<br></br>
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
<div className='confirm'>

View File

@@ -4,6 +4,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
module.exports = function(props){
return <Nav.item
href='/new'
newTab={true}
color='purple'
icon='fas fa-plus-square'>
new

View File

@@ -125,7 +125,7 @@ const BrewItem = createClass({
<div className='info'>
{brew.tags?.length ? <>
<div className='brewTags' title={`Tags:\n${brew.tags.join('\n')}`}>
<div className='brewTags' title={`${brew.tags.length} tags:\n${brew.tags.join('\n')}`}>
<i className='fas fa-tags'/>
{brew.tags.map((tag, idx)=>{
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
@@ -135,7 +135,11 @@ const BrewItem = createClass({
</> : <></>
}
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
<i className='fas fa-user'/> {brew.authors?.join(', ')}
<i className='fas fa-user'/> {brew.authors?.map((author, index)=>(
<>
<a key={index} href={`/user/${author}`}>{author}</a>
{index < brew.authors.length - 1 && ', '}
</>))}
</span>
<br />
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>

View File

@@ -48,6 +48,10 @@
&>span{
margin-right : 12px;
line-height : 1.5em;
a {
color:inherit;
}
}
}
.brewTags span {

View File

@@ -220,6 +220,7 @@ const ListPage = createClass({
render : function(){
return <div className='listPage sitePage'>
{/*<style>@layer V3_5ePHB, bundle;</style>*/}
<link href='/themes/V3/Blank/style.css' rel='stylesheet'/>
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet'/>
{this.props.navItems}
{this.renderSortOptions()}

View File

@@ -2,17 +2,18 @@
.noColumns(){
column-count : auto;
column-fill : auto;
column-gap : auto;
column-gap : normal;
column-width : auto;
-webkit-column-count : auto;
-moz-column-count : auto;
-webkit-column-width : auto;
-moz-column-width : auto;
-webkit-column-gap : auto;
-moz-column-gap : auto;
-webkit-column-gap : normal;
-moz-column-gap : normal;
height : auto;
min-height : 279.4mm;
margin : 20px auto;
contain : unset;
}
.listPage{
.content{

View File

@@ -50,7 +50,8 @@ const EditPage = createClass({
url : '',
autoSave : true,
autoSaveWarning : false,
unsavedTime : new Date()
unsavedTime : new Date(),
currentEditorPage : 0
};
},
savedBrew : null,
@@ -109,9 +110,10 @@ const EditPage = createClass({
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState((prevState)=>({
brew : { ...prevState.brew, text: text },
isPending : true,
htmlErrors : htmlErrors
brew : { ...prevState.brew, text: text },
isPending : true,
htmlErrors : htmlErrors,
currentEditorPage : this.refs.editor.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
}), ()=>{if(this.state.autoSave) this.trySave();});
},
@@ -405,6 +407,7 @@ const EditPage = createClass({
theme={this.state.brew.theme}
errors={this.state.htmlErrors}
lang={this.state.brew.lang}
currentEditorPage={this.state.currentEditorPage}
/>
</SplitPane>
</div>

View File

@@ -22,18 +22,18 @@ const errorIndex = (props)=>{
## We can't find this brew in Google Drive!
This file was saved on Google Drive, but this link doesn't work anymore.
${ props.brew.authors?.length > 0
? `Note that this brew belongs to the Homebrewery account **${ props.brew.authors[0] }**,
${ props.brew.account
? `which is
${props.brew.authors?.length > 0
? `Note that this brew belongs to the Homebrewery account **${props.brew.authors[0]}**,
${props.brew.account
? `which is
${props.brew.authors[0] == props.brew.account
? `your account.`
: `not your account (you are currently signed in as **${props.brew.account}**).`
}`
: 'and you are not currently signed in to any account.'
}`
: ''
}
? `your account.`
: `not your account (you are currently signed in as **${props.brew.account}**).`
}`
: 'and you are not currently signed in to any account.'
}`
: ''
}
The Homebrewery cannot delete files from Google Drive on its own, so there
are three most likely possibilities:
:
@@ -75,7 +75,9 @@ const errorIndex = (props)=>{
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
**Current Authors:** ${props.brew.authors?.map((author)=>{return `${author}`;}).join(', ') || 'Unable to list authors'}`,
**Current Authors:** ${props.brew.authors?.map((author)=>{return `${author}`;}).join(', ') || 'Unable to list authors'}
[Click here to be redirected to the brew's share page.](/share/${props.brew.shareId})`,
// User is not signed in; must be a user on the Authors List
'04' : dedent`

View File

@@ -31,9 +31,10 @@ const HomePage = createClass({
},
getInitialState : function() {
return {
brew : this.props.brew,
welcomeText : this.props.brew.text,
error : undefined
brew : this.props.brew,
welcomeText : this.props.brew.text,
error : undefined,
currentEditorPage : 0
};
},
handleSave : function(){
@@ -53,7 +54,8 @@ const HomePage = createClass({
},
handleTextChange : function(text){
this.setState((prevState)=>({
brew : { ...prevState.brew, text: text }
brew : { ...prevState.brew, text: text },
currentEditorPage : this.refs.editor.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
}));
},
renderNavbar : function(){
@@ -85,7 +87,12 @@ const HomePage = createClass({
renderer={this.state.brew.renderer}
showEditButtons={false}
/>
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer}/>
<BrewRenderer
text={this.state.brew.text}
style={this.state.brew.style}
renderer={this.state.brew.renderer}
currentEditorPage={this.state.currentEditorPage}
/>
</SplitPane>
</div>

View File

@@ -143,6 +143,7 @@ Much nicer than `<br><br><br><br><br>`
### 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.

View File

@@ -38,11 +38,12 @@ const NewPage = createClass({
const brew = this.props.brew;
return {
brew : brew,
isSaving : false,
saveGoogle : (global.account && global.account.googleId ? true : false),
error : null,
htmlErrors : Markdown.validate(brew.text)
brew : brew,
isSaving : false,
saveGoogle : (global.account && global.account.googleId ? true : false),
error : null,
htmlErrors : Markdown.validate(brew.text),
currentEditorPage : 0
};
},
@@ -104,8 +105,9 @@ const NewPage = createClass({
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState((prevState)=>({
brew : { ...prevState.brew, text: text },
htmlErrors : htmlErrors
brew : { ...prevState.brew, text: text },
htmlErrors : htmlErrors,
currentEditorPage : this.refs.editor.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
}));
localStorage.setItem(BREWKEY, text);
},
@@ -220,7 +222,15 @@ const NewPage = createClass({
onMetaChange={this.handleMetaChange}
renderer={this.state.brew.renderer}
/>
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} theme={this.state.brew.theme} lang={this.state.brew.lang} errors={this.state.htmlErrors}/>
<BrewRenderer
text={this.state.brew.text}
style={this.state.brew.style}
renderer={this.state.brew.renderer}
theme={this.state.brew.theme}
errors={this.state.htmlErrors}
lang={this.state.brew.lang}
currentEditorPage={this.state.currentEditorPage}
/>
</SplitPane>
</div>
</div>;

View File

@@ -21,7 +21,8 @@ const PrintPage = createClass({
brew : {
text : '',
style : '',
renderer : 'legacy'
renderer : 'legacy',
lang : ''
}
};
},
@@ -32,7 +33,8 @@ const PrintPage = createClass({
text : this.props.brew.text || '',
style : this.props.brew.style || undefined,
renderer : this.props.brew.renderer || 'legacy',
theme : this.props.brew.theme || '5ePHB'
theme : this.props.brew.theme || '5ePHB',
lang : this.props.brew.lang || 'en'
}
};
},
@@ -49,7 +51,8 @@ const PrintPage = createClass({
text : brewStorage,
style : styleStorage,
renderer : metaStorage?.renderer || 'legacy',
theme : metaStorage?.theme || '5ePHB'
theme : metaStorage?.theme || '5ePHB',
lang : metaStorage?.lang || 'en'
}
};
});
@@ -100,7 +103,7 @@ const PrintPage = createClass({
<link href={`/themes/${rendererPath}/${themePath}/style.css`} rel='stylesheet'/>
{/* Apply CSS from Style tab */}
{this.renderStyle()}
<div className='pages' ref='pages'>
<div className='pages' ref='pages' lang={this.state.brew.lang}>
{this.renderPages()}
</div>
</div>;

4
faq.md
View File

@@ -102,7 +102,7 @@ The best way to avoid this is to leave space at the end of a column equal to one
### Why do I need to manually create a new page? Why doesn't text flow between pages?
A Homebrewery document is at it's core an HTML & CSS document, and currently limited by the specs of those technologies. It is currently not possible to flow content from inside one box ("page") to the inside of another box. It seems likely that someday CSS will add this capability, and if/when that happens, Homebrewery will adopt it as soon as possible.
A Homebrewery document is at its core an HTML & CSS document, and currently limited by the specs of those technologies. It is currently not possible to flow content from inside one box ("page") to the inside of another box. It seems likely that someday CSS will add this capability, and if/when that happens, Homebrewery will adopt it as soon as possible.
### Where do I get images?
The Homebrewery does not provide images for use besides some page elements and example images for snippets. You will need to find your own images for use and be sure you are following the appropriate license requirements.
@@ -126,4 +126,4 @@ The Homebrewery defaults to creating US Letter page sizes. If you are printing
### Typing `#### Adhesion` in the text editor doesn't show the header at all in the completed page?
Your ad-blocking software is mistakenly assuming your text to be an ad. Whitelist homebrewery.naturalcrit.com in your ad-blocking software.
Your ad-blocking software is mistakenly assuming your text to be an ad. Whitelist homebrewery.naturalcrit.com in your ad-blocking software.

1275
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
{
"name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.10.0",
"version": "3.11.0",
"engines": {
"npm": "^10.2.x",
"node": ">=20.8.x"
"npm": "^10.2.x",
"node": "^20.8.x"
},
"repository": {
"type": "git",
@@ -26,6 +26,7 @@
"test:coverage": "jest --coverage --silent --runInBand",
"test:dev": "jest --verbose --watch",
"test:basic": "jest tests/markdown/basic.test.js --verbose",
"test:variables": "jest tests/markdown/variables.test.js --verbose",
"test:mustache-syntax": "jest '.*(mustache-syntax).*' --verbose --noStackTrace",
"test:mustache-syntax:inline": "jest '.*(mustache-syntax).*' -t '^Inline:.*' --verbose --noStackTrace",
"test:mustache-syntax:block": "jest '.*(mustache-syntax).*' -t '^Block:.*' --verbose --noStackTrace",
@@ -79,53 +80,54 @@
]
},
"dependencies": {
"@babel/core": "^7.23.2",
"@babel/plugin-transform-runtime": "^7.23.2",
"@babel/preset-env": "^7.23.2",
"@babel/preset-react": "^7.22.15",
"@googleapis/drive": "^8.4.0",
"@babel/core": "^7.23.9",
"@babel/plugin-transform-runtime": "^7.23.9",
"@babel/preset-env": "^7.23.9",
"@babel/preset-react": "^7.23.3",
"@googleapis/drive": "^8.7.0",
"body-parser": "^1.20.2",
"classnames": "^2.3.2",
"codemirror": "^5.65.6",
"cookie-parser": "^1.4.6",
"create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.3",
"expr-eval": "^2.0.2",
"express": "^4.18.2",
"express-async-handler": "^1.2.0",
"express-static-gzip": "2.1.7",
"fs-extra": "11.1.1",
"fs-extra": "11.2.0",
"js-yaml": "^4.1.0",
"jwt-simple": "^0.5.6",
"less": "^3.13.1",
"lodash": "^4.17.21",
"marked": "5.1.1",
"marked-extended-tables": "^1.0.7",
"marked-gfm-heading-id": "^3.1.0",
"marked-smartypants-lite": "^1.0.1",
"marked": "11.2.0",
"marked-extended-tables": "^1.0.8",
"marked-gfm-heading-id": "^3.1.3",
"marked-smartypants-lite": "^1.0.2",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.29.4",
"mongoose": "^7.6.1",
"moment": "^2.30.1",
"mongoose": "^8.2.0",
"nanoid": "3.3.4",
"nconf": "^0.12.0",
"nconf": "^0.12.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-frame-component": "^4.1.3",
"react-router-dom": "6.16.0",
"react-router-dom": "6.22.1",
"sanitize-filename": "1.6.3",
"superagent": "^8.1.2",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
},
"devDependencies": {
"eslint": "^8.51.0",
"eslint-plugin-jest": "^27.4.2",
"eslint": "^8.56.0",
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-react": "^7.33.2",
"jest": "^29.7.0",
"jest-expect-message": "^1.1.3",
"postcss-less": "^6.0.0",
"stylelint": "^15.10.3",
"stylelint-config-recess-order": "^4.3.0",
"stylelint": "^15.11.0",
"stylelint-config-recess-order": "^4.6.0",
"stylelint-config-recommended": "^13.0.0",
"stylelint-stylistic": "^0.4.3",
"supertest": "^6.3.3"
"supertest": "^6.3.4"
}
}

View File

@@ -101,7 +101,10 @@ fs.emptyDirSync('./build');
//v==---------------------------MOVE CM EDITOR THEMES -----------------------------==v//
editorThemeFiles = fs.readdirSync('./node_modules/codemirror/theme');
const editorThemesBuildDir = './build/homebrew/cm-themes';
await fs.copy('./node_modules/codemirror/theme', editorThemesBuildDir);
await fs.copy('./themes/codeMirror/customThemes', editorThemesBuildDir);
editorThemeFiles = fs.readdirSync(editorThemesBuildDir);
const editorThemeFile = './themes/codeMirror/editorThemes.json';
if(fs.existsSync(editorThemeFile)) fs.rmSync(editorThemeFile);
@@ -114,7 +117,7 @@ fs.emptyDirSync('./build');
stream.write('\n]\n');
stream.end();
await fs.copy('./node_modules/codemirror/theme', './build/homebrew/cm-themes');
await fs.copy('./themes/codeMirror', './build/homebrew/codeMirror');
//v==----------------------------- BUNDLE PACKAGES --------------------------------==v//
@@ -151,14 +154,14 @@ fs.emptyDirSync('./build');
// build(bundles);
//
})().catch(console.error);
//In development, set up LiveReload (refreshes browser), and Nodemon (restarts server)
if(isDev){
livereload('./build'); // Install the Chrome extension LiveReload to automatically refresh the browser
watchFile('./server.js', { // Restart server when change detected to this file or any nested directory from here
ignore : ['./build', './client', './themes'], // Ignore folders that are not running server code / avoids unneeded restarts
ext : 'js json' // Extensions to watch (only .js/.json by default)
//watch : ['./server', './themes'], // Watch additional folders if needed
});
}
//In development, set up LiveReload (refreshes browser), and Nodemon (restarts server)
if(isDev){
livereload('./build'); // Install the Chrome extension LiveReload to automatically refresh the browser
watchFile('./server.js', { // Restart server when change detected to this file or any nested directory from here
ignore : ['./build', './client', './themes'], // Ignore folders that are not running server code / avoids unneeded restarts
ext : 'js json' // Extensions to watch (only .js/.json by default)
//watch : ['./server', './themes'], // Watch additional folders if needed
});
}
})().catch(console.error);

View File

@@ -26,7 +26,6 @@
"codemirror/addon/edit/trailingspace.js",
"codemirror/addon/selection/active-line.js",
"moment",
"superagent",
"marked"
"superagent"
]
}

View File

@@ -26,85 +26,124 @@ const mw = {
}
};
/* Search for brews that are older than 3 days and that are shorter than a tweet */
const junkBrewQuery = HomebrewModel.find({
'$where' : 'this.text.length < 140',
createdAt : {
$lt : Moment().subtract(30, 'days').toDate()
}
}).limit(100).maxTime(60000);
const junkBrewPipeline = [
{ $match : {
updatedAt : { $lt: Moment().subtract(30, 'days').toDate() },
lastViewed : { $lt: Moment().subtract(30, 'days').toDate() }
}},
{ $project: { textBinSize: { $binarySize: '$textBin' } } },
{ $match: { textBinSize: { $lt: 140 } } },
{ $limit: 100 }
];
/* Search for brews that aren't compressed (missing the compressed text field) */
const uncompressedBrewQuery = HomebrewModel.find({
'text' : { '$exists': true }
}).lean().limit(10000).select('_id');
// Search for up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{
junkBrewQuery.exec((err, objs)=>{
if(err) return res.status(500).send(err);
return res.json({ count: objs.length });
});
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
.then((objs)=>res.json({ count: objs.length }))
.catch((error)=>{
console.error(error);
res.status(500).json({ error: 'Internal Server Error' });
});
});
/* Removes all empty brews that are older than 3 days and that are shorter than a tweet */
// Delete up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
junkBrewQuery.remove().exec((err, objs)=>{
if(err) return res.status(500).send(err);
return res.json({ count: objs.length });
});
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
.then((docs)=>{
const ids = docs.map((doc)=>doc._id);
return HomebrewModel.deleteMany({ _id: { $in: ids } });
}).then((result)=>{
res.json({ count: result.deletedCount });
}).catch((error)=>{
console.error(error);
res.status(500).json({ error: 'Internal Server Error' });
});
});
/* Searches for matching edit or share id, also attempts to partial match */
router.get('/admin/lookup/:id', mw.adminOnly, (req, res, next)=>{
HomebrewModel.findOne({ $or : [
{ editId: { '$regex': req.params.id, '$options': 'i' } },
{ shareId: { '$regex': req.params.id, '$options': 'i' } },
] }).exec((err, brew)=>{
return res.json(brew);
router.get('/admin/lookup/:id', mw.adminOnly, async (req, res, next)=>{
HomebrewModel.findOne({
$or : [
{ editId: { $regex: req.params.id, $options: 'i' } },
{ shareId: { $regex: req.params.id, $options: 'i' } },
]
}).exec()
.then((brew)=>{
if(!brew) // No document found
return res.status(404).json({ error: 'Document not found' });
else
return res.json(brew);
})
.catch((err)=>{
console.error(err);
return res.status(500).json({ error: 'Internal Server Error' });
});
});
/* Find 50 brews that aren't compressed yet */
router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
uncompressedBrewQuery.exec((err, objs)=>{
if(err) return res.status(500).send(err);
objs = objs.map((obj)=>{return obj._id;});
return res.json({ count: objs.length, ids: objs });
});
const query = uncompressedBrewQuery.clone();
query.exec()
.then((objs)=>{
const ids = objs.map((obj)=>obj._id);
res.json({ count: ids.length, ids });
})
.catch((err)=>{
console.error(err);
res.status(500).send(err.message || 'Internal Server Error');
});
});
/* Compresses the "text" field of a brew to binary */
router.put('/admin/compress/:id', (req, res)=>{
HomebrewModel.get({ _id: req.params.id })
HomebrewModel.findOne({ _id: req.params.id })
.then((brew)=>{
brew.textBin = zlib.deflateRawSync(brew.text); // Compress brew text to binary before saving
brew.text = undefined; // Delete the non-binary text field since it's not needed anymore
if(!brew)
return res.status(404).send('Brew not found');
brew.save((err, obj)=>{
if(err) throw err;
return res.status(200).send(obj);
});
if(brew.text) {
brew.textBin = brew.textBin || zlib.deflateRawSync(brew.text); //Don't overwrite textBin if exists
brew.text = undefined;
}
return brew.save();
})
.then((obj)=>res.status(200).send(obj))
.catch((err)=>{
console.log(err);
return res.status(500).send('Error while saving');
console.error(err);
res.status(500).send('Error while saving');
});
});
router.get('/admin/stats', mw.adminOnly, (req, res)=>{
HomebrewModel.count({}, (err, count)=>{
router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
try {
const totalBrewsCount = await HomebrewModel.countDocuments({});
const publishedBrewsCount = await HomebrewModel.countDocuments({ published: true });
return res.json({
totalBrews : count
totalBrews : totalBrewsCount,
totalPublishedBrews : publishedBrewsCount
});
});
} catch (error) {
console.error(error);
return res.status(500).json({ error: 'Internal Server Error' });
}
});
router.get('/admin', mw.adminOnly, (req, res)=>{
templateFn('admin', {
url : req.originalUrl
})
.then((page)=>res.send(page))
.catch((err)=>res.sendStatus(500));
.then((page)=>res.send(page))
.catch((err)=>res.sendStatus(500));
});
module.exports = router;

View File

@@ -304,7 +304,8 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
text : req.brew.text,
style : req.brew.style,
renderer : req.brew.renderer,
theme : req.brew.theme
theme : req.brew.theme,
tags : req.brew.tags
};
req.brew = _.defaults(brew, DEFAULT_BREW);
@@ -327,14 +328,17 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r
type : 'article'
};
if(req.params.id.length > 12 && !brew._id) {
const googleId = brew.googleId;
const shareId = brew.shareId;
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
.catch((err)=>{next(err);});
} else {
await HomebrewModel.increaseView({ shareId: brew.shareId });
}
// increase visitor view count, do not include visits by author(s)
if(!brew.authors.includes(req.account?.username)){
if(req.params.id.length > 12 && !brew._id) {
const googleId = brew.googleId;
const shareId = brew.shareId;
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
.catch((err)=>{next(err);});
} else {
await HomebrewModel.increaseView({ shareId: brew.shareId });
}
};
sanitizeBrew(req.brew, 'share');
splitTextStyleAndMetadata(req.brew);
return next();
@@ -469,9 +473,18 @@ const getPureError = (error)=>{
};
app.use(async (err, req, res, next)=>{
const status = err.status || err.code || 500;
err.originalUrl = req.originalUrl;
console.error(err);
if(err.originalUrl?.startsWith('/api/')) {
// console.log('API error');
res.status(err.status || err.response?.status || 500).send(err);
return;
}
// console.log('non-API error');
const status = err.status || err.code || 500;
req.ogMeta = { ...defaultMetaTags,
title : 'Error Page',
description : 'Something went wrong!'

View File

@@ -79,7 +79,7 @@ const api = {
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
const accessError = { name: 'Access Error', status: 401 };
if(req.account){
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title };
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId };
}
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title };
}

View File

@@ -7,7 +7,7 @@ const cx = require('classnames');
const closeTag = require('./close-tag');
let CodeMirror;
if(typeof navigator !== 'undefined'){
if(typeof window !== 'undefined'){
CodeMirror = require('codemirror');
//Language Modes
@@ -112,6 +112,10 @@ const CodeEditor = createClass({
'Shift-Tab' : this.dedent,
'Ctrl-B' : this.makeBold,
'Cmd-B' : this.makeBold,
'Shift-Ctrl-=' : this.makeSuper,
'Shift-Cmd-=' : this.makeSuper,
'Ctrl-=' : this.makeSub,
'Cmd-=' : this.makeSub,
'Ctrl-I' : this.makeItalic,
'Cmd-I' : this.makeItalic,
'Ctrl-U' : this.makeUnderline,
@@ -219,6 +223,25 @@ const CodeEditor = createClass({
}
},
makeSuper : function() {
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '^' && selection.slice(-1) === '^';
this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `^${selection}^`, 'around');
if(selection.length === 0){
const cursor = this.codeMirror.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
}
},
makeSub : function() {
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '^^' && selection.slice(-2) === '^^';
this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `^^${selection}^^`, 'around');
if(selection.length === 0){
const cursor = this.codeMirror.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
}
},
makeNbsp : function() {
this.codeMirror.replaceSelection('&nbsp;', 'end');
},

View File

@@ -4,7 +4,40 @@ const Marked = require('marked');
const MarkedExtendedTables = require('marked-extended-tables');
const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite');
const { gfmHeadingId: MarkedGFMHeadingId } = require('marked-gfm-heading-id');
const MathParser = require('expr-eval').Parser;
const renderer = new Marked.Renderer();
const tokenizer = new Marked.Tokenizer();
//Limit math features to simple items
const mathParser = new MathParser({
operators : {
// These default to true, but are included to be explicit
add : true,
subtract : true,
multiply : true,
divide : true,
power : true,
round : true,
floor : true,
ceil : true,
sin : false, cos : false, tan : false, asin : false, acos : false,
atan : false, sinh : false, cosh : false, tanh : false, asinh : false,
acosh : false, atanh : false, sqrt : false, cbrt : false, log : false,
log2 : false, ln : false, lg : false, log10 : false, expm1 : false,
log1p : false, abs : false, trunc : false, join : false, sum : false,
'-' : false, '+' : false, exp : false, not : false, length : false,
'!' : false, sign : false, random : false, fac : false, min : false,
max : false, hypot : false, pyt : false, pow : false, atan2 : false,
'if' : false, gamma : false, roundTo : false, map : false, fold : false,
filter : false, indexOf : false,
remainder : false, factorial : false,
comparison : false, concatenate : false,
logical : false, assignment : false,
array : false, fndef : false
}
});
//Processes the markdown within an HTML block if it's just a class-wrapper
renderer.html = function (html) {
@@ -28,13 +61,40 @@ renderer.paragraph = function(text){
return `<p>${text}</p>\n`;
};
//Fix local links in the Preview iFrame to link inside the frame
renderer.link = function (href, title, text) {
let self = false;
if(href[0] == '#') {
self = true;
}
href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
if(href === null) {
return text;
}
let out = `<a href="${escape(href)}"`;
if(title) {
out += ` title="${title}"`;
}
if(self) {
out += ' target="_self"';
}
out += `>${text}</a>`;
return out;
};
// Disable default reflink behavior, as it steps on our variables extension
tokenizer.def = function () {
return undefined;
};
const mustacheSpans = {
name : 'mustacheSpans',
level : 'inline', // Is this a block-level or inline-level tokenizer?
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) {
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
const inlineRegex = /{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *|}}/g;
const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g;
const match = completeSpan.exec(src);
if(match) {
//Find closing delimiter
@@ -45,7 +105,7 @@ const mustacheSpans = {
let delim;
while (delim = inlineRegex.exec(match[0])) {
if(!tags) {
tags = ` ${processStyleTags(delim[0].substring(2))}`;
tags = `${processStyleTags(delim[0].substring(2))}`;
endTags = delim[0].length;
}
if(delim[0].startsWith('{{')) {
@@ -84,7 +144,7 @@ const mustacheDivs = {
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) {
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
const blockRegex = /^ *{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *$|^ *}}$/gm;
const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm;
const match = completeBlock.exec(src);
if(match) {
//Find closing delimiter
@@ -95,8 +155,8 @@ const mustacheDivs = {
let delim;
while (delim = blockRegex.exec(match[0])?.[0].trim()) {
if(!tags) {
tags = ` ${processStyleTags(delim.substring(2))}`;
endTags = delim.length;
tags = `${processStyleTags(delim.substring(2))}`;
endTags = delim.length + src.indexOf(delim);
}
if(delim.startsWith('{{')) {
blockCount++;
@@ -132,14 +192,14 @@ const mustacheInjectInline = {
level : 'inline',
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) {
const inlineRegex = /^ *{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/g;
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g;
const match = inlineRegex.exec(src);
if(match) {
const lastToken = tokens[tokens.length - 1];
if(!lastToken || lastToken.type == 'mustacheInjectInline')
return false;
const tags = ` ${processStyleTags(match[1])}`;
const tags = `${processStyleTags(match[1])}`;
lastToken.originalType = lastToken.type;
lastToken.type = 'mustacheInjectInline';
lastToken.tags = tags;
@@ -167,7 +227,7 @@ const mustacheInjectBlock = {
level : 'block',
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) {
const inlineRegex = /^ *{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/ym;
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym;
const match = inlineRegex.exec(src);
if(match) {
const lastToken = tokens[tokens.length - 1];
@@ -175,7 +235,7 @@ const mustacheInjectBlock = {
return false;
lastToken.originalType = 'mustacheInjectBlock';
lastToken.tags = ` ${processStyleTags(match[1])}`;
lastToken.tags = `${processStyleTags(match[1])}`;
return {
type : 'mustacheInjectBlock', // Should match "name" above
raw : match[0], // Text to consume from the source
@@ -206,6 +266,34 @@ const mustacheInjectBlock = {
}
};
const superSubScripts = {
name : 'superSubScript',
level : 'inline',
start(src) { return src.match(/\^/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) {
const superRegex = /^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/m;
const subRegex = /^\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/m;
let isSuper = false;
let match = subRegex.exec(src);
if(!match){
match = superRegex.exec(src);
if(match)
isSuper = true;
}
if(match?.length) {
return {
type : 'superSubScript', // Should match "name" above
raw : match[0], // Text to consume from the source
tag : isSuper ? 'sup' : 'sub',
tokens : this.lexer.inlineTokens(match[1])
};
}
},
renderer(token) {
return `<${token.tag}>${this.parser.parseInline(token.tokens)}</${token.tag}>`;
}
};
const definitionLists = {
name : 'definitionLists',
level : 'block',
@@ -238,33 +326,257 @@ const definitionLists = {
}
};
Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists] });
Marked.use(mustacheInjectBlock);
Marked.use({ renderer: renderer, mangle: false });
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite());
//Fix local links in the Preview iFrame to link inside the frame
renderer.link = function (href, title, text) {
let self = false;
if(href[0] == '#') {
self = true;
}
href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
//v=====--------------------< Variable Handling >-------------------=====v// 242 lines
const replaceVar = function(input, hoist=false, allowUnresolved=false) {
const regex = /([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g;
const match = regex.exec(input);
if(href === null) {
return text;
const prefix = match[1];
const label = match[2];
//v=====--------------------< HANDLE MATH >-------------------=====v//
const mathRegex = /[a-z]+\(|[+\-*/^()]/g;
const matches = label.split(mathRegex);
const mathVars = matches.filter((match)=>isNaN(match))?.map((s)=>s.trim()); // Capture any variable names
let replacedLabel = label;
if(prefix[0] == '$' && mathVars?.[0] !== label.trim()) {// If there was mathy stuff not captured, let's do math!
mathVars?.forEach((variable)=>{
const foundVar = lookupVar(variable, globalPageNumber, hoist);
if(foundVar && foundVar.resolved && foundVar.content && !isNaN(foundVar.content)) // Only subsitute math values if fully resolved, not empty strings, and numbers
replacedLabel = replacedLabel.replaceAll(variable, foundVar.content);
});
try {
return mathParser.evaluate(replacedLabel);
} catch (error) {
return undefined; // Return undefined if invalid math result
}
}
let out = `<a href="${escape(href)}"`;
if(title) {
out += ` title="${title}"`;
}
if(self) {
out += ' target="_self"';
}
out += `>${text}</a>`;
return out;
//^=====--------------------< HANDLE MATH >-------------------=====^//
const foundVar = lookupVar(label, globalPageNumber, hoist);
if(!foundVar || (!foundVar.resolved && !allowUnresolved))
return undefined; // Return undefined if not found, or parially-resolved vars are not allowed
// url or <url> "title" or 'title' or (title)
const linkRegex = /^([^<\s][^\s]*|<.*?>)(?: ("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\((?:\\\(|\\\)|[^()])*\)))?$/m;
const linkMatch = linkRegex.exec(foundVar.content);
const href = linkMatch ? linkMatch[1] : null; //TODO: TRIM OFF < > IF PRESENT
const title = linkMatch ? linkMatch[2]?.slice(1, -1) : null;
if(!prefix[0] && href) // Link
return `[${label}](${href}${title ? ` "${title}"` : ''})`;
if(prefix[0] == '!' && href) // Image
return `![${label}](${href} ${title ? ` "${title}"` : ''})`;
if(prefix[0] == '$') // Variable
return foundVar.content;
};
const lookupVar = function(label, index, hoist=false) {
while (index >= 0) {
if(globalVarsList[index]?.[label] !== undefined)
return globalVarsList[index][label];
index--;
}
if(hoist) { //If normal lookup failed, attempt hoisting
index = Object.keys(globalVarsList).length; // Move index to start from last page
while (index >= 0) {
if(globalVarsList[index]?.[label] !== undefined)
return globalVarsList[index][label];
index--;
}
}
return undefined;
};
const processVariableQueue = function() {
let resolvedOne = true;
let finalLoop = false;
while (resolvedOne || finalLoop) { // Loop through queue until no more variable calls can be resolved
resolvedOne = false;
for (const item of varsQueue) {
if(item.type == 'text')
continue;
if(item.type == 'varDefBlock') {
const regex = /[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g;
let match;
let resolved = true;
let tempContent = item.content;
while (match = regex.exec(item.content)) { // regex to find variable calls
const value = replaceVar(match[0], true);
if(value == undefined)
resolved = false;
else
tempContent = tempContent.replaceAll(match[0], value);
}
if(resolved == true || item.content != tempContent) {
resolvedOne = true;
item.content = tempContent;
}
globalVarsList[globalPageNumber][item.varName] = {
content : item.content,
resolved : resolved
};
if(resolved)
item.type = 'resolved';
}
if(item.type == 'varCallBlock' || item.type == 'varCallInline') {
const value = replaceVar(item.content, true, finalLoop); // final loop will just use the best value so far
if(value == undefined)
continue;
resolvedOne = true;
item.content = value;
item.type = 'text';
}
}
varsQueue = varsQueue.filter((item)=>item.type !== 'resolved'); // Remove any fully-resolved variable definitions
if(finalLoop)
break;
if(!resolvedOne)
finalLoop = true;
}
varsQueue = varsQueue.filter((item)=>item.type !== 'varDefBlock');
};
function MarkedVariables() {
return {
hooks : {
preprocess(src) {
const codeBlockSkip = /^(?: {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+|^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})(?:[^\n]*)(?:\n|$)(?:|(?:[\s\S]*?)(?:\n|$))(?: {0,3}\2[~`]* *(?=\n|$))|`[^`]*?`/;
const blockDefRegex = /^[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]:(?!\() *((?:\n? *[^\s].*)+)(?=\n+|$)/; //Matches 3, [4]:5
const blockCallRegex = /^[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\](?=\n|$)/; //Matches 6, [7]
const inlineDefRegex = /([!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\])\(([^\n]+)\)/; //Matches 8, 9[10](11)
const inlineCallRegex = /[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\](?!\()/; //Matches 12, [13]
// Combine regexes and wrap in parens like so: (regex1)|(regex2)|(regex3)|(regex4)
const combinedRegex = new RegExp([codeBlockSkip, blockDefRegex, blockCallRegex, inlineDefRegex, inlineCallRegex].map((s)=>`(${s.source})`).join('|'), 'gm');
let lastIndex = 0;
let match;
while ((match = combinedRegex.exec(src)) !== null) {
// Format any matches into tokens and store
if(match.index > lastIndex) { // Any non-variable stuff
varsQueue.push(
{ type : 'text',
varName : null,
content : src.slice(lastIndex, match.index)
});
}
if(match[1]) {
varsQueue.push(
{ type : 'text',
varName : null,
content : match[0]
});
}
if(match[3]) { // Block Definition
const label = match[4] ? match[4].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
const content = match[5] ? match[5].trim().replace(/[ \t]+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
varsQueue.push(
{ type : 'varDefBlock',
varName : label,
content : content
});
}
if(match[6]) { // Block Call
const label = match[7] ? match[7].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
varsQueue.push(
{ type : 'varCallBlock',
varName : label,
content : match[0]
});
}
if(match[8]) { // Inline Definition
const label = match[10] ? match[10].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
let content = match[11] ? match[11].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
// In case of nested (), find the correct matching end )
let level = 0;
let i;
for (i = 0; i < content.length; i++) {
if(content[i] === '\\') {
i++;
} else if(content[i] === '(') {
level++;
} else if(content[i] === ')') {
level--;
if(level < 0)
break;
}
}
if(i > -1) {
combinedRegex.lastIndex = combinedRegex.lastIndex - (content.length - i);
content = content.slice(0, i).trim().replace(/\s+/g, ' ');
}
varsQueue.push(
{ type : 'varDefBlock',
varName : label,
content : content
});
varsQueue.push(
{ type : 'varCallInline',
varName : label,
content : match[9]
});
}
if(match[12]) { // Inline Call
const label = match[13] ? match[13].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
varsQueue.push(
{ type : 'varCallInline',
varName : label,
content : match[0]
});
}
lastIndex = combinedRegex.lastIndex;
}
if(lastIndex < src.length) {
varsQueue.push(
{ type : 'text',
varName : null,
content : src.slice(lastIndex)
});
}
processVariableQueue();
const output = varsQueue.map((item)=>item.content).join('');
varsQueue = []; // Must clear varsQueue because custom HTML renderer uses Marked.parse which will preprocess again without clearing the array
return output;
}
}
};
};
//^=====--------------------< Variable Handling >-------------------=====^//
Marked.use(MarkedVariables());
Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists, superSubScripts] });
Marked.use(mustacheInjectBlock);
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite());
const nonWordAndColonTest = /[^\w:]/g;
const cleanUrl = function (sanitize, base, href) {
if(sanitize) {
@@ -326,24 +638,43 @@ const voidTags = new Set([
]);
const processStyleTags = (string)=>{
//split tags up. quotes can only occur right after colons.
//split tags up. quotes can only occur right after : or =.
//TODO: can we simplify to just split on commas?
const tags = string.match(/(?:[^, ":]+|:(?:"[^"]*"|))+/g);
const tags = string.match(/(?:[^, ":=]+|[:=](?:"[^"]*"|))+/g);
if(!tags) return '"';
const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0];
const classes = _.remove(tags, (tag)=>(!tag.includes(':')) && (!tag.includes('=')));
const attributes = _.remove(tags, (tag)=>(tag.includes('='))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"'));
const styles = tags?.length ? tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;').trim()) : [];
const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0];
const classes = _.remove(tags, (tag)=>!tag.includes(':'));
const styles = tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;'));
return `${classes.join(' ')}" ${id ? `id="${id}"` : ''} ${styles.length ? `style="${styles.join(' ')}"` : ''}`;
return `${classes?.length ? ` ${classes.join(' ')}` : ''}"` +
`${id ? ` id="${id}"` : ''}` +
`${styles?.length ? ` style="${styles.join(' ')}"` : ''}` +
`${attributes?.length ? ` ${attributes.join(' ')}` : ''}`;
};
const globalVarsList = {};
let varsQueue = [];
let globalPageNumber = 0;
module.exports = {
marked : Marked,
render : (rawBrewText)=>{
render : (rawBrewText, pageNumber=1)=>{
globalVarsList[pageNumber] = {}; //Reset global links for current page, to ensure values are parsed in order
varsQueue = []; //Could move into MarkedVariables()
globalPageNumber = pageNumber;
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
return Marked.parse(rawBrewText);
const opts = Marked.defaults;
rawBrewText = opts.hooks.preprocess(rawBrewText);
const tokens = Marked.lexer(rawBrewText, opts);
Marked.walkTokens(tokens, opts.walkTokens);
const html = Marked.parser(tokens, opts);
return opts.hooks.postprocess(html);
},
validate : (rawBrewText)=>{

View File

@@ -13,137 +13,134 @@ String.prototype.trimReturns = function(){
// Remove the `.failing()` method once you have fixed the issue.
describe('Inline: When using the Inline syntax {{ }}', ()=>{
it.failing('Renders a mustache span with text only', function() {
it('Renders a mustache span with text only', function() {
const source = '{{ text}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">text</span>');
});
it.failing('Renders a mustache span with text only, but with spaces', function() {
it('Renders a mustache span with text only, but with spaces', function() {
const source = '{{ this is a text}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">this is a text</span>');
});
it.failing('Renders an empty mustache span', function() {
it('Renders an empty mustache span', function() {
const source = '{{}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
});
it.failing('Renders a mustache span with just a space', function() {
it('Renders a mustache span with just a space', function() {
const source = '{{ }}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
});
it.failing('Renders a mustache span with a few spaces only', function() {
it('Renders a mustache span with a few spaces only', function() {
const source = '{{ }}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
});
it.failing('Renders a mustache span with text and class', function() {
it('Renders a mustache span with text and class', function() {
const source = '{{my-class text}}';
const rendered = Markdown.render(source);
// FIXME: adds two extra \s before closing `>` in opening tag.
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class">text</span>');
});
it.failing('Renders a mustache span with text and two classes', function() {
it('Renders a mustache span with text and two classes', function() {
const source = '{{my-class,my-class2 text}}';
const rendered = Markdown.render(source);
// FIXME: adds two extra \s before closing `>` in opening tag.
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class my-class2">text</span>');
});
it.failing('Renders a mustache span with text with spaces and class', function() {
it('Renders a mustache span with text with spaces and class', function() {
const source = '{{my-class this is a text}}';
const rendered = Markdown.render(source);
// FIXME: adds two extra \s before closing `>` in opening tag
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class">this is a text</span>');
});
it.failing('Renders a mustache span with text and id', function() {
it('Renders a mustache span with text and id', function() {
const source = '{{#my-span text}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s before closing `>` in opening tag, and another after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" id="my-span">text</span>');
});
it.failing('Renders a mustache span with text and two ids', function() {
it('Renders a mustache span with text and two ids', function() {
const source = '{{#my-span,#my-favorite-span text}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s before closing `>` in opening tag, and another after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" id="my-span">text</span>');
});
it.failing('Renders a mustache span with text and css property', function() {
it('Renders a mustache span with text and css property', function() {
const source = '{{color:red text}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>');
});
it.failing('Renders a mustache span with text and two css properties', function() {
it('Renders a mustache span with text and two css properties', function() {
const source = '{{color:red,padding:5px text}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red; padding:5px;">text</span>');
});
it.failing('Renders a mustache span with text and css property which contains quotes', function() {
it('Renders a mustache span with text and css property which contains quotes', function() {
const source = '{{font-family:"trebuchet ms" text}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms;">text</span>');
});
it.failing('Renders a mustache span with text and two css properties which contains quotes', function() {
it('Renders a mustache span with text and two css properties which contains quotes', function() {
const source = '{{font-family:"trebuchet ms",padding:"5px 10px" text}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms; padding:5px 10px;">text</span>');
});
it.failing('Renders a mustache span with text with quotes and css property which contains quotes', function() {
it('Renders a mustache span with text with quotes and css property which contains double quotes', function() {
const source = '{{font-family:"trebuchet ms" text "with quotes"}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms;">text “with quotes”</span>');
});
it('Renders a mustache span with text with quotes and css property which contains double and simple quotes', function() {
const source = `{{--stringVariable:"'string'" text "with quotes"}}`;
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<span class="inline-block" style="--stringVariable:'string';">text “with quotes”</span>`);
});
it('Renders a mustache span with text, id, class and a couple of css properties', function() {
const source = '{{pen,#author,color:orange,font-family:"trebuchet ms" text}}';
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block pen" id="author" style="color:orange; font-family:trebuchet ms;">text</span>');
});
it('Renders a span with added attributes', function() {
const source = 'Text and {{pen,#author,color:orange,font-family:"trebuchet ms",a="b and c",d=e, text}} and more text!';
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>Text and <span class="inline-block pen" id="author" style="color:orange; font-family:trebuchet ms;" a="b and c" d="e">text</span> and more text!</p>\n');
});
});
// BLOCK SYNTAX
describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
it.failing('Renders a div with text only', function() {
it('Renders a div with text only', function() {
const source = dedent`{{
text
}}`;
const rendered = Markdown.render(source).trimReturns();
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"><p>text</p></div>`);
});
it.failing('Renders an empty div', function() {
it('Renders an empty div', function() {
const source = dedent`{{
}}`;
const rendered = Markdown.render(source).trimReturns();
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"></div>`);
});
@@ -151,52 +148,62 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
const source = dedent`{{
}}`;
const rendered = Markdown.render(source).trimReturns();
// this actually renders in HB as '{{ }}'...
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>{{}}</p>`);
});
it.failing('Renders a div with a single class', function() {
it('Renders a div with a single class', function() {
const source = dedent`{{cat
}}`;
const rendered = Markdown.render(source).trimReturns();
// FIXME: adds two extra \s before closing `>` in opening tag
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"></div>`);
});
it.failing('Renders a div with a single class and text', function() {
it('Renders a div with a single class and text', function() {
const source = dedent`{{cat
Sample text.
}}`;
const rendered = Markdown.render(source).trimReturns();
// FIXME: adds two extra \s before closing `>` in opening tag
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"><p>Sample text.</p></div>`);
});
it.failing('Renders a div with two classes and text', function() {
it('Renders a div with two classes and text', function() {
const source = dedent`{{cat,dog
Sample text.
}}`;
const rendered = Markdown.render(source).trimReturns();
// FIXME: adds two extra \s before closing `>` in opening tag
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat dog"><p>Sample text.</p></div>`);
});
it.failing('Renders a div with a style and text', function() {
it('Renders a div with a style and text', function() {
const source = dedent`{{color:red
Sample text.
}}`;
const rendered = Markdown.render(source).trimReturns();
// FIXME: adds two extra \s before closing `>` in opening tag
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:red;"><p>Sample text.</p></div>`);
});
it.failing('Renders a div with a class, style and text', function() {
it('Renders a div with a style that has a string variable, and text', function() {
const source = dedent`{{--stringVariable:"'string'"
Sample text.
}}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string';"><p>Sample text.</p></div>`);
});
it('Renders a div with a style that has a string variable, and text', function() {
const source = dedent`{{--stringVariable:"'string'"
Sample text.
}}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string';"><p>Sample text.</p></div>`);
});
it('Renders a div with a class, style and text', function() {
const source = dedent`{{cat,color:red
Sample text.
}}`;
const rendered = Markdown.render(source).trimReturns();
// FIXME: adds extra \s after the class attribute
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" style="color:red;"><p>Sample text.</p></div>`);
});
@@ -208,14 +215,27 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" id="dog" style="color:red;"><p>Sample text.</p></div>`);
});
it.failing('Renders a div with a single ID', function() {
it('Renders a div with a single ID', function() {
const source = dedent`{{#cat,#dog
Sample text.
}}`;
const rendered = Markdown.render(source).trimReturns();
// FIXME: adds extra \s before closing `>` in opening tag, and another after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" id="cat"><p>Sample text.</p></div>`);
});
it('Renders a div with an ID, class, style and text, and a variable assignment', function() {
const source = dedent`{{color:red,cat,#dog,a="b and c",d="e"
Sample text.
}}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class=\"block cat\" id=\"dog\" style=\"color:red;\" a=\"b and c\" d=\"e\"><p>Sample text.</p></div>`);
});
it('Renders a div with added attributes', function() {
const source = '{{pen,#author,color:orange,font-family:"trebuchet ms",a="b and c",d=e\nText and text and more text!\n}}\n';
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block pen" id="author" style="color:orange; font-family:trebuchet ms;" a="b and c" d="e"><p>Text and text and more text!</p>\n</div>');
});
});
// MUSTACHE INJECTION SYNTAX
@@ -235,12 +255,24 @@ describe('Injection: When an injection tag follows an element', ()=>{
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block ClassName">text</span>');
});
it.failing('Renders a span "text" with injected attribute', function() {
const source = '{{ text}}{a="b and c"}';
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span a="b and c" class="inline-block ">text</span>');
});
it.failing('Renders a span "text" with injected style', function() {
const source = '{{ text}}{color:red}';
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>');
});
it.failing('Renders a span "text" with injected style using a string variable', function() {
const source = `{{ text}}{--stringVariable:"'string'"}`;
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<span class="inline-block" style="--stringVariable:'string';">text</span>`);
});
it.failing('Renders a span "text" with two injected styles', function() {
const source = '{{ text}}{color:red,background:blue}';
const rendered = Markdown.render(source);
@@ -270,6 +302,12 @@ describe('Injection: When an injection tag follows an element', ()=>{
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><span class="inline-block" style="color:red;">text</span>{background:blue}</p>');
});
it('Renders an image with added attributes', function() {
const source = `![homebrew mug](https://i.imgur.com/hMna6G0.png) {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p><img class="" style="position:absolute; bottom:20px; left:130px; width:220px;" a="b and c" d="e" src="https://i.imgur.com/hMna6G0.png" alt="homebrew mug"></p>`);
});
});
describe('and that element is a block', ()=>{
@@ -297,7 +335,16 @@ describe('Injection: When an injection tag follows an element', ()=>{
}}
{color:red,background:blue}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" style="color:red; background:blue;"><p>text</p></div>');
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:red; background:blue"><p>text</p></div>`);
});
it.failing('renders a div "text" with injected variable string', function() {
const source = dedent`{{
text
}}
{--stringVariable:"'string'"}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="--stringVariable:'string'"><p>text</p></div>`);
});
it.failing('renders an h2 header "text" with injected class name', function() {

View File

@@ -0,0 +1,373 @@
/* eslint-disable max-lines */
const dedent = require('dedent-tabs').default;
const Markdown = require('naturalcrit/markdown.js');
// Marked.js adds line returns after closing tags on some default tokens.
// This removes those line returns for comparison sake.
String.prototype.trimReturns = function(){
return this.replace(/\r?\n|\r/g, '').trim();
};
renderAllPages = function(pages){
const outputs = [];
pages.forEach((page, index)=>{
const output = Markdown.render(page, index);
outputs.push(output);
});
return outputs;
};
// Adding `.failing()` method to `describe` or `it` will make failing tests "pass" as long as they continue to fail.
// Remove the `.failing()` method once you have fixed the issue.
describe('Block-level variables', ()=>{
it('Handles variable assignment and recall with simple text', function() {
const source = dedent`
[var]: string
$[var]
`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string</p>');
});
it('Handles variable assignment and recall with multiline string', function() {
const source = dedent`
[var]: string
across multiple
lines
$[var]`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string across multiple lines</p>');
});
it('Handles variable assignment and recall with tables', function() {
const source = dedent`
[var]:
##### Title
| H1 | H2 |
|:---|:--:|
| A | B |
| C | D |
$[var]`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<h5 id="title">Title</h5>
<table><thead><tr><th align=left>H1</th>
<th align=center>H2</th>
</tr></thead><tbody><tr><td align=left>A</td>
<td align=center>B</td>
</tr><tr><td align=left>C</td>
<td align=center>D</td>
</tr></tbody></table>`.trimReturns());
});
it('Hoists undefined variables', function() {
const source = dedent`
$[var]
[var]: string`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string</p>');
});
it('Hoists last instance of variable', function() {
const source = dedent`
$[var]
[var]: string
[var]: new string`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>new string</p>');
});
it('Handles complex hoisting', function() {
const source = dedent`
$[titleAndName]: $[title] $[fullName]
$[title]: Mr.
$[fullName]: $[firstName] $[lastName]
[firstName]: Bob
Welcome, $[titleAndName]!
[lastName]: Jacob
[lastName]: $[lastName]son
`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>Welcome, Mr. Bob Jacobson!</p>');
});
it('Handles variable reassignment', function() {
const source = dedent`
[var]: one
$[var]
[var]: two
$[var]
`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>one</p><p>two</p>'.trimReturns());
});
it('Handles variable reassignment with hoisting', function() {
const source = dedent`
$[var]
[var]: one
$[var]
[var]: two
$[var]
`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>two</p><p>one</p><p>two</p>'.trimReturns());
});
it('Ignores undefined variables that can\'t be hoisted', function() {
const source = dedent`
$[var](My name is $[first] $[last])
$[last]: Jones
`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>My name is $[first] Jones</p>`.trimReturns());
});
});
describe('Inline-level variables', ()=>{
it('Handles variable assignment and recall with simple text', function() {
const source = dedent`
$[var](string)
$[var]
`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string</p><p>string</p>');
});
it('Hoists undefined variables when possible', function() {
const source = dedent`
$[var](My name is $[name] Jones)
[name]: Bob`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>My name is Bob Jones</p>');
});
it('Hoists last instance of variable', function() {
const source = dedent`
$[var](My name is $[name] Jones)
$[name](Bob)
[name]: Bill`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>My name is Bill Jones</p> <p>Bob</p>`.trimReturns());
});
it('Only captures nested parens if balanced', function() {
const source = dedent`
$[var1](A variable (with nested parens) inside)
$[var1]
$[var2](A variable ) with unbalanced parens)
$[var2]`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p>A variable (with nested parens) inside</p>
<p>A variable (with nested parens) inside</p>
<p>A variable with unbalanced parens)</p>
<p>A variable</p>
`.trimReturns());
});
});
describe('Math', ()=>{
it('Handles simple math using numbers only', function() {
const source = dedent`
$[1 + 3 * 5 - (1 / 4)]
`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>15.75</p>');
});
it('Handles round function', function() {
const source = dedent`
$[round(1/4)]`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>0</p>');
});
it('Handles floor function', function() {
const source = dedent`
$[floor(0.6)]`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>0</p>');
});
it('Handles ceil function', function() {
const source = dedent`
$[ceil(0.2)]`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>1</p>');
});
it('Handles nested functions', function() {
const source = dedent`
$[ceil(floor(round(0.6)))]`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>1</p>');
});
it('Handles simple math with variables', function() {
const source = dedent`
$[num1]: 5
$[num2]: 4
Answer is $[answer]($[1 + 3 * num1 - (1 / num2)]).
`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>Answer is 15.75.</p>');
});
it('Handles variable incrementing', function() {
const source = dedent`
$[num1]: 5
Increment num1 to get $[num1]($[num1 + 1]) and again to $[num1]($[num1 + 1]).
`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>Increment num1 to get 6 and again to 7.</p>');
});
});
describe('Code blocks', ()=>{
it('Ignores all variables in fenced code blocks', function() {
const source = dedent`
\`\`\`
[var]: string
$[var]
$[var](new string)
\`\`\`
`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<pre><code>
[var]: string
$[var]
$[var](new string)
</code></pre>`.trimReturns());
});
it('Ignores all variables in indented code blocks', function() {
const source = dedent`
test
[var]: string
$[var]
$[var](new string)
`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p>test</p>
<pre><code>
[var]: string
$[var]
$[var](new string)
</code></pre>`.trimReturns());
});
it('Ignores all variables in inline code blocks', function() {
const source = '[var](Hello) `[link](url)`. This `[var] does not work`';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p><a href="Hello">var</a> <code>[link](url)</code>. This <code>[var] does not work</code></p>`.trimReturns());
});
});
describe('Normal Links and Images', ()=>{
it('Renders normal images', function() {
const source = `![alt text](url)`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p><img src="url" alt="alt text"></p>`.trimReturns());
});
it('Renders normal images with a title', function() {
const source = 'An image ![alt text](url "and title")!';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p>An image <img src="url" alt="alt text" title="and title">!</p>`.trimReturns());
});
it('Applies curly injectors to images', function() {
const source = `![alt text](url){width:100px}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p><img class="" style="width:100px;" src="url" alt="alt text"></p>`.trimReturns());
});
it('Renders normal links', function() {
const source = 'A Link to my [website](url)!';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p>A Link to my <a href="url">website</a>!</p>`.trimReturns());
});
it('Renders normal links with a title', function() {
const source = 'A Link to my [website](url "and title")!';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p>A Link to my <a href="url" title="and title">website</a>!</p>`.trimReturns());
});
});
describe('Cross-page variables', ()=>{
it('Handles variable assignment and recall across pages', function() {
const source0 = `[var]: string`;
const source1 = `$[var]`;
const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns();
expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('\\page<p>string</p>');
});
it('Handles hoisting across pages', function() {
const source0 = `$[var]`;
const source1 = `[var]: string`;
renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); //Requires one full render of document before hoisting is picked up
const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns();
expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('<p>string</p>\\page');
});
it('Handles reassignment and hoisting across pages', function() {
const source0 = `$[var]\n\n[var]: one\n\n$[var]`;
const source1 = `[var]: two\n\n$[var]`;
renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); //Requires one full render of document before hoisting is picked up
const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns();
expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('<p>two</p><p>one</p>\\page<p>two</p>');
});
});

View File

@@ -40,7 +40,7 @@ body {
-webkit-column-gap : 1cm;
-moz-column-gap : 1cm;
}
.phb{
.phb, .page{
.useColumns();
counter-increment : phb-page-numbers;
position : relative;
@@ -59,6 +59,9 @@ body {
page-break-before : always;
page-break-after : always;
contain : size;
}
.phb{
//*****************************
// * BASE
// *****************************/

View File

@@ -42,8 +42,8 @@ module.exports = function(classname){
#### Equipment
You start with the following equipment, in addition to the equipment granted by your background:
- *(a)* a martial weapon and a shield or *(b)* two martial weapons
- *(a)* five javelins or *(b)* any simple melee weapon
- (*a*) a martial weapon and a shield or (*b*) two martial weapons
- (*a*) five javelins or (*b*) any simple melee weapon
- ${_.sample(['10 lint fluffs', '1 button', 'a cherished lost sock'])}
`;
};

View File

@@ -1,5 +1,6 @@
@import (less) './themes/fonts/5e/fonts.less';
@import (less) './themes/assets/assets.less';
@import (less) './themes/fonts/icon fonts/font-icons.less';
:root {
//Colors
@@ -14,16 +15,13 @@
--HB_Color_Footnotes : #C9AD6A; // Gold
}
@page { margin : 0; }
body { counter-reset : phb-page-numbers; }
* { -webkit-print-color-adjust : exact; }
.useSansSerif() {
font-family : "ScalySansRemake";
font-family : 'ScalySansRemake';
font-size : 0.318cm;
line-height : 1.2em;
p,dl,ul,ol { line-height : 1.2em; }
ul, ol { padding-left : 1em; }
em { font-style : italic; }
em { font-style : italic; }
strong {
font-weight : 800;
letter-spacing : -0.02em;
@@ -42,55 +40,31 @@ body { counter-reset : phb-page-numbers; }
-webkit-column-gap : 0.9cm;
-moz-column-gap : 0.9cm;
}
.columnWrapper {
column-gap : inherit;
max-height : 100%;
column-span : all;
columns : inherit;
}
.page {
.useColumns();
position : relative;
z-index : 15;
box-sizing : border-box;
width : 215.9mm;
height : 279.4mm;
padding : 1.4cm 1.9cm 1.7cm;
overflow : hidden;
font-family : "BookInsanityRemake";
font-size : 0.34cm;
counter-increment : phb-page-numbers;
background-color : var(--HB_Color_Background);
background-image : @backgroundImage;
text-rendering : optimizeLegibility;
page-break-before : always;
page-break-after : always;
font-family : 'BookInsanityRemake';
font-size : 0.34cm;
background-image : @backgroundImage;
}
//*****************************
// *****************************
// * BASE
// *****************************/
// *****************************/
.page {
p {
display : block;
line-height : 1.25em;
overflow-wrap : break-word; //TODO: MAKE ALL MARGINS TOP-ONLY. USE * + * STYLE SELECTORS
& + * { margin-top : 0.325cm; }
line-height : 1.25em;
& + * { margin-top : 0.325cm; } //TODO: MAKE ALL MARGINS TOP-ONLY. USE * + * STYLE SELECTORS
& + p { margin-top : 0; }
}
ul {
padding-left : 1.4em;
margin-bottom : 0.8em;
line-height : 1.25em;
list-style-position : outside;
list-style-type : disc;
padding-left : 1.4em;
margin-bottom : 0.8em;
line-height : 1.25em;
}
ol {
padding-left : 1.4em;
margin-bottom : 0.8em;
line-height : 1.25em;
list-style-position : outside;
list-style-type : decimal;
padding-left : 1.4em;
margin-bottom : 0.8em;
line-height : 1.25em;
}
//Indents after p or lists
p + p, ul + p, ol + p { text-indent : 1em; }
@@ -99,24 +73,12 @@ body { counter-reset : phb-page-numbers; }
font-weight : bold;
letter-spacing : -0.02em;
}
em { font-style : italic; }
sup {
font-size : smaller;
line-height : 0;
vertical-align : super;
}
sub {
font-size : smaller;
line-height : 0;
vertical-align : sub;
}
//*****************************
// *****************************
// * HEADERS
// *****************************/
h1,h2,h3,h4 {
font-family : "MrEavesRemake";
font-weight : 800;
color : var(--HB_Color_HeaderText);
font-family : 'MrEavesRemake';
color : var(--HB_Color_HeaderText);
}
h1 {
margin-bottom : 0.18cm; //Margin-bottom only because this is WIDE
@@ -132,7 +94,7 @@ body { counter-reset : phb-page-numbers; }
margin-top : -0.3cm;
margin-bottom : -20px;
margin-left : -40px;
font-family : "SolberaImitationRemake";
font-family : 'SolberaImitationRemake';
font-size : 3.5cm;
line-height : 1em;
color : rgba(0, 0, 0, 0);
@@ -145,8 +107,8 @@ body { counter-reset : phb-page-numbers; }
h2 {
//margin-top : 0px; //Font is misaligned. Shift up slightly
//margin-bottom : 0.05cm;
font-size : 0.75cm;
line-height : 0.988em; //Font is misaligned. Shift up slightly
font-size : 0.75cm;
line-height : 0.988em; //Font is misaligned. Shift up slightly
}
h3 {
//margin-top : -0.1cm; //Font is misaligned. Shift up slightly
@@ -162,8 +124,8 @@ body { counter-reset : phb-page-numbers; }
h4 {
//margin-top : -0.02cm; //Font is misaligned. Shift up slightly
//margin-bottom : 0.02cm;
font-size : 0.458cm;
line-height : 0.971em; //Font is misaligned. Shift up slightly
font-size : 0.458cm;
line-height : 0.971em; //Font is misaligned. Shift up slightly
& + * { margin-top : 0.09cm; }
}
* + h4 {
@@ -172,19 +134,17 @@ body { counter-reset : phb-page-numbers; }
h5 {
//margin-top : -0.02cm; //Font is misaligned. Shift up slightly
//margin-bottom : 0.02cm;
font-family : "ScalySansSmallCapsRemake";
font-size : 0.423cm;
font-weight : 900;
line-height : 0.951em; //Font is misaligned. Shift up slightly
font-family : 'ScalySansSmallCapsRemake';
font-size : 0.423cm;
line-height : 0.951em; //Font is misaligned. Shift up slightly
& + * { margin-top : 0.2cm; }
}
//*****************************
// *****************************
// * TABLE
// *****************************/
table {
.useSansSerif();
width : 100%;
line-height : 16px;
line-height : 16px;
& + * { margin-top : 0.325cm; }
thead {
display : table-row-group;
@@ -200,15 +160,15 @@ body { counter-reset : phb-page-numbers; }
tr {
td {
//padding : 0.14em 0.4em;
padding : 0px 1.5px; // Both of these are temporary, just to force
padding : 0px 1.5px; // Both of these are temporary, just to force
//line-height : 16px; // PDF to render at same height until Chrome 108
}
&:nth-child(odd) { background-color : var(--HB_Color_Accent); }
}
}
}
//*****************************
// * QUOTE
// *****************************
// * QUOTE
// *****************************/
.quote {
@@ -241,9 +201,7 @@ body { counter-reset : phb-page-numbers; }
}
//*****************************
// *****************************
// * NOTE
// *****************************/
.note {
@@ -257,7 +215,7 @@ body { counter-reset : phb-page-numbers; }
border-image-outset : 9px 0px;
box-shadow : 1px 4px 14px #888888;
.page :where(&) {
margin-top : 9px; //Prevent top border getting cut off on colbreak
margin-top : 9px; //Prevent top border getting cut off on colbreak
}
& + * { margin-top : 0.45cm; }
h5 { font-size : 0.375cm; }
@@ -267,7 +225,7 @@ body { counter-reset : phb-page-numbers; }
}
:last-child { margin-bottom : 0; }
}
//************************************
// ************************************
// * DESCRIPTIVE TEXT BOX
// ************************************/
.descriptive {
@@ -291,7 +249,7 @@ body { counter-reset : phb-page-numbers; }
}
:last-child { margin-bottom : 0; }
}
//*****************************
// *****************************
// * Images Snippets
// *****************************/
@@ -299,7 +257,7 @@ body { counter-reset : phb-page-numbers; }
.artist {
position : absolute;
width : auto;
font-family : "WalterTurncoat";
font-family : 'WalterTurncoat';
font-size : 0.27cm;
color : var(--HB_Color_CaptionText);
text-align : center;
@@ -309,7 +267,7 @@ body { counter-reset : phb-page-numbers; }
text-indent : unset;
}
h5 {
font-family : "WalterTurncoat";
font-family : 'WalterTurncoat';
font-size : 1.3em;
}
a {
@@ -320,42 +278,10 @@ body { counter-reset : phb-page-numbers; }
}
/* Watermark */
.watermark {
position : absolute;
top : 0;
left : 0;
z-index : 500;
display : grid !important;
place-items : center;
justify-content : center;
width : 100%;
height : 100%;
font-size : 120px;
color : black;
text-transform : uppercase;
mix-blend-mode : overlay;
opacity : 30%;
transform : rotate(-45deg);
p { margin-bottom : none; }
}
.watermark { color : black; }
/* Watercolor */
[class*='watercolor'] {
position : absolute;
z-index : -2;
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 */
background-color : var(--HB_Color_WatercolorStain); /* default color */
background-size : cover;
-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;
--wc : @watercolor1; /* default image */
}
.watercolor1 { --wc : @watercolor1; }
.watercolor2 { --wc : @watercolor2; }
.watercolor3 { --wc : @watercolor3; }
@@ -369,7 +295,7 @@ body { counter-reset : phb-page-numbers; }
.watercolor11 { --wc : @watercolor11; }
.watercolor12 { --wc : @watercolor12; }
//*****************************
// *****************************
// * MONSTER STAT BLOCK
// *****************************/
.monster {
@@ -381,7 +307,7 @@ body { counter-reset : phb-page-numbers; }
margin-left : -0.16cm;
background-color : var(--HB_Color_MonsterStatBackground);
background-image : @monsterBlockBackground;
background-attachment : fixed;
background-attachment : unset;
background-blend-mode : overlay;
border-style : solid;
border-width : 7px 6px;
@@ -390,27 +316,24 @@ body { counter-reset : phb-page-numbers; }
box-shadow : 1px 4px 14px #888888;
}
position : relative;
padding : 0px;
margin-bottom : 0.325cm;
position : relative;
padding : 0px;
margin-bottom : 0.325cm;
//Headers
h2 {
margin : 0;
font-size : 0.62cm;
line-height : 1em;
margin : 0;
font-size : 0.62cm;
line-height : 1em;
& + p {
margin-bottom : 0;
font-size : 0.304cm; //Monster size and type subtext
}
}
h3 {
// margin-top : 0.05cm; //Font is misaligned. Shift up slightly
padding-bottom : 0.05cm;
font-family : "ScalySansRemake";
font-weight : 800;
font-variant : small-caps;
border-bottom : 2px solid var(--HB_Color_HeaderText);
font-family : 'ScalySansSmallCapsRemake';
font-size : 0.45cm;
border-bottom : 1.5px solid var(--HB_Color_HeaderText);
}
//Triangle dividers
@@ -446,6 +369,8 @@ body { counter-reset : phb-page-numbers; }
tr { background-color : transparent; }
td,th { padding : 0px; }
}
//indent fix after bulleted lists
:is(ul,ol) + p { text-indent : 0; }
:last-child { margin-bottom : 0; }
}
@@ -455,10 +380,10 @@ body { counter-reset : phb-page-numbers; }
.useColumns(0.96, @fillMode: balance);
}
//*****************************
// *****************************
// * FOOTER
// *****************************/
&:after {
&::after {
position : absolute;
bottom : 0px;
left : 0px;
@@ -498,23 +423,18 @@ body { counter-reset : phb-page-numbers; }
color : var(--HB_Color_Footnotes);
text-align : right;
}
//************************************
// ************************************
// * CODE BLOCKS
// ************************************/
code {
padding : 0px 4px;
font-family : 'Courier New', "Courier", monospace;
font-size : 0.325;
font-size : 0.325cm;
color : #58180D;
overflow-wrap : break-word;
white-space : pre-wrap;
background-color : #FAF7EA;
border-radius : 4px;
}
pre code {
display : inline-block;
width : 100%;
padding : 0.15cm;
margin-bottom : 2px;
border-style : solid;
@@ -528,26 +448,13 @@ body { counter-reset : phb-page-numbers; }
}
& + * { margin-top : 0.325cm; }
}
//*****************************
// *****************************
// * EXTRAS
// *****************************/
hr {
margin : 0px;
visibility : hidden;
}
.columnSplit {
visibility : hidden;
-webkit-column-break-after : always;
break-after : always;
-moz-column-break-after : always;
}
//Avoid breaking up
blockquote,table {
z-index : 15;
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
}
//Text indent right after table
table + p { text-indent : 1em; }
// Nested lists
@@ -555,18 +462,13 @@ body { counter-reset : phb-page-numbers; }
margin-bottom : 0px;
margin-left : 1.5em;
}
li {
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
}
}
//*****************************
// *****************************
// * SPELL LIST
// *****************************/
.page .spellList {
.useSansSerif();
column-count : 2;
column-count : 2;
ul + h5 { margin-top : 15px; }
p, ul {
font-size : 0.352cm;
@@ -584,7 +486,7 @@ body { counter-reset : phb-page-numbers; }
&.wide { column-count : 4; }
}
//*****************************
// *****************************
// * CLASS TABLE
// *****************************/
.page .classTable {
@@ -631,7 +533,7 @@ body { counter-reset : phb-page-numbers; }
}
h5 + table { margin-top : 0.2cm; }
}
//*****************************
// *****************************
// * FRONT COVER PAGE
// *****************************/
.page:has(.frontCover) {
@@ -641,7 +543,7 @@ body { counter-reset : phb-page-numbers; }
h1 {
margin-top : 1.2cm;
margin-bottom : 0;
font-family : "NodestoCapsCondensed";
font-family : 'NodestoCapsCondensed';
font-size : 2.245cm;
font-weight : normal;
line-height : 0.85em;
@@ -654,7 +556,7 @@ body { counter-reset : phb-page-numbers; }
drop-shadow(0 0 0 black) drop-shadow(0 0 0 black);
}
h2 {
font-family : "NodestoCapsCondensed";
font-family : 'NodestoCapsCondensed';
font-size : 0.85cm;
font-weight : normal;
color : white;
@@ -687,7 +589,7 @@ body { counter-reset : phb-page-numbers; }
height : 1.7cm;
padding-top : 0.1cm;
padding-left : 1cm;
font-family : "NodestoCapsCondensed";
font-family : 'NodestoCapsCondensed';
font-size : 1cm;
font-weight : normal;
color : white;
@@ -704,7 +606,7 @@ body { counter-reset : phb-page-numbers; }
width : 70%;
margin-right : auto;
margin-left : auto;
font-family : "Overpass";
font-family : 'Overpass';
font-size : 0.496cm;
color : white;
text-align : center;
@@ -725,7 +627,7 @@ body { counter-reset : phb-page-numbers; }
}
}
}
//*****************************
// *****************************
// * INSIDE COVER PAGE
// *****************************/
.page:has(.insideCover) {
@@ -735,14 +637,14 @@ body { counter-reset : phb-page-numbers; }
h1 {
margin-top : 1.2cm;
margin-bottom : 0;
font-family : "NodestoCapsCondensed";
font-family : 'NodestoCapsCondensed';
font-size : 2.1cm;
font-weight : normal;
line-height : 0.85em;
text-transform : uppercase;
}
h2 {
font-family : "NodestoCapsCondensed";
font-family : 'NodestoCapsCondensed';
font-size : 0.85cm;
font-weight : normal;
letter-spacing : 0.5cm;
@@ -770,7 +672,7 @@ body { counter-reset : phb-page-numbers; }
}
}
}
//*****************************
// *****************************
// * BACK COVER
// *****************************/
.page:has(.backCover) {
@@ -791,7 +693,7 @@ body { counter-reset : phb-page-numbers; }
.blank { height : 1.4em; }
h1 {
margin-bottom : 0.3cm;
font-family : "NodestoCapsCondensed";
font-family : 'NodestoCapsCondensed';
font-size : 1.35cm;
line-height : 0.95em;
color : #ED1C24;
@@ -817,7 +719,7 @@ body { counter-reset : phb-page-numbers; }
border : none;
}
p {
font-family : "Overpass";
font-family : 'Overpass';
font-size : 0.332cm;
line-height : 1.5em;
}
@@ -841,7 +743,7 @@ body { counter-reset : phb-page-numbers; }
p {
position : relative;
width : 100%;
font-family : "NodestoCapsWide";
font-family : 'NodestoCapsWide';
font-size : 0.4cm;
line-height : 1em;
color : #FFFFFF;
@@ -852,10 +754,10 @@ body { counter-reset : phb-page-numbers; }
}
}
//*****************************
// *****************************
// * PART COVER
// *****************************/
.page:has(.partCover) {
// *****************************/
.page:has(.partCover) {
padding-top : 0;
text-align : center;
columns : 1;
@@ -874,7 +776,7 @@ body { counter-reset : phb-page-numbers; }
h1 {
position : relative;
margin-top : 0.4cm;
font-family : "NodestoCapsCondensed";
font-family : 'NodestoCapsCondensed';
font-size : 2.3cm;
text-align : center;
text-transform : uppercase;
@@ -885,13 +787,13 @@ body { counter-reset : phb-page-numbers; }
margin-top : -0.7em;
margin-right : auto;
margin-left : auto;
font-family : "Overpass";
font-family : 'Overpass';
font-size : 0.45cm;
line-height : 1.1em;
}
}
//*****************************
// *****************************
// * TABLE OF CONTENTS
// *****************************/
.page {
@@ -960,34 +862,25 @@ body { counter-reset : phb-page-numbers; }
}
}
//*****************************
// *****************************
// * DEFINITION LISTS
// *****************************/
.page {
dl {
padding-left : 1em;
line-height : 1.25em;
white-space : pre-line;
& + * { margin-top : 0.28cm; }
& + * { margin-top : 0.17cm; }
}
dl + * { margin-top : 0.17cm; }
p + dl { margin-top : 0.17cm; }
dt {
display : inline;
margin-right : 5px;
margin-left : -1em;
}
dd {
display : inline;
margin-left : 0px;
text-indent : 0px;
}
}
//*****************************
// *****************************
// * WIDE
// *****************************/
.page .wide { margin-bottom : 0.325cm; }
.page .wide { margin-bottom : 0.325cm; }
.page h1 + * { margin-top : 0; }
@@ -1009,7 +902,7 @@ body { counter-reset : phb-page-numbers; }
outline : 1px solid #000000;
}
th {
font-family : "BookInsanityRemake";
font-family : 'BookInsanityRemake';
font-size : 0.45cm;
}
td { font-size : 0.7cm; }
@@ -1026,7 +919,7 @@ body { counter-reset : phb-page-numbers; }
}
}
}
//*****************************
// *****************************
// * INDEX
// *****************************/
.page {

View File

@@ -111,6 +111,21 @@ module.exports = [
icon : 'fas fa-code',
gen : '<!-- This is a comment that will not be rendered into your brew. Hotkey (Ctrl/Cmd + /). -->'
},
{
name : 'Homebrewery Credit',
icon : 'fas fa-dice-d20',
gen : function(){
return dedent`
{{homebreweryCredits
Made With
{{homebreweryIcon}}
The Homebrewery
[Homebrewery.Naturalcrit.com](https://homebrewery.naturalcrit.com)
}}\n\n`;
},
}
]
},
{
@@ -289,6 +304,99 @@ module.exports = [
}
]
},
/**************** FONTS *************/
{
groupName : 'Fonts',
icon : 'fas fa-keyboard',
view : 'text',
snippets : [
{
name : 'Open Sans',
icon : 'font OpenSans',
gen : dedent`{{font-family:OpenSans Dummy Text}}`
},
{
name : 'Code Bold',
icon : 'font CodeBold',
gen : dedent`{{font-family:CodeBold Dummy Text}}`
},
{
name : 'Code Light',
icon : 'font CodeLight',
gen : dedent`{{font-family:CodeLight Dummy Text}}`
},
{
name : 'Scaly Sans Remake',
icon : 'font ScalySansRemake',
gen : dedent`{{font-family:ScalySansRemake Dummy Text}}`
},
{
name : 'Book Insanity Remake',
icon : 'font BookInsanityRemake',
gen : dedent`{{font-family:BookInsanityRemake Dummy Text}}`
},
{
name : 'Mr Eaves Remake',
icon : 'font MrEavesRemake',
gen : dedent`{{font-family:MrEavesRemake Dummy Text}}`
},
{
name: 'Solbera Imitation Remake',
icon: 'font SolberaImitationRemake',
gen: dedent`{{font-family:SolberaImitationRemake Dummy Text}}`
},
{
name: 'Scaly Sans Small Caps Remake',
icon: 'font ScalySansSmallCapsRemake',
gen: dedent`{{font-family:ScalySansSmallCapsRemake Dummy Text}}`
},
{
name: 'Walter Turncoat',
icon: 'font WalterTurncoat',
gen: dedent`{{font-family:WalterTurncoat Dummy Text}}`
},
{
name: 'Lato',
icon: 'font Lato',
gen: dedent`{{font-family:Lato Dummy Text}}`
},
{
name: 'Courier',
icon: 'font Courier',
gen: dedent`{{font-family:Courier Dummy Text}}`
},
{
name: 'Nodesto Caps Condensed',
icon: 'font NodestoCapsCondensed',
gen: dedent`{{font-family:NodestoCapsCondensed Dummy Text}}`
},
{
name: 'Overpass',
icon: 'font Overpass',
gen: dedent`{{font-family:Overpass Dummy Text}}`
},
{
name: 'Davek',
icon: 'font Davek',
gen: dedent`{{font-family:Davek Dummy Text}}`
},
{
name: 'Iokharic',
icon: 'font Iokharic',
gen: dedent`{{font-family:Iokharic Dummy Text}}`
},
{
name: 'Rellanic',
icon: 'font Rellanic',
gen: dedent`{{font-family:Rellanic Dummy Text}}`
},
{
name: 'Times New Roman',
icon: 'font TimesNewRoman',
gen: dedent`{{font-family:"Times New Roman" Dummy Text}}`
}
]
},
/**************** PAGE *************/

View File

@@ -7,13 +7,9 @@
--HB_Color_WatercolorStain : #000000; // Black
}
@page { margin: 0; }
body {
counter-reset : phb-page-numbers;
}
*{
-webkit-print-color-adjust : exact;
}
@page { margin : 0; }
body { counter-reset : phb-page-numbers; }
* { -webkit-print-color-adjust : exact; }
//*****************************
// * MUSTACHE DIVS/SPANS
@@ -23,9 +19,7 @@ body {
break-inside : avoid;
display : inline-block;
width : 100%;
img {
z-index : 0;
}
img { z-index : 0; }
}
.inline-block {
display : inline-block;
@@ -33,99 +27,81 @@ body {
}
}
.useColumns(@multiplier : 1, @fillMode: auto){
.useColumns(@multiplier : 1, @fillMode: auto) {
column-fill : @fillMode;
column-count : 2;
}
.columnWrapper{
.columnWrapper {
column-gap : inherit;
max-height : 100%;
column-span : all;
columns : inherit;
column-gap : inherit;
column-fill : inherit;
}
.page{
.page {
.useColumns();
height : 279.4mm;
width : 215.9mm;
padding : 1.4cm 1.9cm 1.7cm;
counter-increment : phb-page-numbers;
background-color : var(--HB_Color_Background);
position : relative;
z-index : 15;
box-sizing : border-box;
width : 215.9mm;
height : 279.4mm;
padding : 1.4cm 1.9cm 1.7cm;
overflow : hidden;
counter-increment : phb-page-numbers;
background-color : var(--HB_Color_Background);
text-rendering : optimizeLegibility;
page-break-before : always;
page-break-after : always;
contain : size;
}
//*****************************
// * BASE
//*****************************
// * BASE
// *****************************/
.page{
p{
overflow-wrap : break-word;
.page {
p {
display : block;
overflow-wrap : break-word;
}
strong{
font-weight : bold;
}
em{
font-style : italic;
}
sup{
strong { font-weight : bold; }
em { font-style : italic; }
sup {
font-size : smaller;
line-height : 0;
vertical-align : super;
font-size : smaller;
line-height : 0;
}
sub{
vertical-align : sub;
sub {
font-size : smaller;
line-height : 0;
vertical-align : sub;
}
ul {
padding-left : 1.4em;
list-style-position : outside; //Needed for multiline list items
list-style-type : disc;
padding-left : 1.4em;
}
ol {
padding-left : 1.4em;
list-style-position : outside;
list-style-type : decimal;
padding-left : 1.4em;
}
img{
z-index : -1;
}
img { z-index : -1; }
//*****************************
// * HEADERS
// *****************************/
h1,h2,h3,h4,h5,h6{
h1,h2,h3,h4,h5,h6 {
font-weight : bold;
line-height : 1.2em;
}
h1{
font-size : 2em;
}
h2{
font-size : 1.5em;
}
h3{
font-size : 1.17em;
}
h4{
font-size : 1em;
}
h5{
font-size : 0.83em;
}
h1 { font-size : 2em; }
h2 { font-size : 1.5em; }
h3 { font-size : 1.17em; }
h4 { font-size : 1em; }
h5 { font-size : 0.83em; }
//*****************************
// * TABLE
// *****************************/
table{
table {
width : 100%;
thead{
thead {
display : table-row-group;
font-weight : bold;
}
@@ -137,42 +113,40 @@ body {
//************************************
// * CODE BLOCKS
// ************************************/
code{
font-family : "Courier New", Courier, monospace;
white-space : pre-wrap;
code {
font-family : 'Courier New', "Courier", monospace;
overflow-wrap : break-word;
white-space : pre-wrap;
}
pre code{
width : 100%;
pre code {
display : inline-block;
width : 100%;
}
//*****************************
// * EXTRAS
// *****************************/
.columnSplit {
margin-top : 0;
visibility : hidden;
-webkit-column-break-after : always;
break-after : always;
-moz-column-break-after : always;
margin-top : 0;
& + * {
margin-top : 0;
}
& + * { margin-top : 0; }
}
//Avoid breaking up
blockquote,table{
blockquote,table {
z-index : 15;
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
}
// Nested lists
ul ul,ol ol,ul ol,ol ul{
ul ul,ol ol,ul ol,ol ul {
margin-bottom : 0px;
margin-left : 1.5em;
}
li{
li {
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
@@ -180,69 +154,66 @@ body {
/* Watermark */
.watermark {
display : grid !important;
place-items : center;
justify-content : center;
position : absolute;
margin : 0;
top : 0;
left : 0;
z-index : 500;
display : grid !important;
place-items : center;
justify-content : center;
width : 100%;
height : 100%;
margin : 0;
font-size : 120px;
text-transform : uppercase;
text-transform : uppercase;
mix-blend-mode : overlay;
opacity : 30%;
transform : rotate(-45deg);
z-index : 500;
p {
margin-bottom : none;
}
p { margin-bottom : none; }
}
/* Watercolor */
[class*="watercolor"] {
[class*='watercolor'] {
position : absolute;
z-index : -2;
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 */
background-color : var(--HB_Color_WatercolorStain); /* default color */
background-size : cover;
-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 : var(--HB_Color_WatercolorStain); /*default color*/
--wc : @watercolor1; /*default image*/
z-index : -2;
--wc : @watercolor1; /* default image */
}
.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; }
.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; }
/* Image Masks */
[class*="imageMask"] {
[class*='imageMask'] {
position : absolute;
height : 200%;
width : 200%;
left : 50%;
bottom : 50%;
--rotation : 0;
--revealer : none;
--checkerboard : none;
--scaleX : 1;
--scaleY : 1;
left : 50%;
z-index : -1;
width : 200%;
height : 200%;
background-image : var(--checkerboard);
background-size : 20px;
transform : translateY(50%) translateX(-50%) rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
-webkit-mask-image : var(--wc), var(--revealer);
-webkit-mask-repeat : repeat-x;
-webkit-mask-size : 50%; //Scale only X to fit page width, leave height at aspect ratio, designed to hang off the edge
@@ -251,61 +222,63 @@ body {
mask-repeat : repeat-x;
mask-size : 50%;
mask-position : 50% calc(50% - var(--offset));
background-image : var(--checkerboard);
background-size : 20px;
z-index : -1;
transform : translateY(50%) translateX(-50%) rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
--rotation : 0;
--revealer : none;
--checkerboard : none;
--scaleX : 1;
--scaleY : 1;
& > p:has(img) {
position : absolute;
width : 50%;
height : 50%;
bottom : 50%;
left : 50%;
width : 50%;
height : 50%;
transform : translateX(-50%) translateY(50%) rotate(calc(-1deg * var(--rotation))) scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)));
}
& img {
position : absolute;
display : block;
bottom : 0;
display : block;
}
&.bottom {
--rotation : 0;
& img {bottom: 0;}
& img {bottom : 0;}
}
&.top {
--rotation : 180;
& img {top: 0;}
& img {top : 0;}
}
&.left {
--rotation : 90;
& img {left: 0;}
& img {left : 0;}
}
&.right {
--rotation : -90;
& img {right: 0;}
& img {right : 0;}
}
&.revealImage {
--revealer : linear-gradient(0deg, rgba(0,0,0,.2) 0%, rgba(0,0,0,0.2));
--checkerboard : url(/assets/waterColorMasks/missingImage.png); //shows any masked regions not filled by image
--revealer : linear-gradient(0deg, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.2));
--checkerboard : url("/assets/waterColorMasks/missingImage.png"); //shows any masked regions not filled by image
}
}
.imageMaskEdge {
&1 { --wc : url(/assets/waterColorMasks/edge/0001.webp); }
&2 { --wc : url(/assets/waterColorMasks/edge/0002.webp); }
&3 { --wc : url(/assets/waterColorMasks/edge/0003.webp); }
&4 { --wc : url(/assets/waterColorMasks/edge/0004.webp); }
&5 { --wc : url(/assets/waterColorMasks/edge/0005.webp); }
&6 { --wc : url(/assets/waterColorMasks/edge/0006.webp); }
&7 { --wc : url(/assets/waterColorMasks/edge/0007.webp); }
&8 { --wc : url(/assets/waterColorMasks/edge/0008.webp); }
&1 { --wc : url("/assets/waterColorMasks/edge/0001.webp"); }
&2 { --wc : url("/assets/waterColorMasks/edge/0002.webp"); }
&3 { --wc : url("/assets/waterColorMasks/edge/0003.webp"); }
&4 { --wc : url("/assets/waterColorMasks/edge/0004.webp"); }
&5 { --wc : url("/assets/waterColorMasks/edge/0005.webp"); }
&6 { --wc : url("/assets/waterColorMasks/edge/0006.webp"); }
&7 { --wc : url("/assets/waterColorMasks/edge/0007.webp"); }
&8 { --wc : url("/assets/waterColorMasks/edge/0008.webp"); }
}
[class*="imageMaskCenter"] {
[class*='imageMaskCenter'] {
bottom : calc(var(--offsetY));
left : calc(var(--offsetX));
width : 100%;
height : 100%;
left : calc(var(--offsetX));
bottom : calc(var(--offsetY));
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
-webkit-mask-image : var(--wc), var(--revealer);
-webkit-mask-repeat : no-repeat;
-webkit-mask-size : 100% 100%; //Scale both dimensions to fit page size
@@ -314,48 +287,48 @@ body {
mask-repeat : no-repeat;
mask-size : 100% 100%; //Scale both dimensions to fit page size
mask-position : 50% 50%;
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
& > p:has(img) {
position : absolute;
width : 100%;
height : 100%;
bottom : 0;
left : 0;
width : 100%;
height : 100%;
transform : unset;
transform : scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)))
rotate(calc(-1deg * var(--rotation)))
translateX(calc(-1 * var(--offsetX)))
translateY(calc(1 * var(--offsetY)));
rotate(calc(-1deg * var(--rotation)))
translateX(calc(-1 * var(--offsetX)))
translateY(calc(1 * var(--offsetY)));
}
}
.imageMaskCenter {
&1 { --wc : url(/assets/waterColorMasks/center/0001.webp); }
&2 { --wc : url(/assets/waterColorMasks/center/0002.webp); }
&3 { --wc : url(/assets/waterColorMasks/center/0003.webp); }
&4 { --wc : url(/assets/waterColorMasks/center/0004.webp); }
&5 { --wc : url(/assets/waterColorMasks/center/0005.webp); }
&6 { --wc : url(/assets/waterColorMasks/center/0006.webp); }
&7 { --wc : url(/assets/waterColorMasks/center/0007.webp); }
&8 { --wc : url(/assets/waterColorMasks/center/0008.webp); }
&9 { --wc : url(/assets/waterColorMasks/center/0009.webp); }
&10 { --wc : url(/assets/waterColorMasks/center/0010.webp); }
&11 { --wc : url(/assets/waterColorMasks/center/0011.webp); }
&12 { --wc : url(/assets/waterColorMasks/center/0012.webp); }
&13 { --wc : url(/assets/waterColorMasks/center/0013.webp); }
&14 { --wc : url(/assets/waterColorMasks/center/0014.webp); }
&15 { --wc : url(/assets/waterColorMasks/center/0015.webp); }
&16 { --wc : url(/assets/waterColorMasks/center/0016.webp); }
&special { --wc : url(/assets/waterColorMasks/center/special.webp); }
&1 { --wc : url("/assets/waterColorMasks/center/0001.webp"); }
&2 { --wc : url("/assets/waterColorMasks/center/0002.webp"); }
&3 { --wc : url("/assets/waterColorMasks/center/0003.webp"); }
&4 { --wc : url("/assets/waterColorMasks/center/0004.webp"); }
&5 { --wc : url("/assets/waterColorMasks/center/0005.webp"); }
&6 { --wc : url("/assets/waterColorMasks/center/0006.webp"); }
&7 { --wc : url("/assets/waterColorMasks/center/0007.webp"); }
&8 { --wc : url("/assets/waterColorMasks/center/0008.webp"); }
&9 { --wc : url("/assets/waterColorMasks/center/0009.webp"); }
&10 { --wc : url("/assets/waterColorMasks/center/0010.webp"); }
&11 { --wc : url("/assets/waterColorMasks/center/0011.webp"); }
&12 { --wc : url("/assets/waterColorMasks/center/0012.webp"); }
&13 { --wc : url("/assets/waterColorMasks/center/0013.webp"); }
&14 { --wc : url("/assets/waterColorMasks/center/0014.webp"); }
&15 { --wc : url("/assets/waterColorMasks/center/0015.webp"); }
&16 { --wc : url("/assets/waterColorMasks/center/0016.webp"); }
&special { --wc : url("/assets/waterColorMasks/center/special.webp"); }
}
[class*="imageMaskCorner"] {
height : 200%;
width : 200%;
left : calc(-50% + var(--offsetX));
[class*='imageMaskCorner'] {
bottom : calc(-50% + var(--offsetY));
left : calc(-50% + var(--offsetX));
width : 200%;
height : 200%;
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
-webkit-mask-image : var(--wc), var(--revealer);
-webkit-mask-repeat : no-repeat;
-webkit-mask-size : 100% 100%; //Scale both dimensions to fit page size
@@ -364,56 +337,55 @@ body {
mask-repeat : no-repeat;
mask-size : 100% 100%; //Scale both dimensions to fit page size
mask-position : 50% 50%;
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
& > p:has(img) {
bottom : 25%;
left : 25%;
width : 50%;
height : 50%; //Complex transform below to handle mix of % and cm offsets
left : 25%;
bottom : 25%;
transform : scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)))
rotate(calc(-1deg * var(--rotation)))
translateX(calc(-1 * var(--offsetX)))
translateY(calc(1 * var(--offsetY)));
rotate(calc(-1deg * var(--rotation)))
translateX(calc(-1 * var(--offsetX)))
translateY(calc(1 * var(--offsetY)));
}
}
.imageMaskCorner {
&1 { --wc : url(/assets/waterColorMasks/corner/0001.webp); }
&2 { --wc : url(/assets/waterColorMasks/corner/0002.webp); }
&3 { --wc : url(/assets/waterColorMasks/corner/0003.webp); }
&4 { --wc : url(/assets/waterColorMasks/corner/0004.webp); }
&5 { --wc : url(/assets/waterColorMasks/corner/0005.webp); }
&6 { --wc : url(/assets/waterColorMasks/corner/0006.webp); }
&7 { --wc : url(/assets/waterColorMasks/corner/0007.webp); }
&8 { --wc : url(/assets/waterColorMasks/corner/0008.webp); }
&9 { --wc : url(/assets/waterColorMasks/corner/0009.webp); }
&10 { --wc : url(/assets/waterColorMasks/corner/0010.webp); }
&11 { --wc : url(/assets/waterColorMasks/corner/0011.webp); }
&12 { --wc : url(/assets/waterColorMasks/corner/0012.webp); }
&13 { --wc : url(/assets/waterColorMasks/corner/0013.webp); }
&14 { --wc : url(/assets/waterColorMasks/corner/0014.webp); }
&15 { --wc : url(/assets/waterColorMasks/corner/0015.webp); }
&16 { --wc : url(/assets/waterColorMasks/corner/0016.webp); }
&17 { --wc : url(/assets/waterColorMasks/corner/0017.webp); }
&18 { --wc : url(/assets/waterColorMasks/corner/0018.webp); }
&19 { --wc : url(/assets/waterColorMasks/corner/0019.webp); }
&20 { --wc : url(/assets/waterColorMasks/corner/0020.webp); }
&21 { --wc : url(/assets/waterColorMasks/corner/0021.webp); }
&22 { --wc : url(/assets/waterColorMasks/corner/0022.webp); }
&23 { --wc : url(/assets/waterColorMasks/corner/0023.webp); }
&24 { --wc : url(/assets/waterColorMasks/corner/0024.webp); }
&25 { --wc : url(/assets/waterColorMasks/corner/0025.webp); }
&26 { --wc : url(/assets/waterColorMasks/corner/0026.webp); }
&27 { --wc : url(/assets/waterColorMasks/corner/0027.webp); }
&28 { --wc : url(/assets/waterColorMasks/corner/0028.webp); }
&29 { --wc : url(/assets/waterColorMasks/corner/0029.webp); }
&30 { --wc : url(/assets/waterColorMasks/corner/0030.webp); }
&31 { --wc : url(/assets/waterColorMasks/corner/0031.webp); }
&32 { --wc : url(/assets/waterColorMasks/corner/0032.webp); }
&33 { --wc : url(/assets/waterColorMasks/corner/0033.webp); }
&34 { --wc : url(/assets/waterColorMasks/corner/0034.webp); }
&35 { --wc : url(/assets/waterColorMasks/corner/0035.webp); }
&36 { --wc : url(/assets/waterColorMasks/corner/0036.webp); }
&37 { --wc : url(/assets/waterColorMasks/corner/0037.webp); }
&1 { --wc : url("/assets/waterColorMasks/corner/0001.webp"); }
&2 { --wc : url("/assets/waterColorMasks/corner/0002.webp"); }
&3 { --wc : url("/assets/waterColorMasks/corner/0003.webp"); }
&4 { --wc : url("/assets/waterColorMasks/corner/0004.webp"); }
&5 { --wc : url("/assets/waterColorMasks/corner/0005.webp"); }
&6 { --wc : url("/assets/waterColorMasks/corner/0006.webp"); }
&7 { --wc : url("/assets/waterColorMasks/corner/0007.webp"); }
&8 { --wc : url("/assets/waterColorMasks/corner/0008.webp"); }
&9 { --wc : url("/assets/waterColorMasks/corner/0009.webp"); }
&10 { --wc : url("/assets/waterColorMasks/corner/0010.webp"); }
&11 { --wc : url("/assets/waterColorMasks/corner/0011.webp"); }
&12 { --wc : url("/assets/waterColorMasks/corner/0012.webp"); }
&13 { --wc : url("/assets/waterColorMasks/corner/0013.webp"); }
&14 { --wc : url("/assets/waterColorMasks/corner/0014.webp"); }
&15 { --wc : url("/assets/waterColorMasks/corner/0015.webp"); }
&16 { --wc : url("/assets/waterColorMasks/corner/0016.webp"); }
&17 { --wc : url("/assets/waterColorMasks/corner/0017.webp"); }
&18 { --wc : url("/assets/waterColorMasks/corner/0018.webp"); }
&19 { --wc : url("/assets/waterColorMasks/corner/0019.webp"); }
&20 { --wc : url("/assets/waterColorMasks/corner/0020.webp"); }
&21 { --wc : url("/assets/waterColorMasks/corner/0021.webp"); }
&22 { --wc : url("/assets/waterColorMasks/corner/0022.webp"); }
&23 { --wc : url("/assets/waterColorMasks/corner/0023.webp"); }
&24 { --wc : url("/assets/waterColorMasks/corner/0024.webp"); }
&25 { --wc : url("/assets/waterColorMasks/corner/0025.webp"); }
&26 { --wc : url("/assets/waterColorMasks/corner/0026.webp"); }
&27 { --wc : url("/assets/waterColorMasks/corner/0027.webp"); }
&28 { --wc : url("/assets/waterColorMasks/corner/0028.webp"); }
&29 { --wc : url("/assets/waterColorMasks/corner/0029.webp"); }
&30 { --wc : url("/assets/waterColorMasks/corner/0030.webp"); }
&31 { --wc : url("/assets/waterColorMasks/corner/0031.webp"); }
&32 { --wc : url("/assets/waterColorMasks/corner/0032.webp"); }
&33 { --wc : url("/assets/waterColorMasks/corner/0033.webp"); }
&34 { --wc : url("/assets/waterColorMasks/corner/0034.webp"); }
&35 { --wc : url("/assets/waterColorMasks/corner/0035.webp"); }
&36 { --wc : url("/assets/waterColorMasks/corner/0036.webp"); }
&37 { --wc : url("/assets/waterColorMasks/corner/0037.webp"); }
}
}
@@ -425,16 +397,16 @@ body {
padding-left : 1em;
white-space : pre-line;
}
dt {
display : inline;
margin-right : 0.5ch;
dt {
display : inline;
margin-right : 0.5ch;
margin-left : -1em;
}
dd {
}
dd {
display : inline;
margin-left : 0;
text-indent : 0;
}
}
}
//*****************************
@@ -444,9 +416,7 @@ body {
.blank {
height : 1em;
margin-top : 0;
& + * {
margin-top : 0;
}
& + * { margin-top : 0; }
}
}
@@ -454,12 +424,39 @@ body {
// * WIDE
// *****************************/
.page {
.wide{
.wide {
column-span : all;
display : block;
margin-bottom : 1em;
&+* {
margin-top : 0;
}
& + * { margin-top : 0; }
}
}
//*****************************
//* CREDITS
//*****************************/
.page .homebreweryCredits {
p {
font-family : 'NodestoCapsWide';
font-size : 0.4cm;
line-height : 1em;
text-align : center;
text-indent : 0;
letter-spacing : 0.08em;
}
a {
color : inherit;
text-decoration : none;
&:hover { text-decoration : underline; }
}
.homebreweryIcon {
display : block;
height : 1.5cm;
margin : 0 auto;
background-color : black;
-webkit-mask : url("/assets/naturalCritLogoWhite.svg") center / contain no-repeat;
mask : url("/assets/naturalCritLogoWhite.svg") center / contain no-repeat;
}
.homebreweryIcon.red { background-color : red; }
.homebreweryIcon.gold { background-image : linear-gradient(to top left, brown 22.5%, gold 40%, white 60%, gold 67.5%, brown 82.5%); }
}

View File

@@ -0,0 +1,129 @@
/* Main BG color and normal text color */
.CodeMirror {
background: #293134;
color: #91A6AA;
}
/* Brew BG */
.brewRenderer {
background-color: #293134;
}
/* Blinking cursor */
.CodeMirror-cursor {
border-left: 1px solid #e0e2e4;
}
/* HB DARK NAV START*/
/* Bars at the top */
.snippetBar {
background-color: #2F393C;
color: white;
}
nav {
background-color: #293134;
}
nav .navItem {
background-color: #293134;
}
/* Fix for Homebrewery custom Snippet icons */
.snippetBar .fac {
filter: invert(1);
}
.snippetBar .snippetGroup .dropdown {
background-color: #2F393C;
}
/* HB DARK NAV END */
/* Line number stuff */
.CodeMirror-gutter-elt {
color: #81969A;
}
.CodeMirror-linenumber {
background-color: #293134;
}
.CodeMirror-gutter {
background-color: #293134;
}
/* column splits */
.editor .codeEditor .columnSplit {
font-style: italic;
color: inherit;
background-color:#1f5763;
border-bottom: #299 solid 1px;
}
/* Colors for headings and such */
/* ###Headings */
.cm-s-default .cm-header {
color: #c51b1b;
-webkit-text-stroke-width: 0.1px;
-webkit-text-stroke-color: #000;
}
/* bold points */
.cm-header, .cm-strong {
font-weight: bold;
color: #309dd2;
}
/* Link headings */
.cm-s-default .cm-link {
color: #dd6300;
}
/* links */
.cm-s-default .cm-string {
color: #aa8261;
}
/*@import*/
.cm-s-default .cm-def {
color:#2986cc;
}
/* Bullets and such */
.cm-s-default .cm-variable-2 {
color: #3cbf30;
}
/* blocks */
.editor .codeEditor .block:not(.cm-comment) {
color: #e3e3e3;
}
/* inline blocks */
.editor .codeEditor .inline-block {
color: #e3e3e3;
}
/* Tags (divs) */
.cm-s-default .cm-tag {
color: #e3ff00;
}
.cm-s-default .cm-attribute {
color: #e3ff00;
}
.cm-s-default .cm-atom {
color:#000;
}
.cm-s-default .cm-qualifier{
color:#ee1919;
}
.cm-s-default .cm-comment{
color:#bbc700;
}
.cm-s-default .cm-keyword {
color:#c302df;
background-color:#b1b1b1;
}
.cm-s-default .cm-property.cm-error {
color:#c50202;
}
.CodeMirror-foldmarker {
color:#f0ff00;
}
/* New page */
.editor .codeEditor .pageLine {
background: #000;
color:#000;
border-bottom: 1px solid #fff;
}
.cm-s-default .cm-builtin {
color:#fff;
}

View File

@@ -15,6 +15,7 @@
"cobalt",
"colorforth",
"darcula",
"darkbrewery-v301",
"dracula",
"duotone-dark",
"duotone-light",

View File

@@ -0,0 +1,118 @@
/*
Icon Font: dicefont
*/
@font-face {
font-family: 'DiceFont';
src: url('../../../fonts/5e/dicefont.woff2') format('woff2'),
url('../../../fonts/5e/dicefont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
.df {
display: inline-block;
font-family: 'DiceFont';
font-style: normal;
font-weight: normal;
font-variant: normal;
line-height: 1;
text-decoration: inherit;
text-rendering: optimizeLegibility;
text-transform: none;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-smooth: antialiased;
&.F:before { content: '\f190'; }
&.F-minus:before { content: '\f191'; }
&.F-plus:before { content: '\f192'; }
&.F-zero:before { content: '\f193'; }
&.d10:before { content: '\f194'; }
&.d10-0:before { content: '\f100'; }
&.d10-1:before { content: '\f101'; }
&.d10-10:before { content: '\f102'; }
&.d10-2:before { content: '\f103'; }
&.d10-3:before { content: '\f104'; }
&.d10-4:before { content: '\f105'; }
&.d10-5:before { content: '\f106'; }
&.d10-6:before { content: '\f107'; }
&.d10-7:before { content: '\f108'; }
&.d10-8:before { content: '\f109'; }
&.d10-9:before { content: '\f10a'; }
&.d12:before { content: '\f195'; }
&.d12-1:before { content: '\f10b'; }
&.d12-10:before { content: '\f10c'; }
&.d12-11:before { content: '\f10d'; }
&.d12-12:before { content: '\f10e'; }
&.d12-2:before { content: '\f10f'; }
&.d12-3:before { content: '\f110'; }
&.d12-4:before { content: '\f111'; }
&.d12-5:before { content: '\f112'; }
&.d12-6:before { content: '\f113'; }
&.d12-7:before { content: '\f114'; }
&.d12-8:before { content: '\f115'; }
&.d12-9:before { content: '\f116'; }
&.d2:before { content: '\f196'; }
&.d2-1:before { content: '\f117'; }
&.d2-2:before { content: '\f118'; }
&.d20:before { content: '\f197'; }
&.d20-1:before { content: '\f119'; }
&.d20-10:before { content: '\f11a'; }
&.d20-11:before { content: '\f11b'; }
&.d20-12:before { content: '\f11c'; }
&.d20-13:before { content: '\f11d'; }
&.d20-14:before { content: '\f11e'; }
&.d20-15:before { content: '\f11f'; }
&.d20-16:before { content: '\f120'; }
&.d20-17:before { content: '\f121'; }
&.d20-18:before { content: '\f122'; }
&.d20-19:before { content: '\f123'; }
&.d20-2:before { content: '\f124'; }
&.d20-20:before { content: '\f125'; }
&.d20-3:before { content: '\f126'; }
&.d20-4:before { content: '\f127'; }
&.d20-5:before { content: '\f128'; }
&.d20-6:before { content: '\f129'; }
&.d20-7:before { content: '\f12a'; }
&.d20-8:before { content: '\f12b'; }
&.d20-9:before { content: '\f12c'; }
&.d4:before { content: '\f198'; }
&.d4-1:before { content: '\f12d'; }
&.d4-2:before { content: '\f12e'; }
&.d4-3:before { content: '\f12f'; }
&.d4-4:before { content: '\f130'; }
&.d6:before { content: '\f199'; }
&.d6-1:before { content: '\f131'; }
&.d6-2:before { content: '\f132'; }
&.d6-3:before { content: '\f133'; }
&.d6-4:before { content: '\f134'; }
&.d6-5:before { content: '\f135'; }
&.d6-6:before { content: '\f136'; }
&.d8:before { content: '\f19a'; }
&.d8-1:before { content: '\f137'; }
&.d8-2:before { content: '\f138'; }
&.d8-3:before { content: '\f139'; }
&.d8-4:before { content: '\f13a'; }
&.d8-5:before { content: '\f13b'; }
&.d8-6:before { content: '\f13c'; }
&.d8-7:before { content: '\f13d'; }
&.d8-8:before { content: '\f13e'; }
&.dot-d6:before { content: '\f19b'; }
&.dot-d6-1:before { content: '\f13f'; }
&.dot-d6-2:before { content: '\f140'; }
&.dot-d6-3:before { content: '\f141'; }
&.dot-d6-4:before { content: '\f142'; }
&.dot-d6-5:before { content: '\f143'; }
&.dot-d6-6:before { content: '\f18f'; }
&.small-dot-d6-1:before { content: '\f183'; }
&.small-dot-d6-2:before { content: '\f184'; }
&.small-dot-d6-3:before { content: '\f185'; }
&.small-dot-d6-4:before { content: '\f186'; }
&.small-dot-d6-5:before { content: '\f187'; }
&.small-dot-d6-6:before { content: '\f188'; }
&.solid-small-dot-d6-1:before { content: '\f189'; }
&.solid-small-dot-d6-2:before { content: '\f18a'; }
&.solid-small-dot-d6-3:before { content: '\f18b'; }
&.solid-small-dot-d6-4:before { content: '\f18c'; }
&.solid-small-dot-d6-5:before { content: '\f18d'; }
&.solid-small-dot-d6-6:before { content: '\f18e'; }
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,18 @@
# License
DiceFont is open source. You can use it for commercial projects, personal
projects or open source projects.
## Font License
Applies to all desktop and webfont files: [License: SIL OFL 1.1](http://scripts.sil.org/OFL)
## Code License
Applies to all CSS and LESS files: [License: MIT License](http://opensource.org/licenses/mit-license.html)
## Documentation License
Applies to all other files [CC BY 3.0](http://creativecommons.org/licenses/by/3.0/)
Copyright [Franco Ponticelli](https://github.com/fponticelli).

View File

@@ -1,3 +1,5 @@
@import url('./dicefont.less');
/* Main Font, serif */
@font-face {
font-family: BookInsanityRemake;

Binary file not shown.

View File

@@ -0,0 +1,224 @@
/* Main Font, serif */
@font-face {
font-family : 'Eldeberry-Inn';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/icon fonts/Elderberry-Inn-Icons.woff2');
}
.page {
span.ei {
display : inline-block;
margin-right : 3px;
font-family : 'Eldeberry-Inn';
line-height : 1;
vertical-align : baseline;
-moz-osx-font-smoothing : grayscale;
-webkit-font-smoothing : antialiased;
text-rendering : auto;
&.book::before { content : '\E900'; }
&.screen::before { content : '\E901'; }
/* Spell levels */
&.spell-0::before { content : '\E902'; }
&.spell-1::before { content : '\E903'; }
&.spell-2::before { content : '\E904'; }
&.spell-3::before { content : '\E905'; }
&.spell-4::before { content : '\E906'; }
&.spell-5::before { content : '\E907'; }
&.spell-6::before { content : '\E908'; }
&.spell-7::before { content : '\E909'; }
&.spell-8::before { content : '\E90A'; }
&.spell-9::before { content : '\E90B'; }
/* Damage types */
&.acid::before { content : '\E90C'; }
&.bludgeoning::before { content : '\E90D'; }
&.cold::before { content : '\E90E'; }
&.fire::before { content : '\E90F'; }
&.force::before { content : '\E910'; }
&.lightning::before { content : '\E911'; }
&.necrotic::before { content : '\E912'; }
&.piercing::before { content : '\E914'; }
&.poison::before { content : '\E913'; }
&.psychic::before { content : '\E915'; }
&.radiant::before { content : '\E916'; }
&.slashing::before { content : '\E917'; }
&.thunder::before { content : '\E918'; }
/* DnD Conditions */
&.blinded::before { content : '\E919'; }
&.charmed::before { content : '\E91A'; }
&.deafened::before { content : '\E91B'; }
&.exhaust-1::before { content : '\E91C'; }
&.exhaust-2::before { content : '\E91D'; }
&.exhaust-3::before { content : '\E91E'; }
&.exhaust-4::before { content : '\E91F'; }
&.exhaust-5::before { content : '\E920'; }
&.exhaust-6::before { content : '\E921'; }
&.frightened::before { content : '\E922'; }
&.grappled::before { content : '\E923'; }
&.incapacitated::before { content : '\E924'; }
&.invisible::before { content : '\E926'; }
&.paralyzed::before { content : '\E927'; }
&.petrified::before { content : '\E928'; }
&.poisoned::before { content : '\E929'; }
&.prone::before { content : '\E92A'; }
&.restrained::before { content : '\E92B'; }
&.stunned::before { content : '\E92C'; }
&.unconscious::before { content : '\E925'; }
/* Character Classes and Features */
&.barbarian-rage::before { content : '\E92D'; }
&.barbarian-reckless-attack::before { content : '\E92E'; }
&.bardic-inspiration::before { content : '\E92F'; }
&.cleric-channel-divinity::before { content : '\E930'; }
&.druid-wild-shape::before { content : '\E931'; }
&.fighter-action-surge::before { content : '\E932'; }
&.fighter-second-wind::before { content : '\E933'; }
&.monk-flurry-blows::before { content : '\E934'; }
&.monk-patient-defense::before { content : '\E935'; }
&.monk-step-of-the-wind::before { content : '\E936'; }
&.monk-step-of-the-wind-2::before { content : '\E937'; }
&.monk-step-of-the-wind-3::before { content : '\E938'; }
&.monk-stunning-strike::before { content : '\E939'; }
&.monk-stunning-strike-2::before { content : '\E939'; }
&.paladin-divine-smite::before { content : '\E93B'; }
&.paladin-lay-on-hands::before { content : '\E93C'; }
&.barbarian-abilities::before { content : '\E93D'; }
&.barbarian::before { content : '\E93E'; }
&.bard-abilities::before { content : '\E93F'; }
&.bard::before { content : '\E940'; }
&.cleric-abilities::before { content : '\E941'; }
&.cleric::before { content : '\E942'; }
&.druid-abilities::before { content : '\E943'; }
&.druid::before { content : '\E944'; }
&.fighter-abilities::before { content : '\E945'; }
&.fighter::before { content : '\E946'; }
&.monk-abilities::before { content : '\E947'; }
&.monk::before { content : '\E948'; }
&.paladin-abilities::before { content : '\E949'; }
&.paladin::before { content : '\E94A'; }
&.ranger-abilities::before { content : '\E94B'; }
&.ranger::before { content : '\E94C'; }
&.rogue-abilities::before { content : '\E94D'; }
&.rogue::before { content : '\E94E'; }
&.sorcerer-abilities::before { content : '\E94F'; }
&.sorcerer::before { content : '\E950'; }
&.warlock-abilities::before { content : '\E951'; }
&.warlock::before { content : '\E952'; }
&.wizard-abilities::before { content : '\E953'; }
&.wizard::before { content : '\E954'; }
/* Types of actions */
&.movement::before { content : '\E955'; }
&.action::before { content : '\E956'; }
&.bonus-action::before { content : '\E957'; }
&.reaction::before { content : '\E958'; }
/* SRD Spells */
&.acid-arrow::before { content : '\E959'; }
&.action-1::before { content : '\E95A'; }
&.alter-self::before { content : '\E95B'; }
&.alter-self-2::before { content : '\E95C'; }
&.animal-friendship::before { content : '\E95E'; }
&.animate-dead::before { content : '\E95F'; }
&.animate-objects::before { content : '\E960'; }
&.animate-objects-2::before { content : '\E961'; }
&.bane::before { content : '\E962'; }
&.bless::before { content : '\E963'; }
&.blur::before { content : '\E964'; }
&.bonus::before { content : '\E965'; }
&.branding-smite::before { content : '\E966'; }
&.burning-hands::before { content : '\E967'; }
&.charm-person::before { content : '\E968'; }
&.chill-touch::before { content : '\E969'; }
&.cloudkill::before { content : '\E96A'; }
&.comprehend-languages::before { content : '\E96B'; }
&.cone-of-cold::before { content : '\E96C'; }
&.conjure-elemental::before { content : '\E96D'; }
&.conjure-minor-elemental::before { content : '\E96E'; }
&.control-water::before { content : '\E96F'; }
&.counterspell::before { content : '\E970'; }
&.cure-wounds::before { content : '\E971'; }
&.dancing-lights::before { content : '\E972'; }
&.darkness::before { content : '\E973'; }
&.detect-magic::before { content : '\E974'; }
&.disguise-self::before { content : '\E975'; }
&.disintegrate::before { content : '\E976'; }
&.dispel-evil-and-good::before { content : '\E977'; }
&.dispel-magic::before { content : '\E978'; }
&.dominate-monster::before { content : '\E979'; }
&.dominate-person::before { content : '\E97A'; }
&.eldritch-blast::before { content : '\E97B'; }
&.enlarge-reduce::before { content : '\E97C'; }
&.entangle::before { content : '\E97D'; }
&.faerie-fire::before { content : '\E97E'; }
&.faerie-fire2::before { content : '\E97F'; }
&.feather-fall::before { content : '\E980'; }
&.find-familiar::before { content : '\E981'; }
&.finger-of-death::before { content : '\E982'; }
&.fireball::before { content : '\E983'; }
&.floating-disk::before { content : '\E984'; }
&.fly::before { content : '\E985'; }
&.fog-cloud::before { content : '\E986'; }
&.gaseous-form::before { content : '\E987'; }
&.gaseous-form2::before { content : '\E988'; }
&.gentle-repose::before { content : '\E989'; }
&.gentle-repose2::before { content : '\E98A'; }
&.globe-of-invulnerability::before { content : '\E98B'; }
&.guiding-bolt::before { content : '\E98C'; }
&.healing-word::before { content : '\E98D'; }
&.heat-metal::before { content : '\E98E'; }
&.hellish-rebuke::before { content : '\E98F'; }
&.heroes-feast::before { content : '\E990'; }
&.heroism::before { content : '\E991'; }
&.hideous-laughter::before { content : '\E992'; }
&.identify::before { content : '\E993'; }
&.illusory-script::before { content : '\E994'; }
&.inflict-wounds::before { content : '\E995'; }
&.light::before { content : '\E996'; }
&.longstrider::before { content : '\E997'; }
&.mage-armor::before { content : '\E998'; }
&.mage-hand::before { content : '\E999'; }
&.magic-missile::before { content : '\E99A'; }
&.mass-cure-wounds::before { content : '\E99B'; }
&.mass-healing-word::before { content : '\E99C'; }
&.Mending::before { content : '\E99D'; }
&.message::before { content : '\E99E'; }
&.Minor-illusion::before { content : '\E99F'; }
&.movement1::before { content : '\E9A0'; }
&.polymorph::before { content : '\E9A1'; }
&.power-word-kill::before { content : '\E9A2'; }
&.power-word-stun::before { content : '\E9A3'; }
&.prayer-of-healing::before { content : '\E9A4'; }
&.prestidigitation::before { content : '\E9A5'; }
&.protection-from-evil-and-good::before { content : '\E9A6'; }
&.raise-read::before { content : '\E9A7'; }
&.raise-read2::before { content : '\E9A8'; }
&.reaction1::before { content : '\E9A9'; }
&.resurrection::before { content : '\E9AA'; }
&.resurrection2::before { content : '\E9AB'; }
&.revivify::before { content : '\E9AC'; }
&.revivify2::before { content : '\E9AD'; }
&.sacred-flame::before { content : '\E9AE'; }
&.sanctuary::before { content : '\E9AF'; }
&.scorching-ray::before { content : '\E9B0'; }
&.sending::before { content : '\E9B1'; }
&.shatter::before { content : '\E9B2'; }
&.shield::before { content : '\E9B3'; }
&.silent-image::before { content : '\E9B4'; }
&.sleep::before { content : '\E9B5'; }
&.speak-with-animals::before { content : '\E9B6'; }
&.telekinesis::before { content : '\E9B7'; }
&.true-strike::before { content : '\E9B8'; }
&.vicious-mockery::before { content : '\E9B9'; }
&.wall-of-fire::before { content : '\E9BA'; }
&.wall-of-force::before { content : '\E9BB'; }
&.wall-of-ice::before { content : '\E9BC'; }
&.wall-of-stone::before { content : '\E9BD'; }
&.wall-of-thorns::before { content : '\E9BE'; }
&.wish::before { content : '\E9BF'; }
}
}