Stats
Total Brew Count
{this.state.stats.totalBrews}
+ Total Brews Published
+ {this.state.stats.totalPublishedBrews}
{this.state.fetching
diff --git a/client/admin/brewUtils/stats/stats.less b/client/admin/brewUtils/stats/stats.less
deleted file mode 100644
index b5a4612e1..000000000
--- a/client/admin/brewUtils/stats/stats.less
+++ /dev/null
@@ -1,13 +0,0 @@
-
-.Stats {
- position : relative;
-
- .pending {
- position : absolute;
- top : 0px;
- left : 0px;
- width : 100%;
- height : 100%;
- background-color : rgba(238,238,238, 0.5);
- }
-}
\ No newline at end of file
diff --git a/client/admin/notificationUtils/notificationAdd/notificationAdd.less b/client/admin/notificationUtils/notificationAdd/notificationAdd.less
index 878da24c2..9256b43cb 100644
--- a/client/admin/notificationUtils/notificationAdd/notificationAdd.less
+++ b/client/admin/notificationUtils/notificationAdd/notificationAdd.less
@@ -6,18 +6,21 @@
.field {
display : grid;
- grid-template-columns : 120px 150px;
+ grid-template-columns : 120px 200px;
align-items : center;
justify-items : stretch;
width : 100%;
margin-bottom : 20px;
-
-
+
input {
height : 33px;
padding : 0px 10px;
margin-bottom : unset;
font-family : monospace;
+
+ &[type="date"] {
+ width:14ch;
+ }
}
textarea {
diff --git a/client/admin/notificationUtils/notificationLookup/notificationLookup.less b/client/admin/notificationUtils/notificationLookup/notificationLookup.less
index 3f9b78310..830467368 100644
--- a/client/admin/notificationUtils/notificationLookup/notificationLookup.less
+++ b/client/admin/notificationUtils/notificationLookup/notificationLookup.less
@@ -1,8 +1,8 @@
-
.notificationLookup {
width : 450px;
height : fit-content;
+ .noNotification { margin-block : 20px; }
.notificationList {
display : flex;
flex-direction : column;
@@ -30,11 +30,6 @@
font-size : 20px;
font-weight : 900;
}
-
- dl dt{
- font-weight: 900;
- }
}
}
- .noNotification { margin-block : 20px; }
}
\ No newline at end of file
diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx
index 56633bbdd..ae9f1d7f8 100644
--- a/client/components/combobox.jsx
+++ b/client/components/combobox.jsx
@@ -45,6 +45,7 @@ const Combobox = createClass({
},
handleDropdown : function(show){
this.setState({
+ value : show ? '' : this.props.default,
showDropdown : show,
inputFocused : this.props.autoSuggest.clearAutoSuggestOnClick ? show : false
});
@@ -78,7 +79,7 @@ const Combobox = createClass({
if(!e.target.checkValidity()){
this.setState({
value : this.props.default
- }, ()=>this.props.onEntry(e));
+ });
}
}}
/>
diff --git a/client/homebrew/brewRenderer/headerNav/headerNav.jsx b/client/homebrew/brewRenderer/headerNav/headerNav.jsx
index 68963129f..cfc10bbd6 100644
--- a/client/homebrew/brewRenderer/headerNav/headerNav.jsx
+++ b/client/homebrew/brewRenderer/headerNav/headerNav.jsx
@@ -3,7 +3,6 @@ require('./headerNav.less');
import * as React from 'react';
import * as _ from 'lodash';
-
const MAX_TEXT_LENGTH = 40;
const HeaderNav = React.forwardRef(({}, pagesRef)=>{
@@ -11,11 +10,30 @@ const HeaderNav = React.forwardRef(({}, pagesRef)=>{
const renderHeaderLinks = ()=>{
if(!pagesRef.current) return;
+ // Top Level Pages
+ // Pages that contain an element with a specified class (e.g. cover pages, table of contents)
+ // will NOT have its content scanned for navigation headers, instead displaying a custom label
+ // ---
+ // The property name is class that will be used for detecting the page is a top level page
+ // The property value is a function that returns the text to be used
+
+ const topLevelPages = {
+ '.frontCover' : (el, pageType)=>{ const text = getHeaderContent(el); return text ? `Cover: ${text}` : 'Cover Page'; },
+ '.insideCover' : (el, pageType)=>{ const text = getHeaderContent(el); return text ? `Interior: ${text}` : 'Interior Cover Page'; },
+ '.partCover' : (el, pageType)=>{ const text = getHeaderContent(el); return text ? `Section: ${text}` : 'Section Cover Page'; },
+ '.backCover' : (el, pageType)=>{ const text = getHeaderContent(el); return text ? `Back: ${text}` : 'Rear Cover Page'; },
+ '.toc' : ()=>{ return 'Table of Contents'; },
+ };
+
+ const getHeaderContent = el => el.querySelector('h1')?.textContent;
+
+ const topLevelPageSelector = Object.keys(topLevelPages).join(',');
+
const selector = [
- '.pages > .page', // All page elements, which by definition have IDs
- '.page:not(:has(.toc)) > [id]', // All direct children of non-ToC .page with an ID (Legacy)
- '.page:not(:has(.toc)) > .columnWrapper > [id]', // All direct children of non-ToC .page > .columnWrapper with an ID (V3)
- '.page:not(:has(.toc)) h2', // All non-ToC H2 titles, like Monster frame titles
+ '.pages > .page', // All page elements, which by definition have IDs
+ `.page:not(:has(${topLevelPageSelector})) > [id]`, // All direct children of non-excluded .pages with an ID (Legacy)
+ `.page:not(:has(${topLevelPageSelector})) > .columnWrapper > [id]`, // All direct children of non-excluded .page > .columnWrapper with an ID (V3)
+ `.page:not(:has(${topLevelPageSelector})) h2`, // All non-excluded H2 titles, like Monster frame titles
];
const elements = pagesRef.current.querySelectorAll(selector.join(','));
if(!elements) return;
@@ -23,45 +41,37 @@ const HeaderNav = React.forwardRef(({}, pagesRef)=>{
// navList is a list of objects which have the following structure:
// {
- // depth : how deeply indented the item should be
- // text : the text to display in the nav link
- // link : the hyperlink to navigate to when clicked
- // className : [optional] the class to apply to the nav link for styling
+ // depth : how deeply indented the item should be
+ // text : the text to display in the nav link
+ // link : the hyperlink to navigate to when clicked
+ // className : [optional] the class to apply to the nav link for styling
// }
elements.forEach((el)=>{
- if(el.className.match(/\bpage\b/)) {
- let text = `Page ${el.id.slice(1)}`; // The ID of a page *should* always be equal to `p` followed by the page number
- if(el.querySelector('.toc')){ // If the page contains a table of contents, add "- Contents" to the display text
- text += ' - Contents';
- };
- navList.push({
- depth : 0, // Pages are always at the least indented level
- text : text,
- link : el.id,
- className : 'pageLink'
- });
- return;
- }
- if(el.localName.match(/^h[1-6]/)){ // Header elements H1 through H6
- navList.push({
- depth : el.localName[1], // Depth is set by the header level
- text : el.textContent, // Use `textContent` because `innerText` is affected by rendering, e.g. 'content-visibility: auto'
- link : el.id
- });
- return;
- }
- navList.push({
- depth : 7, // All unmatched elements with IDs are set to the maximum depth (7)
- text : el.textContent, // Use `textContent` because `innerText` is affected by rendering, e.g. 'content-visibility: auto'
+ const navEntry = { // Default structure of a navList entry
+ depth : 7, // All unmatched elements with IDs are set to the maximum depth (7)
+ text : el.textContent, // Use `textContent` because `innerText` is affected by rendering, e.g. 'content-visibility: auto'
link : el.id
- });
- });
-
- return _.map(navList, (navItem, index)=>{
- return
;
+ }
+ if(el.classList.contains('page')) {
+ let text = `Page ${el.id.slice(1)}`; // Get the page # by trimming off the 'p' from the ID
+ const pageType = Object.keys(topLevelPages).find(pageType => el.querySelector(pageType));
+ if (pageType)
+ text += ` - ${topLevelPages[pageType](el, pageType)}` // If a Top Level Page, add extra label
+
+ navEntry.depth = 0; // Pages are always at the least indented level
+ navEntry.text = text;
+ navEntry.className = 'pageLink';
+ }
+ else if(el.localName.match(/^h[1-6]/)){ // Header elements H1 through H6
+ navEntry.depth = el.localName[1]; // Depth is set by the header level
+ }
+ navList.push(navEntry);
});
+ return _.map(navList, (navItem, index)=>
+
+ );
};
return
@@ -69,8 +79,7 @@ const HeaderNav = React.forwardRef(({}, pagesRef)=>{
{renderHeaderLinks()}
;
-}
-);
+});
const HeaderNavItem = ({ link, text, depth, className })=>{
diff --git a/client/homebrew/brewRenderer/headerNav/headerNav.less b/client/homebrew/brewRenderer/headerNav/headerNav.less
index 8b35041d9..a5769ef99 100644
--- a/client/homebrew/brewRenderer/headerNav/headerNav.less
+++ b/client/homebrew/brewRenderer/headerNav/headerNav.less
@@ -35,11 +35,11 @@
font-weight: 900;
}
- @depths: 1,2,3,4,5,6,7;
+ @depths: 0,1,2,3,4,5,6,7;
each(@depths, {
&.depth-@{value} {
- padding-left: ((@value - 1) * 0.5em);
+ padding-left: ((@value) * 0.5em);
}
});
}
diff --git a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less
index be642f0fe..6180decc6 100644
--- a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less
+++ b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less
@@ -85,9 +85,4 @@
display : inline-block;
width : 100%;
}
- .blank {
- height : 1em;
- margin-top : 0;
- & + * { margin-top : 0; }
- }
}
\ No newline at end of file
diff --git a/client/homebrew/editor/editor.jsx b/client/homebrew/editor/editor.jsx
index 2d0a26268..9e6178f3e 100644
--- a/client/homebrew/editor/editor.jsx
+++ b/client/homebrew/editor/editor.jsx
@@ -456,6 +456,7 @@ const Editor = createClass({
rerenderParent={this.rerenderParent} />
diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx
index 9d2ec7db6..2a65d9384 100644
--- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx
+++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx
@@ -40,6 +40,7 @@ const MetadataEditor = createClass({
theme : '5ePHB',
lang : 'en'
},
+
onChange : ()=>{},
reportError : ()=>{}
};
@@ -47,7 +48,7 @@ const MetadataEditor = createClass({
getInitialState : function(){
return {
- showThumbnail : true
+ showThumbnail : true
};
},
@@ -67,6 +68,11 @@ const MetadataEditor = createClass({
const inputRules = validations[name] ?? [];
const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean);
+ const debouncedReportValidity = _.debounce((target, errMessage) => {
+ callIfExists(target, 'setCustomValidity', errMessage);
+ callIfExists(target, 'reportValidity');
+ }, 300); // 300ms debounce delay, adjust as needed
+
// if no validation rules, save to props
if(validationErr.length === 0){
callIfExists(e.target, 'setCustomValidity', '');
@@ -74,14 +80,16 @@ const MetadataEditor = createClass({
...this.props.metadata,
[name] : e.target.value
});
+ return true;
} else {
// if validation issues, display built-in browser error popup with each error.
const errMessage = validationErr.map((err)=>{
return `- ${err}`;
}).join('\n');
- callIfExists(e.target, 'setCustomValidity', errMessage);
- callIfExists(e.target, 'reportValidity');
+
+ debouncedReportValidity(e.target, errMessage);
+ return false;
}
},
@@ -112,6 +120,14 @@ const MetadataEditor = createClass({
handleTheme : function(theme){
this.props.metadata.renderer = theme.renderer;
this.props.metadata.theme = theme.path;
+
+ this.props.onChange(this.props.metadata, 'theme');
+ },
+
+ handleThemeWritein : function(e) {
+ const shareId = e.target.value.split('/').pop(); //Extract just the ID if a URL was pasted in
+ this.props.metadata.theme = shareId;
+
this.props.onChange(this.props.metadata, 'theme');
},
@@ -200,7 +216,7 @@ const MetadataEditor = createClass({
if(theme.path == this.props.metadata.shareId) return;
const preview = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`;
const texture = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`;
- return this.handleTheme(theme)} title={''}>
+ return
{theme.author ?? renderer} : {theme.name}
@@ -210,26 +226,40 @@ const MetadataEditor = createClass({
;
- });
+ }).filter(Boolean);
};
const currentRenderer = this.props.metadata.renderer;
- const currentTheme = mergedThemes[`${_.upperFirst(this.props.metadata.renderer)}`][this.props.metadata.theme]
- ?? { name: `!!! THEME MISSING !!! ID=${this.props.metadata.theme}` };
+ const currentThemeDisplay = this.props.themeBundle?.name ? `${this.props.themeBundle.author ?? currentRenderer} : ${this.props.themeBundle.name}` : 'No Theme Selected';
let dropdown;
if(currentRenderer == 'legacy') {
dropdown =
-
- {`Themes are not supported in the Legacy Renderer`}
- ;
+
+
Themes are not supported in the Legacy Renderer
+
;
} else {
dropdown =
-
- {currentTheme.author ?? _.upperFirst(currentRenderer)} : {currentTheme.name}
-
- {listThemes(currentRenderer)}
- ;
+
+ this.handleTheme(value)}
+ onEntry={(e)=>{
+ e.target.setCustomValidity(''); //Clear the validation popup while typing
+ if(this.handleFieldChange('theme', e))
+ this.handleThemeWritein(e);
+ }}
+ options={listThemes(currentRenderer)}
+ autoSuggest={{
+ suggestMethod : 'includes',
+ clearAutoSuggestOnClick : true,
+ filterOn : ['value', 'title']
+ }}
+ />
+ Select from the list below (built-in themes and brews you have tagged "meta:theme"), or paste in the Share URL or Share ID of any brew.
+
;
}
return
@@ -251,8 +281,6 @@ const MetadataEditor = createClass({
});
};
- const debouncedHandleFieldChange = _.debounce(this.handleFieldChange, 500);
-
return
language
@@ -263,7 +291,7 @@ const MetadataEditor = createClass({
onSelect={(value)=>this.handleLanguage(value)}
onEntry={(e)=>{
e.target.setCustomValidity(''); //Clear the validation popup while typing
- debouncedHandleFieldChange('lang', e);
+ this.handleFieldChange('lang', e);
}}
options={listLanguages()}
autoSuggest={{
@@ -271,8 +299,7 @@ const MetadataEditor = createClass({
clearAutoSuggestOnClick : true,
filterOn : ['value', 'detail', 'title']
}}
- >
-
+ />
Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.
@@ -345,7 +372,7 @@ const MetadataEditor = createClass({
placeholder='add tag' unique={true}
values={this.props.metadata.tags}
onChange={(e)=>this.handleFieldChange('tags', e)}
- />
+ />
systems
@@ -370,7 +397,7 @@ const MetadataEditor = createClass({
values={this.props.metadata.invitedAuthors}
notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}
- />
+ />
Privacy
diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.less b/client/homebrew/editor/metadataEditor/metadataEditor.less
index 2cff01cfe..c5658a796 100644
--- a/client/homebrew/editor/metadataEditor/metadataEditor.less
+++ b/client/homebrew/editor/metadataEditor/metadataEditor.less
@@ -1,9 +1,12 @@
@import 'naturalcrit/styles/colors.less';
+.userThemeName {
+ padding-left: 10px;
+ padding-right: 10px;
+}
.metadataEditor {
position : absolute;
- z-index : 5;
box-sizing : border-box;
width : 100%;
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
@@ -71,8 +74,7 @@
border : 1px solid gray;
&:focus { outline : 1px solid #444444; }
}
- &.thumbnail {
- height : 1.4em;
+ &.thumbnail, &.themes{
label { line-height : 2.0em; }
.value {
overflow : hidden;
@@ -88,6 +90,17 @@
}
}
+ &.themes{
+ .value {
+ overflow : visible;
+ text-overflow : auto;
+ }
+ button {
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+ }
+
&.description {
flex : 1;
textarea.value {
@@ -156,89 +169,73 @@
}
.themes.field {
- .navDropdownContainer {
+ & .dropdown-container {
position : relative;
z-index : 100;
background-color : white;
- &.disabled {
- font-style : italic;
- color : dimgray;
- background-color : darkgray;
- }
- & > div:first-child {
- padding : 3px 3px;
- background-color : inherit;
- border : 1px solid gray;
- i { float : right; }
- &:hover {
- color : white;
- background-color : @blue;
+ }
+ & .dropdown-options {
+ overflow-y : visible;
+ }
+ .disabled {
+ font-style : italic;
+ color : dimgray;
+ background-color : darkgray;
+ }
+ .item {
+ position : relative;
+ padding : 3px 3px;
+ overflow : visible;
+ background-color : white;
+ border-top : 1px solid rgb(118, 118, 118);
+ .preview {
+ position : absolute;
+ top : 0;
+ right : 0;
+ z-index : 1;
+ display : flex;
+ flex-direction : column;
+ width : 200px;
+ overflow : hidden;
+ color : black;
+ background : #CCCCCC;
+ border-radius : 5px;
+ box-shadow : 0 0 5px black;
+ opacity : 0;
+ transition : opacity 250ms ease;
+ h6 {
+ padding-block : 0.5em;
+ padding-inline : 1em;
+ font-weight : 900;
+ border-bottom : 2px solid hsl(0,0%,40%);
}
}
- .navDropdown .item > p {
- width : 45%;
- height : 1.1em;
- overflow : hidden;
- text-overflow : ellipsis;
- white-space : nowrap;
- }
- .navDropdown {
- position : absolute;
- width : 100%;
- box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
- .item {
- position : relative;
- padding : 3px 3px;
- overflow : visible;
- background-color : white;
- border-top : 1px solid rgb(118, 118, 118);
- .preview {
- position : absolute;
- top : 0;
- right : 0;
- z-index : 1;
- display : flex;
- flex-direction : column;
- width : 200px;
- overflow : hidden;
- color : black;
- background : #CCCCCC;
- border-radius : 5px;
- box-shadow : 0 0 5px black;
- opacity : 0;
- transition : opacity 250ms ease;
- h6 {
- padding-block : 0.5em;
- padding-inline : 1em;
- font-weight : 900;
- border-bottom : 2px solid hsl(0,0%,40%);
- }
- }
- &:hover {
- color : white;
- background-color : @blue;
- }
- &:hover > .preview { opacity : 1; }
- .texture-container {
- position : absolute;
- top : 0;
- left : 0;
- width : 100%;
- height : 100%;
- min-height : 100%;
- overflow : hidden;
- > img {
- position : absolute;
- top : 0px;
- right : 0;
- width : 50%;
- min-height : 100%;
- -webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
- mask-image : linear-gradient(90deg, transparent, black 20%);
- }
- }
+
+ .texture-container {
+ position : absolute;
+ top : 0;
+ left : 0;
+ width : 100%;
+ height : 100%;
+ min-height : 100%;
+ overflow : hidden;
+ > img {
+ position : absolute;
+ top : 0;
+ right : 0;
+ width : 50%;
+ min-height : 100%;
+ -webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
+ mask-image : linear-gradient(90deg, transparent, black 20%);
}
}
+
+ &:hover {
+ color : white;
+ background-color : @blue;
+ filter : unset;
+ }
+ &:hover > .preview { opacity : 1; }
}
}
diff --git a/client/homebrew/editor/metadataEditor/validations.js b/client/homebrew/editor/metadataEditor/validations.js
index 32c8131f6..b475783a4 100644
--- a/client/homebrew/editor/metadataEditor/validations.js
+++ b/client/homebrew/editor/metadataEditor/validations.js
@@ -27,6 +27,19 @@ module.exports = {
(value)=>{
return new RegExp(/^([a-zA-Z]{2,3})(-[a-zA-Z]{4})?(-(?:[0-9]{3}|[a-zA-Z]{2}))?$/).test(value) === false && (value.length > 0) ? 'Invalid language code.' : null;
}
+ ],
+ theme: [
+ (value) => {
+ const URL = global.config.baseUrl.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); //Escape any regex characters
+ const shareIDPattern = '[a-zA-Z0-9-_]{12}';
+ const shareURLRegex = new RegExp(`^${URL}\\/share\\/${shareIDPattern}$`);
+ const shareIDRegex = new RegExp(`^${shareIDPattern}$`);
+ if (value?.length === 0) return null;
+ if (shareURLRegex.test(value)) return null;
+ if (shareIDRegex.test(value)) return null;
+
+ return 'Must be a valid Share URL or a 12-character ID.';
+ }
]
};
diff --git a/client/homebrew/navbar/error-navitem.jsx b/client/homebrew/navbar/error-navitem.jsx
index f6788e6d5..3de26ca56 100644
--- a/client/homebrew/navbar/error-navitem.jsx
+++ b/client/homebrew/navbar/error-navitem.jsx
@@ -116,6 +116,19 @@ const ErrorNavItem = createClass({
;
}
+ if(HBErrorCode === '10') {
+ return
+ Oops!
+
+ Looks like the brew you have selected
+ as a theme is not tagged for use as a
+ theme. Verify that
+ brew
+ {response.body.brewId} has the
meta:theme tag!
+
+ ;
+ }
+
return
Oops!
diff --git a/client/homebrew/pages/basePages/uiPage/uiPage.less b/client/homebrew/pages/basePages/uiPage/uiPage.less
index 913c74a2e..49cb8311d 100644
--- a/client/homebrew/pages/basePages/uiPage/uiPage.less
+++ b/client/homebrew/pages/basePages/uiPage/uiPage.less
@@ -59,11 +59,6 @@
padding-left : 1.25em;
list-style : square;
}
- .blank {
- height : 1em;
- margin-top : 0;
- & + * { margin-top : 0; }
- }
}
}
}
\ No newline at end of file
diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx
index ffb6a6b40..4db6d6e23 100644
--- a/client/homebrew/pages/editPage/editPage.jsx
+++ b/client/homebrew/pages/editPage/editPage.jsx
@@ -379,7 +379,7 @@ const EditPage = createClass({
const title = `${this.props.brew.title} ${systems}`;
const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
-**[Homebrewery Link](${global.config.publicUrl}/share/${shareLink})**`;
+**[Homebrewery Link](${global.config.baseUrl}/share/${shareLink})**`;
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`;
},
@@ -410,7 +410,7 @@ const EditPage = createClass({
view
-
{navigator.clipboard.writeText(`${global.config.publicUrl}/share/${shareLink}`);}}>
+ {navigator.clipboard.writeText(`${global.config.baseUrl}/share/${shareLink}`);}}>
copy url
@@ -443,6 +443,7 @@ const EditPage = createClass({
reportError={this.errorReported}
renderer={this.state.brew.renderer}
userThemes={this.props.userThemes}
+ themeBundle={this.state.themeBundle}
snippetBundle={this.state.themeBundle.snippets}
updateBrew={this.updateBrew}
onCursorPageChange={this.handleEditorCursorPageChange}
diff --git a/client/homebrew/pages/errorPage/errors/errorIndex.js b/client/homebrew/pages/errorPage/errors/errorIndex.js
index 2c7be5d4b..c2c49f958 100644
--- a/client/homebrew/pages/errorPage/errors/errorIndex.js
+++ b/client/homebrew/pages/errorPage/errors/errorIndex.js
@@ -167,6 +167,14 @@ const errorIndex = (props)=>{
**Requested access:** ${props.brew.accessType}
**Brew ID:** ${props.brew.brewId}`,
+
+ // Theme Not Valid
+ '10' : dedent`
+ ## The selected theme is not tagged as a theme.
+
+ The brew selected as a theme exists, but has not been marked for use as a theme with the \`theme:meta\` tag.
+
+ If the selected brew is your document, you may designate it as a theme by adding the \`theme:meta\` tag.`,
//account page when account is not defined
'50' : dedent`
diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx
index ee2c67d5f..1d5887b8a 100644
--- a/client/homebrew/pages/newPage/newPage.jsx
+++ b/client/homebrew/pages/newPage/newPage.jsx
@@ -233,6 +233,7 @@ const NewPage = createClass({
onMetaChange={this.handleMetaChange}
renderer={this.state.brew.renderer}
userThemes={this.props.userThemes}
+ themeBundle={this.state.themeBundle}
snippetBundle={this.state.themeBundle.snippets}
onCursorPageChange={this.handleEditorCursorPageChange}
onViewPageChange={this.handleEditorViewPageChange}
diff --git a/package-lock.json b/package-lock.json
index e78af217c..1d1658dd9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,9 +10,9 @@
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
- "@babel/core": "^7.26.8",
- "@babel/plugin-transform-runtime": "^7.26.8",
- "@babel/preset-env": "^7.26.8",
+ "@babel/core": "^7.26.9",
+ "@babel/plugin-transform-runtime": "^7.26.9",
+ "@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.26.3",
"@googleapis/drive": "^8.14.0",
"body-parser": "^1.20.2",
@@ -40,15 +40,16 @@
"marked-gfm-heading-id": "^4.0.1",
"marked-nonbreaking-spaces": "^1.0.0",
"marked-smartypants-lite": "^1.0.3",
+ "marked-subsuper-text": "^1.0.3",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1",
- "mongoose": "^8.10.0",
- "nanoid": "5.0.9",
+ "mongoose": "^8.10.1",
+ "nanoid": "5.1.0",
"nconf": "^0.12.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-frame-component": "^4.1.3",
- "react-router": "^7.1.5",
+ "react-router": "^7.2.0",
"sanitize-filename": "1.6.3",
"superagent": "^10.1.1",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
@@ -59,7 +60,7 @@
"eslint": "^9.20.1",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-react": "^7.37.4",
- "globals": "^15.14.0",
+ "globals": "^15.15.0",
"jest": "^29.7.0",
"jest-expect-message": "^1.1.3",
"jsdom-global": "^3.0.2",
@@ -111,22 +112,21 @@
}
},
"node_modules/@babel/core": {
- "version": "7.26.8",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.8.tgz",
- "integrity": "sha512-l+lkXCHS6tQEc5oUpK28xBOZ6+HwaH7YwoYQbLFiYb4nS2/l1tKnZEtEWkD0GuiYdvArf9qBS0XlQGXzPMsNqQ==",
+ "version": "7.26.9",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz",
+ "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==",
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.2",
- "@babel/generator": "^7.26.8",
+ "@babel/generator": "^7.26.9",
"@babel/helper-compilation-targets": "^7.26.5",
"@babel/helper-module-transforms": "^7.26.0",
- "@babel/helpers": "^7.26.7",
- "@babel/parser": "^7.26.8",
- "@babel/template": "^7.26.8",
- "@babel/traverse": "^7.26.8",
- "@babel/types": "^7.26.8",
- "@types/gensync": "^1.0.0",
+ "@babel/helpers": "^7.26.9",
+ "@babel/parser": "^7.26.9",
+ "@babel/template": "^7.26.9",
+ "@babel/traverse": "^7.26.9",
+ "@babel/types": "^7.26.9",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -142,13 +142,13 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.26.8",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.8.tgz",
- "integrity": "sha512-ef383X5++iZHWAXX0SXQR6ZyQhw/0KtTkrTz61WXRhFM6dhpHulO/RJz79L8S6ugZHJkOOkUrUdxgdF2YiPFnA==",
+ "version": "7.26.9",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz",
+ "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==",
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.26.8",
- "@babel/types": "^7.26.8",
+ "@babel/parser": "^7.26.9",
+ "@babel/types": "^7.26.9",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^3.0.2"
@@ -378,25 +378,25 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.26.7",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz",
- "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==",
+ "version": "7.26.9",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz",
+ "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==",
"license": "MIT",
"dependencies": {
- "@babel/template": "^7.25.9",
- "@babel/types": "^7.26.7"
+ "@babel/template": "^7.26.9",
+ "@babel/types": "^7.26.9"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
- "version": "7.26.8",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.8.tgz",
- "integrity": "sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==",
+ "version": "7.26.9",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz",
+ "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.26.8"
+ "@babel/types": "^7.26.9"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -975,11 +975,12 @@
}
},
"node_modules/@babel/plugin-transform-for-of": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz",
- "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==",
+ "version": "7.26.9",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz",
+ "integrity": "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.25.9",
+ "@babel/helper-plugin-utils": "^7.26.5",
"@babel/helper-skip-transparent-expression-wrappers": "^7.25.9"
},
"engines": {
@@ -1407,9 +1408,9 @@
}
},
"node_modules/@babel/plugin-transform-runtime": {
- "version": "7.26.8",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.26.8.tgz",
- "integrity": "sha512-H0jlQxFMI0Q8SyGPsj9pO3ygVQRxPkIGytsL3m1Zqca8KrCPpMlvh+e2dxknqdfS8LFwBw+PpiYPD9qy/FPQpA==",
+ "version": "7.26.9",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.26.9.tgz",
+ "integrity": "sha512-Jf+8y9wXQbbxvVYTM8gO5oEF2POdNji0NMltEkG7FtmzD9PVz7/lxpqSdTvwsjTMU5HIHuDVNf2SOxLkWi+wPQ==",
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.25.9",
@@ -1559,9 +1560,9 @@
}
},
"node_modules/@babel/preset-env": {
- "version": "7.26.8",
- "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.8.tgz",
- "integrity": "sha512-um7Sy+2THd697S4zJEfv/U5MHGJzkN2xhtsR3T/SWRbVSic62nbISh51VVfU9JiO/L/Z97QczHTaFVkOU8IzNg==",
+ "version": "7.26.9",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz",
+ "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==",
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.26.8",
@@ -1593,7 +1594,7 @@
"@babel/plugin-transform-dynamic-import": "^7.25.9",
"@babel/plugin-transform-exponentiation-operator": "^7.26.3",
"@babel/plugin-transform-export-namespace-from": "^7.25.9",
- "@babel/plugin-transform-for-of": "^7.25.9",
+ "@babel/plugin-transform-for-of": "^7.26.9",
"@babel/plugin-transform-function-name": "^7.25.9",
"@babel/plugin-transform-json-strings": "^7.25.9",
"@babel/plugin-transform-literals": "^7.25.9",
@@ -1700,30 +1701,30 @@
}
},
"node_modules/@babel/template": {
- "version": "7.26.8",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.8.tgz",
- "integrity": "sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==",
+ "version": "7.26.9",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
+ "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.26.2",
- "@babel/parser": "^7.26.8",
- "@babel/types": "^7.26.8"
+ "@babel/parser": "^7.26.9",
+ "@babel/types": "^7.26.9"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.26.8",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.8.tgz",
- "integrity": "sha512-nic9tRkjYH0oB2dzr/JoGIm+4Q6SuYeLEiIiZDwBscRMYFJ+tMAz98fuel9ZnbXViA2I0HVSSRRK8DW5fjXStA==",
+ "version": "7.26.9",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz",
+ "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.26.2",
- "@babel/generator": "^7.26.8",
- "@babel/parser": "^7.26.8",
- "@babel/template": "^7.26.8",
- "@babel/types": "^7.26.8",
+ "@babel/generator": "^7.26.9",
+ "@babel/parser": "^7.26.9",
+ "@babel/template": "^7.26.9",
+ "@babel/types": "^7.26.9",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
@@ -1741,9 +1742,9 @@
}
},
"node_modules/@babel/types": {
- "version": "7.26.8",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.8.tgz",
- "integrity": "sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==",
+ "version": "7.26.9",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz",
+ "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
@@ -2872,12 +2873,6 @@
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true
},
- "node_modules/@types/gensync": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.4.tgz",
- "integrity": "sha512-C3YYeRQWp2fmq9OryX+FoDy8nXS6scQ7dPptD8LnFDAUNcKWJjXQKDNJD3HVm+kOUsXhTOkpi69vI4EuAr95bA==",
- "license": "MIT"
- },
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@@ -6814,10 +6809,11 @@
}
},
"node_modules/globals": {
- "version": "15.14.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz",
- "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==",
+ "version": "15.15.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
+ "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=18"
},
@@ -9893,6 +9889,15 @@
"marked": ">=4 <16"
}
},
+ "node_modules/marked-subsuper-text": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/marked-subsuper-text/-/marked-subsuper-text-1.0.3.tgz",
+ "integrity": "sha512-v5hVVJo6L7HQtplIT8OYNbRWMCGupXYuZ7U9qTsC4yLDtfw24oM5xmWVYfzqzX6hD7KneMfDssMPt6U7fslbxQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "marked": ">=3 <16"
+ }
+ },
"node_modules/markedLegacy": {
"name": "marked",
"version": "0.3.19",
@@ -10248,9 +10253,9 @@
}
},
"node_modules/mongoose": {
- "version": "8.10.0",
- "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.10.0.tgz",
- "integrity": "sha512-nLhk3Qrv6q/HpD2k1O7kbBqsq+/kmKpdv5KJ+LLhQlII3e1p/SSLoLP6jMuSiU6+iLK7zFw4T1niAk3mA3QVug==",
+ "version": "8.10.1",
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.10.1.tgz",
+ "integrity": "sha512-5beTeBZnJNndRXU9rxPol0JmTWZMAtgkPbooROkGilswvrZALDERY4cJrGZmgGwDS9dl0mxiB7si+Mv9Yms2fg==",
"license": "MIT",
"dependencies": {
"bson": "^6.10.1",
@@ -10403,9 +10408,9 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/nanoid": {
- "version": "5.0.9",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
- "integrity": "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.0.tgz",
+ "integrity": "sha512-zDAl/llz8Ue/EblwSYwdxGBYfj46IM1dhjVi8dyp9LQffoIGxJEAHj2oeZ4uNcgycSRcQ83CnfcZqEJzVDLcDw==",
"funding": [
{
"type": "github",
@@ -11676,9 +11681,9 @@
"license": "MIT"
},
"node_modules/react-router": {
- "version": "7.1.5",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.5.tgz",
- "integrity": "sha512-8BUF+hZEU4/z/JD201yK6S+UYhsf58bzYIDq2NS1iGpwxSXDu7F+DeGSkIXMFBuHZB21FSiCzEcUb18cQNdRkA==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.2.0.tgz",
+ "integrity": "sha512-fXyqzPgCPZbqhrk7k3hPcCpYIlQ2ugIXDboHUzhJISFVy2DEPsmHgN588MyGmkIOv3jDgNfUE3kJi83L28s/LQ==",
"license": "MIT",
"dependencies": {
"@types/cookie": "^0.6.0",
diff --git a/package.json b/package.json
index 20999aa4d..e73471556 100644
--- a/package.json
+++ b/package.json
@@ -84,9 +84,9 @@
]
},
"dependencies": {
- "@babel/core": "^7.26.8",
- "@babel/plugin-transform-runtime": "^7.26.8",
- "@babel/preset-env": "^7.26.8",
+ "@babel/core": "^7.26.9",
+ "@babel/plugin-transform-runtime": "^7.26.9",
+ "@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.26.3",
"@googleapis/drive": "^8.14.0",
"body-parser": "^1.20.2",
@@ -114,15 +114,16 @@
"marked-gfm-heading-id": "^4.0.1",
"marked-nonbreaking-spaces": "^1.0.0",
"marked-smartypants-lite": "^1.0.3",
+ "marked-subsuper-text": "^1.0.3",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1",
- "mongoose": "^8.10.0",
- "nanoid": "5.0.9",
+ "mongoose": "^8.10.1",
+ "nanoid": "5.1.0",
"nconf": "^0.12.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-frame-component": "^4.1.3",
- "react-router": "^7.1.5",
+ "react-router": "^7.2.0",
"sanitize-filename": "1.6.3",
"superagent": "^10.1.1",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
@@ -133,7 +134,7 @@
"eslint": "^9.20.1",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-react": "^7.37.4",
- "globals": "^15.14.0",
+ "globals": "^15.15.0",
"jest": "^29.7.0",
"jest-expect-message": "^1.1.3",
"jsdom-global": "^3.0.2",
diff --git a/server/admin.api.js b/server/admin.api.js
index a7d1d9811..e70268afa 100644
--- a/server/admin.api.js
+++ b/server/admin.api.js
@@ -147,7 +147,6 @@ router.put('/admin/compress/:id', (req, res)=>{
});
});
-
router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
try {
const totalBrewsCount = await HomebrewModel.countDocuments({});
diff --git a/server/app.js b/server/app.js
index 4dec6b4c4..76caf6fed 100644
--- a/server/app.js
+++ b/server/app.js
@@ -552,6 +552,7 @@ const renderPage = async (req, res)=>{
const configuration = {
local : isLocalEnvironment,
publicUrl : config.get('publicUrl') ?? '',
+ baseUrl : `${req.protocol}://${req.get('host')}`,
environment : nodeEnv,
deployment : config.get('heroku_app_name') ?? ''
};
diff --git a/server/homebrew.api.js b/server/homebrew.api.js
index 9a479732e..7bd88cbdb 100644
--- a/server/homebrew.api.js
+++ b/server/homebrew.api.js
@@ -1,6 +1,6 @@
/* eslint-disable max-lines */
import _ from 'lodash';
-import {model as HomebrewModel} from './homebrew.model.js';
+import { model as HomebrewModel } from './homebrew.model.js';
import express from 'express';
import zlib from 'zlib';
import GoogleActions from './googleActions.js';
@@ -279,6 +279,8 @@ const api = {
let currentTheme;
const completeStyles = [];
const completeSnippets = [];
+ let themeName;
+ let themeAuthor;
while (req.params.id) {
//=== User Themes ===//
@@ -292,6 +294,10 @@ const api = {
currentTheme = req.brew;
splitTextStyleAndMetadata(currentTheme);
+ if(!currentTheme.tags.some(tag => tag === "meta:theme" || tag === "meta:Theme"))
+ throw { brewId: req.params.id, name: 'Invalid Theme Selected', message: 'Selected theme does not have the meta:theme tag', status: 422, HBErrorCode: '10' };
+ themeName ??= currentTheme.title;
+ themeAuthor ??= currentTheme.authors?.[0];
// If there is anything in the snippets or style members, append them to the appropriate array
if(currentTheme?.snippets) completeSnippets.push(JSON.parse(currentTheme.snippets));
@@ -301,6 +307,7 @@ const api = {
req.params.renderer = currentTheme.renderer;
} else {
//=== Static Themes ===//
+ themeName ??= req.params.id;
const localSnippets = `${req.params.renderer}_${req.params.id}`; // Just log the name for loading on client
const localStyle = `@import url(\"/themes/${req.params.renderer}/${req.params.id}/style.css\");`;
completeSnippets.push(localSnippets);
@@ -313,7 +320,9 @@ const api = {
const returnObj = {
// Reverse the order of the arrays so they are listed oldest parent to youngest child.
styles : completeStyles.reverse(),
- snippets : completeSnippets.reverse()
+ snippets : completeSnippets.reverse(),
+ name : themeName,
+ author : themeAuthor
};
res.setHeader('Content-Type', 'application/json');
diff --git a/server/homebrew.api.spec.js b/server/homebrew.api.spec.js
index 8270b1568..1c42cb4be 100644
--- a/server/homebrew.api.spec.js
+++ b/server/homebrew.api.spec.js
@@ -576,7 +576,7 @@ brew`);
describe('Theme bundle', ()=>{
it('should return Theme Bundle for a User Theme', async ()=>{
const brews = {
- userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: null, shareId: 'userThemeAID', style: 'User Theme A Style' }
+ userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: null, shareId: 'userThemeAID', style: 'User Theme A Style', tags: ['meta:theme'], authors: ['authorName'] }
};
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
@@ -587,6 +587,8 @@ brew`);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith({
+ name : 'User Theme A',
+ author : 'authorName',
styles : ['/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'],
snippets : []
});
@@ -594,9 +596,9 @@ brew`);
it('should return Theme Bundle for nested User Themes', async ()=>{
const brews = {
- userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style' },
- userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style' },
- userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: null, shareId: 'userThemeCID', style: 'User Theme C Style' }
+ userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style', tags: ['meta:theme'], authors: ['authorName'] },
+ userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style', tags: ['meta:theme'], authors: ['authorName'] },
+ userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: null, shareId: 'userThemeCID', style: 'User Theme C Style', tags: ['meta:theme'], authors: ['authorName'] }
};
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
@@ -607,6 +609,8 @@ brew`);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith({
+ name : 'User Theme A',
+ author : 'authorName',
styles : [
'/* From Brew: https://localhost/share/userThemeCID */\n\nUser Theme C Style',
'/* From Brew: https://localhost/share/userThemeBID */\n\nUser Theme B Style',
@@ -623,6 +627,8 @@ brew`);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith({
+ name : '5ePHB',
+ author : undefined,
styles : [
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`
@@ -636,9 +642,9 @@ brew`);
it('should return Theme Bundle for nested User and Static Themes together', async ()=>{
const brews = {
- userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style' },
- userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style' },
- userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: '5eDMG', shareId: 'userThemeCID', style: 'User Theme C Style' }
+ userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style', tags: ['meta:theme'], authors: ['authorName'] },
+ userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style', tags: ['meta:theme'], authors: ['authorName'] },
+ userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: '5eDMG', shareId: 'userThemeCID', style: 'User Theme C Style', tags: ['meta:theme'], authors: ['authorName'] }
};
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
@@ -649,6 +655,8 @@ brew`);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith({
+ name : 'User Theme A',
+ author : 'authorName',
styles : [
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`,
@@ -665,9 +673,9 @@ brew`);
});
});
- it('should fail for an invalid Theme in the chain', async()=>{
+ it('should fail for a missing Theme in the chain', async()=>{
const brews = {
- userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'missingTheme', shareId: 'userThemeAID', style: 'User Theme A Style' },
+ userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'missingTheme', shareId: 'userThemeAID', style: 'User Theme A Style', tags: ['meta:theme'], authors: ['authorName'] },
};
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
@@ -686,6 +694,27 @@ brew`);
name : 'ThemeLoad Error',
status : 404 });
});
+
+ it('should fail for a User Theme not tagged with meta:theme', async ()=>{
+ const brews = {
+ userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: null, shareId: 'userThemeAID', style: 'User Theme A Style' }
+ };
+
+ const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
+ model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
+ const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
+
+ let err;
+ await api.getThemeBundle(req, res)
+ .catch((e)=>err = e);
+
+ expect(err).toEqual({
+ HBErrorCode : '10',
+ brewId : 'userThemeAID',
+ message : 'Selected theme does not have the meta:theme tag',
+ name : 'Invalid Theme Selected',
+ status : 422 });
+ });
});
describe('deleteBrew', ()=>{
diff --git a/shared/helpers.js b/shared/helpers.js
index b2190cdcd..2ad9218fa 100644
--- a/shared/helpers.js
+++ b/shared/helpers.js
@@ -44,13 +44,19 @@ const fetchThemeBundle = async (obj, renderer, theme)=>{
.catch((err)=>{
obj.setState({ error: err });
});
- if(!res) return;
-
+ if(!res) {
+ obj.setState((prevState)=>({
+ ...prevState,
+ themeBundle : {}
+ }));
+ return;
+ }
const themeBundle = res.body;
themeBundle.joinedStyles = themeBundle.styles.map((style)=>``).join('\n\n');
obj.setState((prevState)=>({
...prevState,
- themeBundle : themeBundle
+ themeBundle : themeBundle,
+ error : null
}));
};
diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js
index 6fc03b379..3555d145c 100644
--- a/shared/naturalcrit/markdown.js
+++ b/shared/naturalcrit/markdown.js
@@ -8,6 +8,7 @@ import { markedSmartypantsLite as MarkedSmartypantsLite }
import { gfmHeadingId as MarkedGFMHeadingId, resetHeadings as MarkedGFMResetHeadingIDs } from 'marked-gfm-heading-id';
import { markedEmoji as MarkedEmojis } from 'marked-emoji';
import MarkedNonbreakingSpaces from 'marked-nonbreaking-spaces';
+import MarkedSubSuperText from 'marked-subsuper-text';
//Icon fonts included so they can appear in emoji autosuggest dropdown
import diceFont from '../../themes/fonts/iconFonts/diceFont.js';
@@ -334,35 +335,6 @@ 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 justifiedParagraphClasses = [];
justifiedParagraphClasses[2] = 'Left';
justifiedParagraphClasses[4] = 'Right';
@@ -416,7 +388,7 @@ const forcedParagraphBreaks = {
}
},
renderer(token) {
- return `
`.repeat(token.length).concat('\n');
+ return ` \n`.repeat(token.length);
}
};
@@ -776,8 +748,9 @@ const tableTerminators = [
Marked.use(MarkedVariables());
Marked.use({ extensions : [justifiedParagraphs, definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks,
- superSubScripts, mustacheSpans, mustacheDivs, mustacheInjectInline] });
+ mustacheSpans, mustacheDivs, mustacheInjectInline] });
Marked.use(mustacheInjectBlock);
+Marked.use(MarkedSubSuperText());
Marked.use(MarkedNonbreakingSpaces());
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
Marked.use(MarkedExtendedTables(tableTerminators), MarkedGFMHeadingId({ globalSlugs: true }),
diff --git a/tests/markdown/definition-lists.test.js b/tests/markdown/definition-lists.test.js
index 039754ca1..4c754db48 100644
--- a/tests/markdown/definition-lists.test.js
+++ b/tests/markdown/definition-lists.test.js
@@ -92,12 +92,12 @@ describe('Multiline Definition Lists', ()=>{
test('Multiline Definition Term must have at least one non-empty Definition', function() {
const source = 'Term 1\n::';
const rendered = Markdown.render(source).trim();
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`Term 1
\n
`);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`Term 1
\n \n `);
});
test('Multiline Definition List must have at least one non-newline character after ::', function() {
const source = 'Term 1\n::\nDefinition 1\n\n';
const rendered = Markdown.render(source).trim();
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`Term 1
\n
\nDefinition 1
`);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`Term 1
\n \n \nDefinition 1
`);
});
});
diff --git a/tests/markdown/hard-breaks.test.js b/tests/markdown/hard-breaks.test.js
index 8af102716..fe646eac9 100644
--- a/tests/markdown/hard-breaks.test.js
+++ b/tests/markdown/hard-breaks.test.js
@@ -6,37 +6,37 @@ describe('Hard Breaks', ()=>{
test('Single Break', function() {
const source = ':\n\n';
const rendered = Markdown.render(source).trim();
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(` `);
});
test('Double Break', function() {
const source = '::\n\n';
const rendered = Markdown.render(source).trim();
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(` \n `);
});
test('Triple Break', function() {
const source = ':::\n\n';
const rendered = Markdown.render(source).trim();
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(` \n \n `);
});
test('Many Break', function() {
const source = '::::::::::\n\n';
const rendered = Markdown.render(source).trim();
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(` \n \n \n \n \n \n \n \n \n `);
});
test('Multiple sets of Breaks', function() {
const source = ':::\n:::\n:::';
const rendered = Markdown.render(source).trim();
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
\n
\n
`);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(` \n \n \n \n \n \n \n \n `);
});
test('Break directly between two paragraphs', function() {
const source = 'Line 1\n::\nLine 2';
const rendered = Markdown.render(source).trim();
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`Line 1
\n
\nLine 2
`);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`Line 1
\n \n \nLine 2
`);
});
test('Ignored inside a code block', function() {
diff --git a/themes/V3/5ePHB/style.less b/themes/V3/5ePHB/style.less
index ba975e58a..5eebcb12e 100644
--- a/themes/V3/5ePHB/style.less
+++ b/themes/V3/5ePHB/style.less
@@ -17,7 +17,6 @@
.useSansSerif() {
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; }
@@ -622,7 +621,6 @@
left : 0;
filter : drop-shadow(0 0 0.075cm black);
img {
- width : 100%;
height : 2cm;
}
}
@@ -667,7 +665,6 @@
left : 0;
height : 2cm;
img {
- width : 100%;
height : 2cm;
}
}
@@ -679,18 +676,17 @@
padding : 2.25cm 1.3cm 2cm 1.3cm;
color : #FFFFFF;
columns : 1;
+ line-height : 1.4em;
&::after { display : none; }
.columnWrapper { width : 7.6cm; }
.backCover {
position : absolute;
inset : 0;
z-index : -1;
- width : 11cm;
background-image : @backCover;
background-repeat : no-repeat;
background-size : contain;
}
- .blank { height : 1.4em; }
h1 {
margin-bottom : 0.3cm;
font-family : 'NodestoCapsCondensed';
@@ -737,7 +733,6 @@
img {
position : relative;
z-index : 0;
- width : 100%;
height : 1.5cm;
}
p {
diff --git a/themes/V3/Blank/style.less b/themes/V3/Blank/style.less
index 65eeee683..1a22db6a7 100644
--- a/themes/V3/Blank/style.less
+++ b/themes/V3/Blank/style.less
@@ -427,17 +427,6 @@ body { counter-reset : page-numbers 0; }
}
}
-//*****************************
-// * BLANK LINE
-// *****************************/
-.page {
- .blank {
- height : 1em;
- margin-top : 0;
- & + * { margin-top : 0; }
- }
-}
-
//*****************************
// * WIDE
// *****************************/