mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2025-12-25 16:12:37 +00:00
Merge branch 'master' of https://github.com/naturalcrit/homebrewery into small-fixes,-snippet-uniformity
This commit is contained in:
@@ -64,6 +64,12 @@ jobs:
|
||||
- run:
|
||||
name: Test - Mustache Spans
|
||||
command: npm run test:mustache-syntax
|
||||
- run:
|
||||
name: Test - Definition Lists
|
||||
command: npm run test:definition-lists
|
||||
- run:
|
||||
name: Test - Variables
|
||||
command: npm run test:variables
|
||||
- run:
|
||||
name: Test - Routes
|
||||
command: npm run test:route
|
||||
|
||||
73
changelog.md
73
changelog.md
@@ -84,7 +84,70 @@ pre {
|
||||
## changelog
|
||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||
|
||||
### Wednesday 21/2/2024 - v3.11.0
|
||||
### Monday 18/3/2024 - v3.12.0
|
||||
{{taskList
|
||||
|
||||
##### 5e-Cleric
|
||||
|
||||
* [x] Fix language-specific hyphenation on print page
|
||||
|
||||
Fixes issue [#3294](https://github.com/naturalcrit/homebrewery/issues/3294)
|
||||
|
||||
* [x] Upgrade Font-Awesome to v6.51
|
||||
|
||||
* [x] Allow downloaded files to be uploaded via {{openSans **NEW {{fa,fa-plus-square}} → FROM UPLOAD {{fa,fa-upload}}**}}
|
||||
|
||||
##### G-Ambatte
|
||||
|
||||
* [x] Fix an edge case crash with empty documents
|
||||
|
||||
Fixes issue [#3315](https://github.com/naturalcrit/homebrewery/issues/3315)
|
||||
|
||||
* [x] Brews on the user page can be searched by tag; clicking a tag adds it to the filter
|
||||
|
||||
Fixes issue [#3164](https://github.com/naturalcrit/homebrewery/issues/3164)
|
||||
|
||||
* [x] Add *DiceFont* icons {{df,d20-20}} `{{df,icon-name}}`
|
||||
|
||||
##### abquintic
|
||||
|
||||
* [x] Fix ^super^ and ^^sub^^ highlighting in the text editor
|
||||
|
||||
* [x] Add new syntax for multiline Definition Lists:
|
||||
|
||||
|
||||
```
|
||||
Term
|
||||
::Definition 1
|
||||
::Definition 2
|
||||
with more text
|
||||
```
|
||||
|
||||
produces:
|
||||
|
||||
Term
|
||||
::Definition 1
|
||||
::Definition 2
|
||||
with more text
|
||||
|
||||
Fixes issue [#2340](https://github.com/naturalcrit/homebrewery/issues/2340)
|
||||
|
||||
##### RKuerten :
|
||||
* [x] Fix monster stat block backgrounds on print page
|
||||
|
||||
Fixes issue [#3275](https://github.com/naturalcrit/homebrewery/issues/3275)
|
||||
|
||||
* [x] Added new text editor theme: "Darkvision".
|
||||
|
||||
##### calculuschild, G-Ambatte, 5e-Cleric
|
||||
|
||||
* [x] Codebase and UI cleanup
|
||||
}}
|
||||
|
||||
\page
|
||||
|
||||
|
||||
### Friday 21/2/2024 - v3.11.0
|
||||
{{taskList
|
||||
|
||||
##### Gazook89
|
||||
@@ -166,14 +229,16 @@ Fixes issue [1488](https://github.com/naturalcrit/homebrewery/issues/1488)
|
||||
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.
|
||||
* [x] Brew Variables
|
||||
}}
|
||||
|
||||
\
|
||||
|
||||
{{wide
|
||||
|
||||
### Brew Variable Syntax
|
||||
|
||||
You may already be familiar with `[link](url)` and `` syntax. We have expanded this to include a third `$[variable](text)` syntax. All three of these syntaxes now share a common set of features:
|
||||
You may already be familiar with `[link](url)` and `` synax. 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 |
|
||||
@@ -1512,7 +1577,7 @@ myStyle {color: black}
|
||||
### Sunday, 29/05/2016 - v2.1.0
|
||||
- Finally added a syntax for doing spell lists. A bit in-depth about why this took so long. Essentially I'm running out of syntax to use in stardard Markdown. There are too many unique elements in the PHB-style to be mapped. I solved this earlier by stacking certain elements together (eg. an `<hr>` before a `blockquote` turns it into moster state block), but those are getting unweildly. I would like to simply wrap these in `div`s with classes, but unfortunately Markdown stops processing when within HTML blocks. To get around this I wrote my own override to the Markdown parser and lexer to process Markdown within a simple div class wrapper. This should open the door for more unique syntaxes in the future. Big step!
|
||||
- Override Ctrl+P (and cmd+P) to launch to the print page. Many people try to just print either the editing or share page to get a PDF. While this dones;t make much sense, I do get a ton of issues about it. So now if you try to do this, it'll just bring you imediately to the print page. Everybody wins!
|
||||
- The onboarding flow has also been confusing a few users (Homepage -> new -> save -> edit page). If you edit the Homepage text now, a Call to Action to save your work will pop-up.
|
||||
- The onboarding flow has also been confusing a few users (Homepage → new → save → edit page). If you edit the Homepage text now, a Call to Action to save your work will pop-up.
|
||||
- Added a 'Recently Edited' and 'Recently Viewed' nav item to the edit and share page respectively. Each will remember the last 8 items you edited or viewed and when you viewed it. Makes use of the new title attribute of brews to easy navigatation.
|
||||
- Paragraphs now indent properly after lists (thanks u/slitjen!)
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ const PAGE_HEIGHT = 1056;
|
||||
|
||||
const INITIAL_CONTENT = dedent`
|
||||
<!DOCTYPE html><html><head>
|
||||
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
||||
<link href="//use.fontawesome.com/releases/v6.5.1/css/all.css" rel="stylesheet" type="text/css" />
|
||||
<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' />
|
||||
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
|
||||
<base target=_blank>
|
||||
</head><body style='overflow: hidden'><div></div></body></html>`;
|
||||
|
||||
@@ -211,11 +211,11 @@ const BrewRenderer = (props)=>{
|
||||
<RenderWarnings />
|
||||
<NotificationPopup />
|
||||
</div>
|
||||
<link href={`/themes/${rendererPath}/Blank/style.css`} rel='stylesheet'/>
|
||||
<link href={`/themes/${rendererPath}/Blank/style.css`} type="text/css" rel='stylesheet'/>
|
||||
{baseThemePath &&
|
||||
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
|
||||
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} type="text/css" rel='stylesheet'/>
|
||||
}
|
||||
<link href={`/themes/${rendererPath}/${themePath}/style.css`} rel='stylesheet'/>
|
||||
<link href={`/themes/${rendererPath}/${themePath}/style.css`} type="text/css" rel='stylesheet'/>
|
||||
|
||||
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||
{state.isMounted
|
||||
|
||||
@@ -14,6 +14,15 @@
|
||||
box-shadow : 1px 4px 14px #000000;
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 20px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
width:20px;
|
||||
background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
|
||||
}
|
||||
|
||||
}
|
||||
.pane { position : relative; }
|
||||
.pageInfo {
|
||||
|
||||
@@ -151,30 +151,37 @@ const Editor = createClass({
|
||||
|
||||
// definition lists
|
||||
if(line.includes('::')){
|
||||
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
|
||||
if(/^:*$/.test(line) == true){ return };
|
||||
const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error.
|
||||
let match;
|
||||
while ((match = regex.exec(line)) != null){
|
||||
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[0]) }, { line: lineNumber, ch: line.indexOf(match[0]) + match[0].length }, { className: 'define' });
|
||||
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'term' });
|
||||
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[2]) }, { line: lineNumber, ch: line.indexOf(match[2]) + match[2].length }, { className: 'definition' });
|
||||
codeMirror.markText({ line: lineNumber, ch: match.indices[0][0] }, { line: lineNumber, ch: match.indices[0][1] }, { className: 'dl-highlight' });
|
||||
codeMirror.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' });
|
||||
codeMirror.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' });
|
||||
const ddIndex = match.indices[2][0];
|
||||
let colons = /::/g;
|
||||
let colonMatches = colons.exec(match[2]);
|
||||
if(colonMatches !== null){
|
||||
codeMirror.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight'} )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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' });
|
||||
// Subscript & Superscript
|
||||
if(line.includes('^')) {
|
||||
let startIndex = line.indexOf('^');
|
||||
const superRegex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/gy;
|
||||
const subRegex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/gy;
|
||||
|
||||
while (startIndex >= 0) {
|
||||
superRegex.lastIndex = subRegex.lastIndex = startIndex;
|
||||
let isSuper = false;
|
||||
let match = subRegex.exec(line) || superRegex.exec(line);
|
||||
if (match) {
|
||||
isSuper = !subRegex.lastIndex;
|
||||
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' });
|
||||
}
|
||||
startIndex = line.indexOf('^', Math.max(startIndex + 1, subRegex.lastIndex, superRegex.lastIndex));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,16 @@
|
||||
vertical-align : sub;
|
||||
font-size : 0.9em;
|
||||
}
|
||||
.dl-highlight {
|
||||
&.dl-colon-highlight {
|
||||
font-weight : bold;
|
||||
color : #949494;
|
||||
background : #E5E5E5;
|
||||
border-radius : 3px;
|
||||
}
|
||||
&.dt-highlight { color : rgb(96, 117, 143); }
|
||||
&.dd-highlight { color : rgb(97, 57, 178); }
|
||||
}
|
||||
}
|
||||
|
||||
.brewJump {
|
||||
|
||||
@@ -15,6 +15,14 @@
|
||||
}
|
||||
&.listPage .content {
|
||||
overflow-y : scroll;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 20px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
width:20px;
|
||||
background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,272 +1,351 @@
|
||||
@import "naturalcrit/styles/colors.less";
|
||||
@navbarHeight : 28px;
|
||||
@keyframes pinkColoring {
|
||||
0% {color : pink;}
|
||||
50% {color : pink;}
|
||||
75% {color : red;}
|
||||
100% {color : pink;}
|
||||
}
|
||||
.homebrew nav {
|
||||
.homebrewLogo {
|
||||
.animate(color);
|
||||
font-family : CodeBold;
|
||||
font-size : 12px;
|
||||
color : white;
|
||||
div {
|
||||
margin-top : 2px;
|
||||
margin-bottom : -2px;
|
||||
}
|
||||
&:hover {
|
||||
color : @blue;
|
||||
}
|
||||
}
|
||||
.editTitle.navItem {
|
||||
padding : 2px 12px;
|
||||
input {
|
||||
font-family : "Open Sans", sans-serif;
|
||||
font-size : 12px;
|
||||
font-weight : 800;
|
||||
width : 250px;
|
||||
margin : 0;
|
||||
padding : 2px;
|
||||
text-align : center;
|
||||
color : white;
|
||||
border : 1px solid @blue;
|
||||
outline : none;
|
||||
background-color : transparent;
|
||||
}
|
||||
.charCount {
|
||||
display : inline-block;
|
||||
margin-left : 8px;
|
||||
text-align : right;
|
||||
vertical-align : bottom;
|
||||
color : #666;
|
||||
&.max {
|
||||
color : @red;
|
||||
}
|
||||
}
|
||||
}
|
||||
.brewTitle.navItem {
|
||||
font-size : 12px;
|
||||
font-weight : 800;
|
||||
height : 100%;
|
||||
text-align : center;
|
||||
text-transform : initial;
|
||||
color : white;
|
||||
background-color : transparent;
|
||||
flex-grow : 1;
|
||||
}
|
||||
.save-menu {
|
||||
.dropdown {
|
||||
z-index : 1000;
|
||||
}
|
||||
.navItem i.fa-power-off {
|
||||
color : red;
|
||||
&.active {
|
||||
color : rgb(0, 182, 52);
|
||||
filter : drop-shadow(0 0 2px rgba(0, 182, 52, 0.765));
|
||||
}
|
||||
}
|
||||
}
|
||||
.patreon.navItem {
|
||||
border-right : 1px solid #666;
|
||||
border-left : 1px solid #666;
|
||||
&:hover i {
|
||||
color : red;
|
||||
}
|
||||
i {
|
||||
.animate(color);
|
||||
animation-name : pinkColoring;
|
||||
animation-duration : 2s;
|
||||
color : pink;
|
||||
}
|
||||
}
|
||||
.recent.navDropdownContainer {
|
||||
position : relative;
|
||||
.navDropdown .navItem {
|
||||
overflow : hidden auto;
|
||||
max-height : ~"calc(100vh - 28px)";
|
||||
scrollbar-color : #666 #333;
|
||||
scrollbar-width : thin;
|
||||
|
||||
@import 'naturalcrit/styles/colors.less';
|
||||
|
||||
#backgroundColorsHover;
|
||||
.animate(background-color);
|
||||
position : relative;
|
||||
display : block;
|
||||
overflow : clip;
|
||||
box-sizing : border-box;
|
||||
padding : 8px 5px 13px;
|
||||
text-decoration : none;
|
||||
color : white;
|
||||
border-top : 1px solid #888;
|
||||
background-color : #333;
|
||||
.clear {
|
||||
position : absolute;
|
||||
top : 50%;
|
||||
right : 0;
|
||||
display : none;
|
||||
width : 20px;
|
||||
height : 100%;
|
||||
transform : translateY(-50%);
|
||||
opacity : 70%;
|
||||
border-radius : 3px;
|
||||
background-color : #333;
|
||||
&:hover {
|
||||
opacity : 100%;
|
||||
}
|
||||
i {
|
||||
font-size : 10px;
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
margin : 0;
|
||||
text-align : center;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background-color : @blue;
|
||||
.clear {
|
||||
display : grid;
|
||||
place-content : center;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
display : inline-block;
|
||||
overflow : hidden;
|
||||
width : 100%;
|
||||
white-space : nowrap;
|
||||
text-overflow : ellipsis;
|
||||
}
|
||||
.time {
|
||||
font-size : 0.7em;
|
||||
position : absolute;
|
||||
right : 2px;
|
||||
bottom : 2px;
|
||||
color : #888;
|
||||
}
|
||||
&.header {
|
||||
display : block;
|
||||
box-sizing : border-box;
|
||||
padding : 5px 0;
|
||||
text-align : center;
|
||||
color : #BBB;
|
||||
border-top : 1px solid #888;
|
||||
background-color : #333;
|
||||
&:nth-of-type(1) {
|
||||
background-color : darken(@teal, 20%);
|
||||
}
|
||||
&:nth-of-type(2) {
|
||||
background-color : darken(@purple, 30%);
|
||||
}
|
||||
}
|
||||
}
|
||||
@navbarHeight : 28px;
|
||||
|
||||
@keyframes pinkColoring {
|
||||
0% { color : pink; }
|
||||
50% { color : pink; }
|
||||
75% { color : red; }
|
||||
100% { color : pink; }
|
||||
}
|
||||
|
||||
@keyframes glideDropDown {
|
||||
0% {
|
||||
background-color : #333333;
|
||||
opacity : 0;
|
||||
transform : translate(0px, -100%);
|
||||
}
|
||||
.metadata.navItem {
|
||||
position : relative;
|
||||
100% {
|
||||
background-color : #333333;
|
||||
opacity : 1;
|
||||
transform : translate(0px, 0px);
|
||||
}
|
||||
}
|
||||
|
||||
.homebrew nav {
|
||||
background-color : #333333;
|
||||
.navContent {
|
||||
position : relative;
|
||||
z-index : 2;
|
||||
display : flex;
|
||||
justify-content : space-between;
|
||||
}
|
||||
.navSection {
|
||||
display : flex;
|
||||
align-items : center;
|
||||
height : 100%;
|
||||
padding : 0;
|
||||
flex-grow : 1;
|
||||
i {
|
||||
margin-right : 10px;
|
||||
&:last-child .navItem { border-left : 1px solid #666666; }
|
||||
}
|
||||
// "NaturalCrit" logo
|
||||
.navLogo {
|
||||
display : block;
|
||||
margin-top : 0px;
|
||||
margin-right : 8px;
|
||||
margin-left : 8px;
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
&:hover {
|
||||
.name { color : @orange; }
|
||||
svg { fill : @orange; }
|
||||
}
|
||||
.window {
|
||||
position : absolute;
|
||||
z-index : -1;
|
||||
bottom : 0;
|
||||
left : 50%;
|
||||
display : flex;
|
||||
justify-content : flex-start;
|
||||
width : 440px;
|
||||
max-height : ~"calc(100vh - 28px)";
|
||||
margin : 0 auto;
|
||||
padding : 0 10px 5px;
|
||||
transition : transform 0.4s, opacity 0.4s;
|
||||
border : 3px solid #444;
|
||||
border-top : unset;
|
||||
border-radius : 0 0 5px 5px;
|
||||
background-color : #333;
|
||||
box-shadow : inset 0 7px 9px -7px #111;
|
||||
flex-flow : row wrap;
|
||||
align-content : baseline;
|
||||
&.active {
|
||||
transform : translateX(-50%) translateY(100%);
|
||||
opacity : 1;
|
||||
svg {
|
||||
height : 13px;
|
||||
margin-right : 0.2em;
|
||||
cursor : pointer;
|
||||
fill : white;
|
||||
}
|
||||
span.name {
|
||||
font-family : 'CodeLight';
|
||||
font-size : 15px;
|
||||
span.crit { font-family : 'CodeBold'; }
|
||||
small {
|
||||
font-family : 'Open Sans';
|
||||
font-size : 0.3em;
|
||||
font-weight : 800;
|
||||
text-transform : uppercase;
|
||||
}
|
||||
&.inactive {
|
||||
transform : translateX(-50%) translateY(0%);
|
||||
opacity : 0;
|
||||
}
|
||||
}
|
||||
.navItem {
|
||||
#backgroundColorsHover;
|
||||
.animate(background-color);
|
||||
padding : 8px 12px;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
line-height : 13px;
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
text-transform : uppercase;
|
||||
cursor : pointer;
|
||||
background-color : #333333;
|
||||
i {
|
||||
float : right;
|
||||
margin-left : 5px;
|
||||
font-size : 13px;
|
||||
}
|
||||
&.patreon {
|
||||
border-right : 1px solid #666666;
|
||||
border-left : 1px solid #666666;
|
||||
&:hover i { color : red; }
|
||||
i {
|
||||
color : pink;
|
||||
.animate(color);
|
||||
animation-name : pinkColoring;
|
||||
animation-duration : 2s;
|
||||
}
|
||||
.row {
|
||||
display : flex;
|
||||
width : 100%;
|
||||
flex-flow : row wrap;
|
||||
h4 {
|
||||
display : block;
|
||||
box-sizing : border-box;
|
||||
min-width : 76px;
|
||||
padding : 5px 0;
|
||||
text-align : center;
|
||||
color : #BBB;
|
||||
flex-basis : 20%;
|
||||
flex-grow : 1;
|
||||
}
|
||||
&.editTitle { // this is not needed at all currently - you used to be able to edit the title via the navbar.
|
||||
padding : 2px 12px;
|
||||
input {
|
||||
width : 250px;
|
||||
padding : 2px;
|
||||
margin : 0;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 12px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
text-align : center;
|
||||
background-color : transparent;
|
||||
border : 1px solid @blue;
|
||||
outline : none;
|
||||
}
|
||||
.charCount {
|
||||
display : inline-block;
|
||||
margin-left : 8px;
|
||||
color : #666666;
|
||||
text-align : right;
|
||||
vertical-align : bottom;
|
||||
&.max { color : @red; }
|
||||
}
|
||||
}
|
||||
&.brewTitle {
|
||||
flex-grow : 1;
|
||||
font-size : 12px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
text-align : center;
|
||||
text-transform : initial;
|
||||
background-color : transparent;
|
||||
}
|
||||
// "The Homebrewery" logo
|
||||
&.homebrewLogo {
|
||||
.animate(color);
|
||||
font-family : 'CodeBold';
|
||||
font-size : 12px;
|
||||
color : white;
|
||||
div {
|
||||
margin-top : 2px;
|
||||
margin-bottom : -2px;
|
||||
}
|
||||
&:hover { color : @blue; }
|
||||
}
|
||||
&.metadata {
|
||||
position : relative;
|
||||
display : flex;
|
||||
flex-grow : 1;
|
||||
align-items : center;
|
||||
height : 100%;
|
||||
padding : 0;
|
||||
i { margin-right : 10px;}
|
||||
.window {
|
||||
position : absolute;
|
||||
bottom : 0;
|
||||
left : 50%;
|
||||
z-index : -1;
|
||||
display : flex;
|
||||
flex-flow : row wrap;
|
||||
align-content : baseline;
|
||||
justify-content : flex-start;
|
||||
width : 440px;
|
||||
max-height : ~'calc(100vh - 28px)';
|
||||
padding : 0 10px 5px;
|
||||
margin : 0 auto;
|
||||
background-color : #333333;
|
||||
border : 3px solid #444444;
|
||||
border-top : unset;
|
||||
border-radius : 0 0 5px 5px;
|
||||
box-shadow : inset 0 7px 9px -7px #111111;
|
||||
transition : transform 0.4s, opacity 0.4s;
|
||||
&.active {
|
||||
opacity : 1;
|
||||
transform : translateX(-50%) translateY(100%);
|
||||
}
|
||||
p {
|
||||
font-family : "Open Sans", sans-serif;
|
||||
font-size : 10px;
|
||||
font-weight : normal;
|
||||
padding : 5px 0;
|
||||
text-transform : initial;
|
||||
flex-basis : 80%;
|
||||
flex-grow : 1;
|
||||
.tag {
|
||||
display : inline-block;
|
||||
margin : 2px 2px;
|
||||
padding : 2px;
|
||||
border : 2px solid grey;
|
||||
border-radius : 5px;
|
||||
background-color : #444;
|
||||
&.inactive {
|
||||
opacity : 0;
|
||||
transform : translateX(-50%) translateY(0%);
|
||||
}
|
||||
.row {
|
||||
display : flex;
|
||||
flex-flow : row wrap;
|
||||
width : 100%;
|
||||
h4 {
|
||||
box-sizing : border-box;
|
||||
display : block;
|
||||
flex-basis : 20%;
|
||||
flex-grow : 1;
|
||||
min-width : 76px;
|
||||
padding : 5px 0;
|
||||
color : #BBBBBB;
|
||||
text-align : center;
|
||||
}
|
||||
a.userPageLink {
|
||||
text-decoration : none;
|
||||
color : white;
|
||||
&:hover {
|
||||
text-decoration : underline;
|
||||
p {
|
||||
flex-basis : 80%;
|
||||
flex-grow : 1;
|
||||
padding : 5px 0;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 10px;
|
||||
font-weight : normal;
|
||||
text-transform : initial;
|
||||
.tag {
|
||||
display : inline-block;
|
||||
padding : 2px;
|
||||
margin : 2px 2px;
|
||||
background-color : #444444;
|
||||
border : 2px solid grey;
|
||||
border-radius : 5px;
|
||||
}
|
||||
a.userPageLink {
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
&:hover { text-decoration : underline; }
|
||||
}
|
||||
}
|
||||
&:nth-of-type(even) { background-color : #555555; }
|
||||
}
|
||||
&:nth-of-type(even) {
|
||||
background-color : #555;
|
||||
}
|
||||
}
|
||||
&.warning {
|
||||
position : relative;
|
||||
color : white;
|
||||
background-color : @orange;
|
||||
&:hover > .dropdown { visibility : visible; }
|
||||
.dropdown {
|
||||
position : absolute;
|
||||
top : 28px;
|
||||
left : 0;
|
||||
z-index : 10000;
|
||||
box-sizing : border-box;
|
||||
display : block;
|
||||
width : 100%;
|
||||
padding : 13px 5px;
|
||||
text-align : center;
|
||||
visibility : hidden;
|
||||
background-color : #333333;
|
||||
}
|
||||
}
|
||||
&.account {
|
||||
min-width : 100px;
|
||||
&.username { text-transform : none;}
|
||||
}
|
||||
}
|
||||
.navDropdownContainer {
|
||||
position : relative;
|
||||
.navDropdown {
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
right: 0px;
|
||||
z-index: 10000;
|
||||
width: max-content;
|
||||
min-width:100%;
|
||||
max-height: calc(100vh - 28px);
|
||||
overflow: hidden auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
.navItem {
|
||||
position : relative;
|
||||
display : flex;
|
||||
justify-content : space-between;
|
||||
align-items : center;
|
||||
width : 100%;
|
||||
border : 1px solid #888888;
|
||||
border-bottom : 0;
|
||||
animation-name : glideDropDown;
|
||||
animation-duration : 0.4s;
|
||||
}
|
||||
}
|
||||
&.recent {
|
||||
position : relative;
|
||||
.navDropdown .navItem {
|
||||
#backgroundColorsHover;
|
||||
.animate(background-color);
|
||||
position : relative;
|
||||
box-sizing : border-box;
|
||||
display : block;
|
||||
max-width : 15em;
|
||||
max-height : ~'calc(100vh - 28px)';
|
||||
padding : 8px 5px 13px;
|
||||
overflow : hidden auto;
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
background-color : #333333;
|
||||
border-top : 1px solid #888888;
|
||||
scrollbar-color : #666666 #333333;
|
||||
scrollbar-width : thin;
|
||||
.clear {
|
||||
position : absolute;
|
||||
top : 50%;
|
||||
right : 0;
|
||||
display : none;
|
||||
width : 20px;
|
||||
height : 100%;
|
||||
background-color : #333333;
|
||||
border-radius : 3px;
|
||||
opacity : 70%;
|
||||
transform : translateY(-50%);
|
||||
&:hover { opacity : 100%; }
|
||||
i {
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
margin : 0;
|
||||
font-size : 10px;
|
||||
text-align : center;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background-color : @blue;
|
||||
.clear {
|
||||
display : grid;
|
||||
place-content : center;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
display : inline-block;
|
||||
width : 100%;
|
||||
overflow : hidden auto;
|
||||
text-overflow : ellipsis;
|
||||
white-space : nowrap;
|
||||
}
|
||||
.time {
|
||||
position : absolute;
|
||||
right : 2px;
|
||||
bottom : 2px;
|
||||
font-size : 0.7em;
|
||||
color : #888888;
|
||||
}
|
||||
&.header {
|
||||
box-sizing : border-box;
|
||||
display : block;
|
||||
padding : 5px 0;
|
||||
color : #BBBBBB;
|
||||
text-align : center;
|
||||
background-color : #333333;
|
||||
border-top : 1px solid #888888;
|
||||
&:nth-of-type(1) { background-color : darken(@teal, 20%); }
|
||||
&:nth-of-type(2) { background-color : darken(@purple, 30%); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.warning.navItem {
|
||||
position : relative;
|
||||
color : white;
|
||||
background-color : @orange;
|
||||
&:hover > .dropdown {
|
||||
visibility : visible;
|
||||
}
|
||||
|
||||
// this should likely be refactored into .navDropdownContainer
|
||||
.save-menu {
|
||||
.dropdown { z-index : 1000; }
|
||||
.navItem i.fa-power-off {
|
||||
color : red;
|
||||
&.active {
|
||||
color : rgb(0, 182, 52);
|
||||
filter : drop-shadow(0 0 2px rgba(0, 182, 52, 0.765));
|
||||
}
|
||||
.dropdown {
|
||||
position : absolute;
|
||||
z-index : 10000;
|
||||
top : 28px;
|
||||
left : 0;
|
||||
display : block;
|
||||
visibility : hidden;
|
||||
box-sizing : border-box;
|
||||
width : 100%;
|
||||
padding : 13px 5px;
|
||||
text-align : center;
|
||||
background-color : #333;
|
||||
}
|
||||
}
|
||||
.account.navItem {
|
||||
min-width : 100px;
|
||||
}
|
||||
.account.username.navItem {
|
||||
text-transform : none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,64 @@
|
||||
const React = require('react');
|
||||
const _ = require('lodash');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const { splitTextStyleAndMetadata } = require('../../../shared/helpers.js'); // Importing the function from helpers.js
|
||||
|
||||
module.exports = function(props){
|
||||
return <Nav.item
|
||||
href='/new'
|
||||
newTab={true}
|
||||
color='purple'
|
||||
icon='fas fa-plus-square'>
|
||||
new
|
||||
</Nav.item>;
|
||||
const BREWKEY = 'homebrewery-new';
|
||||
const STYLEKEY = 'homebrewery-new-style';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
|
||||
const NewBrew = () => {
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const fileContent = e.target.result;
|
||||
const newBrew = {
|
||||
text: fileContent,
|
||||
style: ''
|
||||
};
|
||||
if(fileContent.startsWith('```metadata')) {
|
||||
splitTextStyleAndMetadata(newBrew); // Modify newBrew directly
|
||||
localStorage.setItem(BREWKEY, newBrew.text);
|
||||
localStorage.setItem(STYLEKEY, newBrew.style);
|
||||
localStorage.setItem(METAKEY, JSON.stringify(_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])));
|
||||
window.location.href = '/new';
|
||||
} else {
|
||||
alert('This file is invalid, please, enter a valid file');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Nav.dropdown>
|
||||
<Nav.item
|
||||
className='new'
|
||||
color='purple'
|
||||
icon='fa-solid fa-plus-square'>
|
||||
new
|
||||
</Nav.item>
|
||||
<Nav.item
|
||||
className='fromBlank'
|
||||
href='/new'
|
||||
newTab={true}
|
||||
color='purple'
|
||||
icon='fa-solid fa-file'>
|
||||
from blank
|
||||
</Nav.item>
|
||||
|
||||
<Nav.item
|
||||
className='fromFile'
|
||||
color='purple'
|
||||
icon='fa-solid fa-upload'
|
||||
onClick={() => { document.getElementById('uploadTxt').click(); }}>
|
||||
<input id="uploadTxt" className='newFromLocal' type="file" onChange={handleFileChange} style={{ display: 'none' }} />
|
||||
from file
|
||||
</Nav.item>
|
||||
</Nav.dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = NewBrew;
|
||||
|
||||
@@ -20,7 +20,8 @@ const BrewItem = createClass({
|
||||
authors : [],
|
||||
stubbed : true
|
||||
},
|
||||
reportError : ()=>{}
|
||||
updateListFilter : ()=>{},
|
||||
reportError : ()=>{}
|
||||
};
|
||||
},
|
||||
|
||||
@@ -44,6 +45,10 @@ const BrewItem = createClass({
|
||||
});
|
||||
},
|
||||
|
||||
updateFilter : function(type, term){
|
||||
this.props.updateListFilter(type, term);
|
||||
},
|
||||
|
||||
renderDeleteBrewLink : function(){
|
||||
if(!this.props.brew.editId) return;
|
||||
|
||||
@@ -109,6 +114,9 @@ const BrewItem = createClass({
|
||||
const brew = this.props.brew;
|
||||
if(Array.isArray(brew.tags)) { // temporary fix until dud tags are cleaned
|
||||
brew.tags = brew.tags?.filter((tag)=>tag); //remove tags that are empty strings
|
||||
brew.tags.sort((a, b)=>{
|
||||
return a.indexOf(':') - b.indexOf(':') != 0 ? a.indexOf(':') - b.indexOf(':') : a.toLowerCase().localeCompare(b.toLowerCase());
|
||||
});
|
||||
}
|
||||
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
@@ -129,7 +137,7 @@ const BrewItem = createClass({
|
||||
<i className='fas fa-tags'/>
|
||||
{brew.tags.map((tag, idx)=>{
|
||||
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
||||
return <span key={idx} className={matches[1]}>{matches[2]}</span>;
|
||||
return <span key={idx} className={matches[1]} onClick={()=>{this.updateFilter(tag);}}>{matches[2]}</span>;
|
||||
})}
|
||||
</div>
|
||||
</> : <></>
|
||||
|
||||
@@ -63,6 +63,41 @@
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
border-color: currentColor;
|
||||
cursor : pointer;
|
||||
&:before {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-size: 12px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
&.type {
|
||||
background-color: #0080003b;
|
||||
color: #008000;
|
||||
&:before{
|
||||
content: '\f0ad';
|
||||
}
|
||||
}
|
||||
&.group {
|
||||
background-color: #5050503b;
|
||||
color: #000000;
|
||||
&:before{
|
||||
content: '\f500';
|
||||
}
|
||||
}
|
||||
&.meta {
|
||||
background-color: #0000803b;
|
||||
color: #000080;
|
||||
&:before{
|
||||
content: '\f05a';
|
||||
}
|
||||
}
|
||||
&.system {
|
||||
background-color: #8000003b;
|
||||
color: #800000;
|
||||
&:before{
|
||||
content: '\f518';
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover{
|
||||
.links{
|
||||
|
||||
@@ -36,6 +36,7 @@ const ListPage = createClass({
|
||||
|
||||
return {
|
||||
filterString : this.props.query?.filter || '',
|
||||
filterTags : [],
|
||||
sortType : this.props.query?.sort || null,
|
||||
sortDir : this.props.query?.dir || null,
|
||||
query : this.props.query,
|
||||
@@ -82,7 +83,7 @@ const ListPage = createClass({
|
||||
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
||||
|
||||
return _.map(brews, (brew, idx)=>{
|
||||
return <BrewItem brew={brew} key={idx} reportError={this.props.reportError}/>;
|
||||
return <BrewItem brew={brew} key={idx} reportError={this.props.reportError} updateListFilter={ (tag)=>{ this.updateUrl(this.state.filterString, this.state.sortType, this.state.sortDir, tag); }}/>;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -136,13 +137,33 @@ const ListPage = createClass({
|
||||
return;
|
||||
},
|
||||
|
||||
updateUrl : function(filterTerm, sortType, sortDir){
|
||||
updateUrl : function(filterTerm, sortType, sortDir, filterTag=''){
|
||||
const url = new URL(window.location.href);
|
||||
const urlParams = new URLSearchParams(url.search);
|
||||
|
||||
urlParams.set('sort', sortType);
|
||||
urlParams.set('dir', sortDir);
|
||||
|
||||
let filterTags = urlParams.getAll('tag');
|
||||
if(filterTag != '') {
|
||||
if(filterTags.findIndex((tag)=>{return tag.toLowerCase()==filterTag.toLowerCase();}) == -1){
|
||||
filterTags.push(filterTag);
|
||||
} else {
|
||||
filterTags = filterTags.filter((tag)=>{ return tag.toLowerCase() != filterTag.toLowerCase(); });
|
||||
}
|
||||
}
|
||||
urlParams.delete('tag');
|
||||
// Add tags to URL in the order they were clicked
|
||||
filterTags.forEach((tag)=>{ urlParams.append('tag', tag); });
|
||||
// Sort tags before updating state
|
||||
filterTags.sort((a, b)=>{
|
||||
return a.indexOf(':') - b.indexOf(':') != 0 ? a.indexOf(':') - b.indexOf(':') : a.toLowerCase().localeCompare(b.toLowerCase());
|
||||
});
|
||||
|
||||
this.setState({
|
||||
filterTags
|
||||
});
|
||||
|
||||
if(!filterTerm)
|
||||
urlParams.delete('filter');
|
||||
else
|
||||
@@ -166,6 +187,16 @@ const ListPage = createClass({
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderTagsOptions : function(){
|
||||
if(this.state.filterTags?.length == 0) return;
|
||||
return <div className='tags-container'>
|
||||
{_.map(this.state.filterTags, (tag, idx)=>{
|
||||
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
||||
return <span key={idx} className={matches[1]} onClick={()=>{ this.updateUrl(this.state.filterString, this.state.sortType, this.state.sortDir, tag); }}>{matches[2]}</span>;
|
||||
})}
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderSortOptions : function(){
|
||||
return <div className='sort-container'>
|
||||
<h6>Sort by :</h6>
|
||||
@@ -176,9 +207,6 @@ const ListPage = createClass({
|
||||
{/* {this.renderSortOption('Latest', 'latest')} */}
|
||||
|
||||
{this.renderFilterOption()}
|
||||
|
||||
|
||||
|
||||
</div>;
|
||||
},
|
||||
|
||||
@@ -186,14 +214,28 @@ const ListPage = createClass({
|
||||
const testString = _.deburr(this.state.filterString).toLowerCase();
|
||||
|
||||
brews = _.filter(brews, (brew)=>{
|
||||
// Filter by user entered text
|
||||
const brewStrings = _.deburr([
|
||||
brew.title,
|
||||
brew.description,
|
||||
brew.tags].join('\n')
|
||||
.toLowerCase());
|
||||
|
||||
return brewStrings.includes(testString);
|
||||
const filterTextTest = brewStrings.includes(testString);
|
||||
|
||||
// Filter by user selected tags
|
||||
let filterTagTest = true;
|
||||
if(this.state.filterTags.length > 0){
|
||||
filterTagTest = Array.isArray(brew.tags) && this.state.filterTags?.every((tag)=>{
|
||||
return brew.tags.findIndex((brewTag)=>{
|
||||
return brewTag.toLowerCase() == tag.toLowerCase();
|
||||
}) >= 0;
|
||||
});
|
||||
}
|
||||
|
||||
return filterTextTest && filterTagTest;
|
||||
});
|
||||
|
||||
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
|
||||
},
|
||||
|
||||
@@ -220,10 +262,11 @@ 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'/>
|
||||
<link href='/themes/V3/Blank/style.css' type="text/css" rel='stylesheet'/>
|
||||
<link href='/themes/V3/5ePHB/style.css' type="text/css" rel='stylesheet'/>
|
||||
{this.props.navItems}
|
||||
{this.renderSortOptions()}
|
||||
{this.renderTagsOptions()}
|
||||
|
||||
<div className='content V3'>
|
||||
<div className='page'>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.sort-container{
|
||||
.sort-container {
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
position : sticky;
|
||||
top : 0;
|
||||
@@ -125,4 +125,66 @@
|
||||
|
||||
|
||||
}
|
||||
.tags-container {
|
||||
height : 30px;
|
||||
background-color : #555;
|
||||
border-top : 1px solid #666;
|
||||
border-bottom : 1px solid #666;
|
||||
color : white;
|
||||
display : flex;
|
||||
justify-content : center;
|
||||
align-items : center;
|
||||
column-gap : 15px;
|
||||
row-gap : 5px;
|
||||
flex-wrap : wrap;
|
||||
span {
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 11px;
|
||||
font-weight : bold;
|
||||
border : 1px solid;
|
||||
border-radius : 3px;
|
||||
padding : 3px;
|
||||
cursor : pointer;
|
||||
color: #dfdfdf;
|
||||
&:before {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-size: 12px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
&:after {
|
||||
content: '\f00d';
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-size: 12px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
&.type {
|
||||
background-color: #008000;
|
||||
border-color: #00a000;
|
||||
&:before{
|
||||
content: '\f0ad';
|
||||
}
|
||||
}
|
||||
&.group {
|
||||
background-color: #505050;
|
||||
border-color: #000000;
|
||||
&:before{
|
||||
content: '\f500';
|
||||
}
|
||||
}
|
||||
&.meta {
|
||||
background-color: #000080;
|
||||
border-color: #0000a0;
|
||||
&:before{
|
||||
content: '\f05a';
|
||||
}
|
||||
}
|
||||
&.system {
|
||||
background-color: #800000;
|
||||
border-color: #a00000;
|
||||
&:before{
|
||||
content: '\f518';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,16 @@ const errorIndex = (props)=>{
|
||||
An error occurred while attempting to remove the user from the Homebrewery document author list!
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}`,
|
||||
|
||||
// Brew locked by Administrators error
|
||||
'100' : dedent`
|
||||
## This brew has been locked.
|
||||
|
||||
Please contact the Administrators to unlock this document.
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}
|
||||
|
||||
**Brew Title:** ${props.brew.brewTitle}`,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -96,11 +96,11 @@ const PrintPage = createClass({
|
||||
|
||||
return <div>
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
<link href={`/themes/${rendererPath}/Blank/style.css`} rel='stylesheet'/>
|
||||
<link href={`/themes/${rendererPath}/Blank/style.css`} type="text/css" rel='stylesheet'/>
|
||||
{baseThemePath &&
|
||||
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
|
||||
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} type="text/css" rel='stylesheet'/>
|
||||
}
|
||||
<link href={`/themes/${rendererPath}/${themePath}/style.css`} rel='stylesheet'/>
|
||||
<link href={`/themes/${rendererPath}/${themePath}/style.css`} type="text/css" rel='stylesheet'/>
|
||||
{/* Apply CSS from Style tab */}
|
||||
{this.renderStyle()}
|
||||
<div className='pages' ref='pages' lang={this.state.brew.lang}>
|
||||
|
||||
@@ -12,9 +12,9 @@ const template = async function(name, title='', props = {}){
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
|
||||
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
||||
<link href="//use.fontawesome.com/releases/v6.5.1/css/all.css" rel="stylesheet" type="text/css" />
|
||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
|
||||
<link href=${`/${name}/bundle.css`} type="text/css" rel='stylesheet' />
|
||||
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
||||
${ogMetaTags}
|
||||
<meta name="twitter:card" content="summary">
|
||||
|
||||
1750
package-lock.json
generated
1750
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "3.11.0",
|
||||
"version": "3.12.0",
|
||||
"engines": {
|
||||
"npm": "^10.2.x",
|
||||
"node": "^20.8.x"
|
||||
@@ -31,6 +31,7 @@
|
||||
"test:mustache-syntax:inline": "jest '.*(mustache-syntax).*' -t '^Inline:.*' --verbose --noStackTrace",
|
||||
"test:mustache-syntax:block": "jest '.*(mustache-syntax).*' -t '^Block:.*' --verbose --noStackTrace",
|
||||
"test:mustache-syntax:injection": "jest '.*(mustache-syntax).*' -t '^Injection:.*' --verbose --noStackTrace",
|
||||
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
|
||||
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
||||
"phb": "node scripts/phb.js",
|
||||
"prod": "set NODE_ENV=production && npm run build",
|
||||
@@ -80,19 +81,19 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.23.9",
|
||||
"@babel/plugin-transform-runtime": "^7.23.9",
|
||||
"@babel/preset-env": "^7.23.9",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@babel/core": "^7.24.3",
|
||||
"@babel/plugin-transform-runtime": "^7.24.3",
|
||||
"@babel/preset-env": "^7.24.3",
|
||||
"@babel/preset-react": "^7.24.1",
|
||||
"@googleapis/drive": "^8.7.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"classnames": "^2.3.2",
|
||||
"classnames": "^2.5.1",
|
||||
"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": "^4.19.2",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"express-static-gzip": "2.1.7",
|
||||
"fs-extra": "11.2.0",
|
||||
@@ -106,21 +107,21 @@
|
||||
"marked-smartypants-lite": "^1.0.2",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.30.1",
|
||||
"mongoose": "^8.2.0",
|
||||
"mongoose": "^8.2.3",
|
||||
"nanoid": "3.3.4",
|
||||
"nconf": "^0.12.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-frame-component": "^4.1.3",
|
||||
"react-router-dom": "6.22.1",
|
||||
"react-router-dom": "6.22.3",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"superagent": "^8.1.2",
|
||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.56.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-jest": "^27.9.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"postcss-less": "^6.0.0",
|
||||
|
||||
@@ -17,21 +17,8 @@ const asyncHandler = require('express-async-handler');
|
||||
|
||||
const { DEFAULT_BREW } = require('./brewDefaults.js');
|
||||
|
||||
const splitTextStyleAndMetadata = (brew)=>{
|
||||
brew.text = brew.text.replaceAll('\r\n', '\n');
|
||||
if(brew.text.startsWith('```metadata')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
const metadataSection = brew.text.slice(12, index - 1);
|
||||
const metadata = yaml.load(metadataSection);
|
||||
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']));
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
if(brew.text.startsWith('```css')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
brew.style = brew.text.slice(7, index - 1);
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
};
|
||||
const { splitTextStyleAndMetadata } = require('../shared/helpers.js');
|
||||
|
||||
|
||||
const sanitizeBrew = (brew, accessType)=>{
|
||||
brew._id = undefined;
|
||||
|
||||
@@ -54,6 +54,10 @@ const api = {
|
||||
});
|
||||
stub = stub?.toObject();
|
||||
|
||||
if(stub?.lock?.locked && accessType != 'edit') {
|
||||
throw { HBErrorCode: '100', code: stub.lock.code, message: stub.lock.message, brewId: stub.shareId, brewTitle: stub.title };
|
||||
}
|
||||
|
||||
// If there is a google id, try to find the google brew
|
||||
if(!stubOnly && (googleId || stub?.googleId)) {
|
||||
let googleError;
|
||||
|
||||
@@ -117,7 +117,7 @@ describe('Tests for api', ()=>{
|
||||
id : '123456789012345678901234567890123abcdefghijkl'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
expect(googleId).toEqual('123456789012345678901234567890123');
|
||||
expect(id).toEqual('abcdefghijkl');
|
||||
});
|
||||
@@ -128,7 +128,7 @@ describe('Tests for api', ()=>{
|
||||
id : '123456789012345678901234567890123abcdefghij'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
expect(googleId).toEqual('123456789012345678901234567890123');
|
||||
expect(id).toEqual('abcdefghij');
|
||||
});
|
||||
@@ -298,6 +298,18 @@ describe('Tests for api', ()=>{
|
||||
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
||||
expect(google.getGoogleBrew).toHaveBeenCalledWith('2', '1', 'share');
|
||||
});
|
||||
|
||||
it('access is denied to a locked brew', async()=>{
|
||||
const lockBrew = { title: 'test brew', shareId: '1', lock: { locked: true, code: 404, message: 'brew locked' } };
|
||||
model.get = jest.fn(()=>toBrewPromise(lockBrew));
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
|
||||
const fn = api.getBrew('share', false);
|
||||
const req = { brew: {} };
|
||||
const next = jest.fn();
|
||||
|
||||
await expect(fn(req, null, next)).rejects.toEqual({ 'HBErrorCode': '100', 'brewId': '1', 'brewTitle': 'test brew', 'code': 404, 'message': 'brew locked' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeBrewText', ()=>{
|
||||
|
||||
22
shared/helpers.js
Normal file
22
shared/helpers.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const _ = require('lodash');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const splitTextStyleAndMetadata = (brew) => {
|
||||
brew.text = brew.text.replaceAll('\r\n', '\n');
|
||||
if (brew.text.startsWith('```metadata')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
const metadataSection = brew.text.slice(12, index - 1);
|
||||
const metadata = yaml.load(metadataSection);
|
||||
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']));
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
if (brew.text.startsWith('```css')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
brew.style = brew.text.slice(7, index - 1);
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
splitTextStyleAndMetadata
|
||||
};
|
||||
@@ -436,7 +436,7 @@ const CodeEditor = createClass({
|
||||
|
||||
render : function(){
|
||||
return <>
|
||||
<link href={`../homebrew/cm-themes/${this.props.editorTheme}.css`} rel='stylesheet' />
|
||||
<link href={`../homebrew/cm-themes/${this.props.editorTheme}.css`} type="text/css" rel='stylesheet' />
|
||||
<div className='codeEditor' ref='editor' style={this.props.style}/>
|
||||
</>;
|
||||
}
|
||||
|
||||
@@ -17,13 +17,28 @@
|
||||
text-shadow: none;
|
||||
font-weight: 600;
|
||||
color: grey;
|
||||
}
|
||||
}
|
||||
|
||||
.sourceMoveFlash .CodeMirror-line{
|
||||
animation-name: sourceMoveAnimation;
|
||||
animation-duration: 0.4s;
|
||||
}
|
||||
|
||||
.CodeMirror-sizer {
|
||||
padding-right: 0 !important;
|
||||
//this setting must be !important, because CodeMirror sets it inline. Achieves overlay scrollbar
|
||||
}
|
||||
|
||||
.CodeMirror-vscrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 20px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
width: 20px;
|
||||
background: linear-gradient(90deg, #85858599 15px, #80808000 15px);
|
||||
}
|
||||
}
|
||||
|
||||
//.cm-tab {
|
||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
||||
//}
|
||||
|
||||
@@ -294,10 +294,10 @@ const superSubScripts = {
|
||||
}
|
||||
};
|
||||
|
||||
const definitionLists = {
|
||||
name : 'definitionLists',
|
||||
const definitionListsInline = {
|
||||
name : 'definitionListsInline',
|
||||
level : 'block',
|
||||
start(src) { return src.match(/^.*?::.*/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
start(src) { return src.match(/^[^\n]*?::[^\n]*/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
|
||||
let match;
|
||||
@@ -312,7 +312,7 @@ const definitionLists = {
|
||||
}
|
||||
if(definitions.length) {
|
||||
return {
|
||||
type : 'definitionLists',
|
||||
type : 'definitionListsInline',
|
||||
raw : src.slice(0, endIndex),
|
||||
definitions
|
||||
};
|
||||
@@ -326,6 +326,51 @@ const definitionLists = {
|
||||
}
|
||||
};
|
||||
|
||||
const definitionListsMultiline = {
|
||||
name : 'definitionListsMultiline',
|
||||
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 regex = /(\n?\n?(?!::)[^\n]+?(?=\n::))|\n::(.(?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y;
|
||||
let match;
|
||||
let endIndex = 0;
|
||||
const definitions = [];
|
||||
while (match = regex.exec(src)) {
|
||||
if(match[1]) {
|
||||
if(this.lexer.blockTokens(match[1].trim())[0]?.type !== 'paragraph') // DT must not be another block-level token besides <p>
|
||||
break;
|
||||
definitions.push({
|
||||
dt : this.lexer.inlineTokens(match[1].trim()),
|
||||
dds : []
|
||||
});
|
||||
}
|
||||
if(match[2] && definitions.length) {
|
||||
definitions[definitions.length - 1].dds.push(
|
||||
this.lexer.inlineTokens(match[2].trim().replace(/\s/g, ' '))
|
||||
);
|
||||
}
|
||||
endIndex = regex.lastIndex;
|
||||
}
|
||||
if(definitions.length) {
|
||||
return {
|
||||
type : 'definitionListsMultiline',
|
||||
raw : src.slice(0, endIndex),
|
||||
definitions
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
let returnVal = `<dl>`;
|
||||
token.definitions.forEach((def)=>{
|
||||
const dds = def.dds.map((s)=>{
|
||||
return `\n<dd>${this.parser.parseInline(s).trim()}</dd>`;
|
||||
}).join('');
|
||||
returnVal += `<dt>${this.parser.parseInline(def.dt)}</dt>${dds}\n`;
|
||||
});
|
||||
returnVal = returnVal.trim();
|
||||
return `${returnVal}</dl>`;
|
||||
}
|
||||
};
|
||||
|
||||
//v=====--------------------< Variable Handling >-------------------=====v// 242 lines
|
||||
const replaceVar = function(input, hoist=false, allowUnresolved=false) {
|
||||
@@ -572,7 +617,7 @@ function MarkedVariables() {
|
||||
//^=====--------------------< Variable Handling >-------------------=====^//
|
||||
|
||||
Marked.use(MarkedVariables());
|
||||
Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists, superSubScripts] });
|
||||
Marked.use({ extensions: [definitionListsMultiline, definitionListsInline, superSubScripts, mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
||||
Marked.use(mustacheInjectBlock);
|
||||
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
|
||||
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require('./nav.less');
|
||||
require('client/homebrew/navbar/navbar.less');
|
||||
const React = require('react');
|
||||
const { useState, useRef, useEffect } = React;
|
||||
const createClass = require('create-react-class');
|
||||
@@ -104,7 +104,7 @@ const Nav = {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={`navDropdownContainer ${props.className}`}
|
||||
<div className={`navDropdownContainer ${props.className ?? ''}`}
|
||||
ref={myRef}
|
||||
onMouseEnter = { props.trigger.includes('hover') ? ()=>handleDropdown(true) : undefined }
|
||||
onMouseLeave = { props.trigger.includes('hover') ? ()=>handleDropdown(false) : undefined }
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
@import '../styles/colors';
|
||||
@keyframes glideDropDown {
|
||||
0% {transform : translate(0px, -100%);
|
||||
opacity : 0;
|
||||
background-color: #333;}
|
||||
100% {transform : translate(0px, 0px);
|
||||
opacity : 1;
|
||||
background-color: #333;}
|
||||
}
|
||||
nav{
|
||||
background-color : #333;
|
||||
.navContent{
|
||||
position : relative;
|
||||
display : flex;
|
||||
justify-content : space-between;
|
||||
z-index : 2;
|
||||
}
|
||||
.navSection{
|
||||
display : flex;
|
||||
align-items : center;
|
||||
}
|
||||
.navLogo{
|
||||
display : block;
|
||||
margin-top : 0px;
|
||||
margin-right : 8px;
|
||||
margin-left : 8px;
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
&:hover{
|
||||
.name{ color : @orange; }
|
||||
svg{ fill : @orange }
|
||||
}
|
||||
svg{
|
||||
height : 13px;
|
||||
margin-right : 0.2em;
|
||||
cursor : pointer;
|
||||
fill : white;
|
||||
}
|
||||
span.name{
|
||||
font-family : 'CodeLight';
|
||||
font-size : 15px;
|
||||
span.crit{
|
||||
font-family : 'CodeBold';
|
||||
}
|
||||
small{
|
||||
font-family : 'Open Sans';
|
||||
font-size : 0.3em;
|
||||
font-weight : 800;
|
||||
text-transform : uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
.navItem{
|
||||
#backgroundColorsHover;
|
||||
.animate(background-color);
|
||||
padding : 8px 12px;
|
||||
cursor : pointer;
|
||||
background-color : #333;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
text-transform : uppercase;
|
||||
line-height : 13px;
|
||||
i{
|
||||
margin-left : 5px;
|
||||
font-size : 13px;
|
||||
float : right;
|
||||
}
|
||||
}
|
||||
.navSection:last-child .navItem{
|
||||
border-left : 1px solid #666;
|
||||
}
|
||||
.navDropdownContainer{
|
||||
position: relative;
|
||||
.navDropdown {
|
||||
position : absolute;
|
||||
top : 28px;
|
||||
left : 0px;
|
||||
z-index : 10000;
|
||||
width : 100%;
|
||||
overflow : hidden auto;
|
||||
max-height : calc(100vh - 28px);
|
||||
.navItem{
|
||||
animation-name: glideDropDown;
|
||||
animation-duration: 0.4s;
|
||||
position : relative;
|
||||
display : block;
|
||||
width : 100%;
|
||||
vertical-align : middle;
|
||||
padding : 8px 5px;
|
||||
border : 1px solid #888;
|
||||
border-bottom : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
91
tests/markdown/definition-lists.test.js
Normal file
91
tests/markdown/definition-lists.test.js
Normal file
@@ -0,0 +1,91 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
describe('Inline Definition Lists', ()=>{
|
||||
test('No Term 1 Definition', function() {
|
||||
const source = ':: My First Definition\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt></dt><dd>My First Definition</dd>\n</dl>');
|
||||
});
|
||||
|
||||
test('Single Definition Term', function() {
|
||||
const source = 'My term :: My First Definition\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>My term</dt><dd>My First Definition</dd>\n</dl>');
|
||||
});
|
||||
|
||||
test('Multiple Definition Terms', function() {
|
||||
const source = 'Term 1::Definition of Term 1\nTerm 2::Definition of Term 2\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt><dd>Definition of Term 1</dd>\n<dt>Term 2</dt><dd>Definition of Term 2</dd>\n</dl>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiline Definition Lists', ()=>{
|
||||
test('Single Term, Single Definition', function() {
|
||||
const source = 'Term 1\n::Definition 1\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd></dl>');
|
||||
});
|
||||
|
||||
test('Single Term, Plural Definitions', function() {
|
||||
const source = 'Term 1\n::Definition 1\n::Definition 2\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl>');
|
||||
});
|
||||
|
||||
test('Multiple Term, Single Definitions', function() {
|
||||
const source = 'Term 1\n::Definition 1\n\nTerm 2\n::Definition 1\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd></dl>');
|
||||
});
|
||||
|
||||
test('Multiple Term, Plural Definitions', function() {
|
||||
const source = 'Term 1\n::Definition 1\n::Definition 2\n\nTerm 2\n::Definition 1\n::Definition 2\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl>');
|
||||
});
|
||||
|
||||
test('Single Term, Single multi-line definition', function() {
|
||||
const source = 'Term 1\n::Definition 1\nand more and\nmore and more\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more and more</dd></dl>');
|
||||
});
|
||||
|
||||
test('Single Term, Plural multi-line definitions', function() {
|
||||
const source = 'Term 1\n::Definition 1\nand more and more\n::Definition 2\nand more\nand more\n::Definition 3\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dd>Definition 2 and more and more</dd>\n<dd>Definition 3</dd></dl>');
|
||||
});
|
||||
|
||||
test('Multiple Term, Single multi-line definition', function() {
|
||||
const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\n';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl>');
|
||||
});
|
||||
|
||||
test('Multiple Term, Single multi-line definition, followed by an inline dl', function() {
|
||||
const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\n::Inline Definition (no term)';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl><dl><dt></dt><dd>Inline Definition (no term)</dd>\n</dl>');
|
||||
});
|
||||
|
||||
test('Multiple Term, Single multi-line definition, followed by paragraph', function() {
|
||||
const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\nParagraph';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl><p>Paragraph</p>');
|
||||
});
|
||||
|
||||
test('Block Token cannot be the Term of a multi-line definition', function() {
|
||||
const source = '## Header\n::Definition 1 of a single-line DL\n::Definition 1 of another single-line DL';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 id="header">Header</h2>\n<dl><dt></dt><dd>Definition 1 of a single-line DL</dd>\n<dt></dt><dd>Definition 1 of another single-line DL</dd>\n</dl>');
|
||||
});
|
||||
|
||||
test('Inline DL has priority over Multiline', function() {
|
||||
const source = 'Term 1 :: Inline definition 1\n:: Inline definition 2 (no DT)';
|
||||
const rendered = Markdown.render(source).trim();
|
||||
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt><dd>Inline definition 1</dd>\n<dt></dt><dd>Inline definition 2 (no DT)</dd>\n</dl>');
|
||||
});
|
||||
});
|
||||
@@ -78,7 +78,7 @@ module.exports = function(props){
|
||||
|
||||
return dedent`
|
||||
{{toc,wide
|
||||
# Table Of Contents
|
||||
# Contents
|
||||
|
||||
${markdown}
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@import (less) './themes/fonts/5e/fonts.less';
|
||||
@import (less) './themes/assets/assets.less';
|
||||
@import (less) './themes/fonts/icon fonts/font-icons.less';
|
||||
@import (less) './themes/fonts/icon fonts/dicefont.less';
|
||||
|
||||
:root {
|
||||
//Colors
|
||||
@@ -307,7 +307,6 @@
|
||||
margin-left : -0.16cm;
|
||||
background-color : var(--HB_Color_MonsterStatBackground);
|
||||
background-image : @monsterBlockBackground;
|
||||
background-attachment : unset;
|
||||
background-blend-mode : overlay;
|
||||
border-style : solid;
|
||||
border-width : 7px 6px;
|
||||
@@ -403,15 +402,9 @@
|
||||
}
|
||||
}
|
||||
.pageNumber {
|
||||
position : absolute;
|
||||
right : 2px;
|
||||
bottom : 22px;
|
||||
width : 50px;
|
||||
font-size : 0.9em;
|
||||
color : var(--HB_Color_Footnotes);
|
||||
text-align : center;
|
||||
text-indent : 0;
|
||||
&.auto::after { content : counter(phb-page-numbers); }
|
||||
}
|
||||
.footnote {
|
||||
position : absolute;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@import (less) './themes/fonts/5e/fonts.less';
|
||||
@import (less) './themes/assets/assets.less';
|
||||
@import (less) './themes/fonts/icon fonts/dicefont.less';
|
||||
|
||||
:root {
|
||||
//Colors
|
||||
@@ -8,7 +9,7 @@
|
||||
}
|
||||
|
||||
@page { margin : 0; }
|
||||
body { counter-reset : phb-page-numbers; }
|
||||
body { counter-reset : page-numbers; }
|
||||
* { -webkit-print-color-adjust : exact; }
|
||||
|
||||
//*****************************
|
||||
@@ -47,7 +48,7 @@ body { counter-reset : phb-page-numbers; }
|
||||
height : 279.4mm;
|
||||
padding : 1.4cm 1.9cm 1.7cm;
|
||||
overflow : hidden;
|
||||
counter-increment : phb-page-numbers;
|
||||
counter-increment : page-numbers;
|
||||
background-color : var(--HB_Color_Background);
|
||||
text-rendering : optimizeLegibility;
|
||||
contain : size;
|
||||
@@ -166,7 +167,6 @@ body { counter-reset : phb-page-numbers; }
|
||||
margin : 0;
|
||||
font-size : 120px;
|
||||
text-transform : uppercase;
|
||||
mix-blend-mode : overlay;
|
||||
opacity : 30%;
|
||||
transform : rotate(-45deg);
|
||||
p { margin-bottom : none; }
|
||||
@@ -460,3 +460,22 @@ body { counter-reset : phb-page-numbers; }
|
||||
.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%); }
|
||||
}
|
||||
|
||||
//*****************************
|
||||
//* Page Number
|
||||
//*****************************/
|
||||
.page {
|
||||
.pageNumber {
|
||||
position : absolute;
|
||||
right : 30px;
|
||||
bottom : 30px;
|
||||
width : 50px;
|
||||
font-size : 0.9em;
|
||||
text-align : center;
|
||||
&.auto::after { content : counter(page-numbers); }
|
||||
}
|
||||
|
||||
&:nth-child(even) {
|
||||
.pageNumber { left : 30px; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,17 +374,9 @@
|
||||
}
|
||||
.pageNumber{
|
||||
font-family : FrederickaTheGreat;
|
||||
position : absolute;
|
||||
right : 3cm;
|
||||
bottom : 1.25cm;
|
||||
width : 50px;
|
||||
font-size : 0.9em;
|
||||
color : var(--HB_Color_HeaderText);
|
||||
text-align : center;
|
||||
text-indent : 0;
|
||||
&.auto::after {
|
||||
content : counter(phb-page-numbers);
|
||||
}
|
||||
}
|
||||
.footnote{
|
||||
position : absolute;
|
||||
|
||||
121
themes/codeMirror/customThemes/darkvision.css
Normal file
121
themes/codeMirror/customThemes/darkvision.css
Normal file
@@ -0,0 +1,121 @@
|
||||
.CodeMirror {
|
||||
background: #0C0C0C;
|
||||
color: #B9BDB6;
|
||||
}
|
||||
|
||||
/* Brew BG */
|
||||
.brewRenderer {
|
||||
background-color: #0C0C0C;
|
||||
}
|
||||
|
||||
.cm-s-darkvision {
|
||||
/* Blinking cursor and selection */
|
||||
.CodeMirror-cursor {
|
||||
border-left: 1px solid #B9BDB6;
|
||||
}
|
||||
.CodeMirror-selected {
|
||||
background: #E0E8FF40;
|
||||
}
|
||||
|
||||
/* Line number stuff */
|
||||
.CodeMirror-gutter-elt {
|
||||
color: #81969A;
|
||||
}
|
||||
.CodeMirror-linenumber {
|
||||
background-color: #0C0C0C;
|
||||
}
|
||||
.CodeMirror-gutter {
|
||||
background-color: #0C0C0C;
|
||||
}
|
||||
|
||||
/* column splits */
|
||||
.editor .codeEditor .columnSplit {
|
||||
font-style: italic;
|
||||
color: inherit;
|
||||
background-color:#1F5763;
|
||||
border-bottom: #299 solid 1px;
|
||||
}
|
||||
|
||||
/* # headings */
|
||||
.cm-header {
|
||||
color: #C51B1B;
|
||||
-webkit-text-stroke-width: 0.1px;
|
||||
}
|
||||
/* bold points */
|
||||
.cm-strong {
|
||||
font-weight: bold;
|
||||
color: #309DD2;
|
||||
}
|
||||
/* Link headings */
|
||||
.cm-link {
|
||||
color: #DD6300;
|
||||
}
|
||||
/* links */
|
||||
.cm-string {
|
||||
color: #5CE638;
|
||||
}
|
||||
/*@import*/
|
||||
.cm-def {
|
||||
color: #2986CC;
|
||||
}
|
||||
/* Bullets and such */
|
||||
.cm-variable-2 {
|
||||
color: #3CBF30;
|
||||
}
|
||||
|
||||
/* Tags (divs) */
|
||||
.cm-tag {
|
||||
color: #E3FF00;
|
||||
}
|
||||
.cm-attribute {
|
||||
color: #E3FF00;
|
||||
}
|
||||
.cm-atom {
|
||||
color: #CF7EA9;
|
||||
}
|
||||
.cm-qualifier {
|
||||
color: #EE1919;
|
||||
}
|
||||
.cm-comment {
|
||||
color: #BBC700;
|
||||
}
|
||||
.cm-keyword {
|
||||
color: #CC66FF;
|
||||
}
|
||||
.cm-property {
|
||||
color: aqua;
|
||||
}
|
||||
.cm-error {
|
||||
color: #C50202;
|
||||
}
|
||||
.CodeMirror-foldmarker {
|
||||
color: #F0FF00;
|
||||
}
|
||||
/* New page */
|
||||
.cm-builtin {
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
|
||||
.editor .codeEditor {
|
||||
/* blocks */
|
||||
.block:not(.cm-comment) {
|
||||
color: magenta;
|
||||
}
|
||||
/* definition lists */
|
||||
.define.definition {
|
||||
color: #FFAA3E;
|
||||
}
|
||||
.define.term {
|
||||
color: #7290d9;
|
||||
}
|
||||
.define:not(.term):not(.definition) {
|
||||
background: #333;
|
||||
}
|
||||
/* New page */
|
||||
.pageLine {
|
||||
background: #000;
|
||||
color: #000;
|
||||
border-bottom: 1px solid #FFF;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
"colorforth",
|
||||
"darcula",
|
||||
"darkbrewery-v301",
|
||||
"darkvision",
|
||||
"dracula",
|
||||
"duotone-dark",
|
||||
"duotone-light",
|
||||
|
||||
114
themes/fonts/icon fonts/dicefont.less
Normal file
114
themes/fonts/icon fonts/dicefont.less
Normal file
@@ -0,0 +1,114 @@
|
||||
/* Icon Font: dicefont */
|
||||
@font-face {
|
||||
font-family : 'DiceFont';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/icon fonts/dicefont.woff2');
|
||||
}
|
||||
|
||||
.df {
|
||||
display : inline-block;
|
||||
font-family : 'DiceFont';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
font-variant : normal;
|
||||
line-height : 1;
|
||||
text-decoration : inherit;
|
||||
text-transform : none;
|
||||
text-rendering : optimizeLegibility;
|
||||
-moz-osx-font-smoothing : grayscale;
|
||||
-webkit-font-smoothing : 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'; }
|
||||
}
|
||||
BIN
themes/fonts/icon fonts/dicefont.woff2
Normal file
BIN
themes/fonts/icon fonts/dicefont.woff2
Normal file
Binary file not shown.
18
themes/fonts/icon fonts/dicefont_license.md
Normal file
18
themes/fonts/icon fonts/dicefont_license.md
Normal 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).
|
||||
@@ -1,6 +1,6 @@
|
||||
/* Main Font, serif */
|
||||
/* Icon Font: Elderberry Inn */
|
||||
@font-face {
|
||||
font-family : 'Eldeberry-Inn';
|
||||
font-family : 'Elderberry-Inn';
|
||||
font-style : normal;
|
||||
font-weight : normal;
|
||||
src : url('../../../fonts/icon fonts/Elderberry-Inn-Icons.woff2');
|
||||
@@ -10,7 +10,7 @@
|
||||
span.ei {
|
||||
display : inline-block;
|
||||
margin-right : 3px;
|
||||
font-family : 'Eldeberry-Inn';
|
||||
font-family : 'Elderberry-Inn';
|
||||
line-height : 1;
|
||||
vertical-align : baseline;
|
||||
-moz-osx-font-smoothing : grayscale;
|
||||
|
||||
Reference in New Issue
Block a user