diff --git a/client/admin/admin.jsx b/client/admin/admin.jsx index f2f2667a4..95c296259 100644 --- a/client/admin/admin.jsx +++ b/client/admin/admin.jsx @@ -1,47 +1,48 @@ -require('./admin.less'); -const React = require('react'); -const createClass = require('create-react-class'); - +import './admin.less'; +import React, { useEffect, useState } from 'react'; const BrewUtils = require('./brewUtils/brewUtils.jsx'); const NotificationUtils = require('./notificationUtils/notificationUtils.jsx'); +import AuthorUtils from './authorUtils/authorUtils.jsx'; -const tabGroups = ['brew', 'notifications']; +const tabGroups = ['brew', 'notifications', 'authors']; -const Admin = createClass({ - getDefaultProps : function() { - return {}; - }, +const Admin = ()=>{ + const [currentTab, setCurrentTab] = useState('brew'); - getInitialState : function(){ - return ({ - currentTab : 'brew' - }); - }, + useEffect(()=>{ + setCurrentTab(localStorage.getItem('hbAdminTab')); + }, []); - handleClick : function(newTab){ - if(this.state.currentTab === newTab) return; - this.setState({ - currentTab : newTab - }); - }, + useEffect(()=>{ + localStorage.setItem('hbAdminTab', currentTab); + }, [currentTab]); - render : function(){ - return
+ return ( +
- homebrewery admin + The Homebrewery Admin Page + back to homepage
- {this.state.currentTab==='brew' && } - {this.state.currentTab==='notifications' && } + {currentTab === 'brew' && } + {currentTab === 'notifications' && } + {currentTab === 'authors' && }
-
; - } -}); +
+ ); +}; module.exports = Admin; diff --git a/client/admin/admin.less b/client/admin/admin.less index c6c9b4662..1955f8a93 100644 --- a/client/admin/admin.less +++ b/client/admin/admin.less @@ -22,7 +22,7 @@ body { } :where(.admin) { - + padding-bottom : 50px; header { padding : 20px 0px; margin-bottom : 30px; @@ -30,6 +30,7 @@ body { color : white; background-color : @red; i { margin-right : 30px; } + a { float : right; } } hr { margin : 30px 0px; } @@ -48,21 +49,23 @@ body { } dl { - @maxItemWidth : 132px; + display : grid; + grid-template-columns : 120px 1fr; + row-gap : 10px; + align-items : center; + justify-items : start; + padding-top : 0.5em; dt { - float : left; - width : @maxItemWidth; - clear : left; - text-align : right; + float : left; + clear : left; + height : fit-content; + font-weight : 900; + text-align : right; &::after { content : ' : '; } } - dd { - height : 1em; - padding : 0 0 0.5em 0; - margin-left : @maxItemWidth + 6px; - } + dd { height : fit-content; } } - + .tabs button { margin-right : 3px; margin-left : 3px; @@ -90,11 +93,45 @@ body { } } + table { + padding : 10px; + + tr { + border-bottom : 1px solid; + &:last-of-type { border : none; } + &:nth-child(even) { background : #DDDDDD; } + } + + thead { + background : rgb(193,236,230); + border-bottom : 2px solid; + } + + th, td { + padding : 5px 10px; + vertical-align : middle; + text-align : center; + border-right : 1px solid; + + &:last-child { border-right : none; } + } + + th { font-weight : 900; } + + td { + &:first-child { + font-weight : 900; + text-align : left; + } + } + } + .error { - background: rgb(178, 54, 54); - color:white; - font-weight: 900; - margin-block:10px; - padding:10px; + float : right; + padding : 10px; + margin-block : 10px; + font-weight : 900; + color : white; + background : rgb(178, 54, 54); } } diff --git a/client/admin/authorUtils/authorLookup/authorLookup.jsx b/client/admin/authorUtils/authorLookup/authorLookup.jsx new file mode 100644 index 000000000..abdece6f7 --- /dev/null +++ b/client/admin/authorUtils/authorLookup/authorLookup.jsx @@ -0,0 +1,87 @@ +import './authorLookup.less'; + +import React from 'react'; +import request from 'superagent'; + +const authorLookup = ()=>{ + const [author, setAuthor] = React.useState(''); + const [searching, setSearching] = React.useState(false); + const [results, setResults] = React.useState([]); + + const lookup = async ()=>{ + if(!author) return; + + setSearching(true); + setResults([]); + + const brews = await request.get(`/admin/user/list/${author}`); + setResults(brews.body); + setSearching(false); + }; + + const renderResults = ()=>{ + if(results.length == 0) return <> +

Results

+

None found.

+ ; + + return <> +

{`Results - ${results.length} brews` }

+ + + + + + + + + + + + {results + .sort((a, b)=>{ // Sort brews from most recently updated + if(a.updatedAt > b.updatedAt) return -1; + return 1; + }) + .map((brew, idx)=>{ + return + + + + + + ; + })} + +
TitleShareEditLast UpdateStorage
{brew.title}{brew.shareId}{brew.editId}{brew.updatedAt}{brew.googleId ? 'Google' : 'Homebrewery'}
+ ; + }; + + const handleKeyPress = (evt)=>{ + if(evt.key === 'Enter') return lookup(); + }; + + const handleChange = (evt)=>{ + setAuthor(evt.target.value); + }; + + return ( +
+
+

Author Lookup

+ +
+
+ {renderResults()} +
+
+ ); +}; + +module.exports = authorLookup; diff --git a/client/admin/authorUtils/authorLookup/authorLookup.less b/client/admin/authorUtils/authorLookup/authorLookup.less new file mode 100644 index 000000000..8fdd56d04 --- /dev/null +++ b/client/admin/authorUtils/authorLookup/authorLookup.less @@ -0,0 +1,29 @@ +.authorLookup { + position : relative; + display : flex; + flex-direction : column; + + .field { + display : flex; + gap : 5px; + align-items : center; + justify-items : stretch; + width : 100%; + margin-bottom : 20px; + + + input { + height : 33px; + padding : 0px 10px; + margin-bottom : unset; + font-family : monospace; + } + + button { + width: 50px; + + i { margin-right : 10px; } + } + } + +} \ No newline at end of file diff --git a/client/admin/authorUtils/authorUtils.jsx b/client/admin/authorUtils/authorUtils.jsx new file mode 100644 index 000000000..a96eea528 --- /dev/null +++ b/client/admin/authorUtils/authorUtils.jsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import AuthorLookup from './authorLookup/authorLookup.jsx'; + +const authorUtils = ()=>{ + return ( +
+ +
+ ); +}; + +module.exports = authorUtils; \ No newline at end of file diff --git a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx index a166ae112..d4b17c570 100644 --- a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx +++ b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx @@ -1,10 +1,8 @@ -require('./brewCleanup.less'); const React = require('react'); const createClass = require('create-react-class'); const request = require('superagent'); - const BrewCleanup = createClass({ displayName : 'BrewCleanup', getDefaultProps(){ @@ -39,9 +37,9 @@ const BrewCleanup = createClass({ if(!this.state.primed) return; if(!this.state.count){ - return
No Matching Brews found.
; + return
No Matching Brews found.
; } - return
+ return
; }, render(){ - return
+ return

Brew Cleanup

Removes very short brews to tidy up the database

@@ -65,7 +63,7 @@ const BrewCleanup = createClass({ {this.renderPrimed()} {this.state.error - &&
{this.state.error.toString()}
+ &&
{this.state.error.toString()}
}
; } diff --git a/client/admin/brewUtils/brewCleanup/brewCleanup.less b/client/admin/brewUtils/brewCleanup/brewCleanup.less deleted file mode 100644 index 16fc98957..000000000 --- a/client/admin/brewUtils/brewCleanup/brewCleanup.less +++ /dev/null @@ -1,9 +0,0 @@ -.BrewCleanup { - .removeBox { - margin-top : 20px; - button { - margin-right : 10px; - background-color : @red; - } - } -} \ No newline at end of file diff --git a/client/admin/brewUtils/brewCompress/brewCompress.jsx b/client/admin/brewUtils/brewCompress/brewCompress.jsx index 2c8e5b023..ccb59e027 100644 --- a/client/admin/brewUtils/brewCompress/brewCompress.jsx +++ b/client/admin/brewUtils/brewCompress/brewCompress.jsx @@ -1,10 +1,7 @@ -require('./brewCompress.less'); const React = require('react'); const createClass = require('create-react-class'); - const request = require('superagent'); - const BrewCompress = createClass({ displayName : 'BrewCompress', getDefaultProps(){ @@ -53,9 +50,9 @@ const BrewCompress = createClass({ if(!this.state.primed) return; if(!this.state.count){ - return
No Matching Brews found.
; + return
No Matching Brews found.
; } - return
+ return
; }, render(){ - return
+ return

Brew Compression

Compresses the text in brews to binary

diff --git a/client/admin/brewUtils/brewCompress/brewCompress.less b/client/admin/brewUtils/brewCompress/brewCompress.less deleted file mode 100644 index 8668e9280..000000000 --- a/client/admin/brewUtils/brewCompress/brewCompress.less +++ /dev/null @@ -1,9 +0,0 @@ -.BrewCompress { - .removeBox { - margin-top : 20px; - button { - margin-right : 10px; - background-color : @red; - } - } -} \ No newline at end of file diff --git a/client/admin/brewUtils/brewLookup/brewLookup.jsx b/client/admin/brewUtils/brewLookup/brewLookup.jsx index e5b585ced..fb780f29e 100644 --- a/client/admin/brewUtils/brewLookup/brewLookup.jsx +++ b/client/admin/brewUtils/brewLookup/brewLookup.jsx @@ -1,5 +1,3 @@ -require('./brewLookup.less'); - const React = require('react'); const createClass = require('create-react-class'); const cx = require('classnames'); @@ -55,7 +53,7 @@ const BrewLookup = createClass({ renderFoundBrew(){ const brew = this.state.foundBrew; - return
+ return
Title
{brew.title}
@@ -90,7 +88,7 @@ const BrewLookup = createClass({ }, render(){ - return
+ return

Brew Lookup

; } diff --git a/client/admin/brewUtils/brewLookup/brewLookup.less b/client/admin/brewUtils/brewLookup/brewLookup.less deleted file mode 100644 index da15e3a64..000000000 --- a/client/admin/brewUtils/brewLookup/brewLookup.less +++ /dev/null @@ -1,6 +0,0 @@ -.brewLookup { - .cleanButton { - display : inline-block; - width : 100%; - } -} \ No newline at end of file diff --git a/client/admin/brewUtils/brewUtils.jsx b/client/admin/brewUtils/brewUtils.jsx index de8c29895..bab2cb82f 100644 --- a/client/admin/brewUtils/brewUtils.jsx +++ b/client/admin/brewUtils/brewUtils.jsx @@ -1,6 +1,6 @@ const React = require('react'); const createClass = require('create-react-class'); - +require('./brewUtils.less'); const BrewCleanup = require('./brewCleanup/brewCleanup.jsx'); const BrewLookup = require('./brewLookup/brewLookup.jsx'); diff --git a/client/admin/brewUtils/brewUtils.less b/client/admin/brewUtils/brewUtils.less new file mode 100644 index 000000000..5bbbc3f69 --- /dev/null +++ b/client/admin/brewUtils/brewUtils.less @@ -0,0 +1,29 @@ +.brewUtil { + .result { + margin-top : 20px; + button { + margin-right : 10px; + background-color : @red; + } + } + .cleanButton { + display : inline-block; + width : 100%; + } +} + +.stats { + position : relative; + + .pending { + position : absolute; + top : 0.5em; + left : 100px; + width : 100%; + height : 100%; + } + + &:has(.pending) { opacity : 0.5; } + + dl { grid-template-columns : 200px 250px; } +} \ No newline at end of file diff --git a/client/admin/brewUtils/stats/stats.jsx b/client/admin/brewUtils/stats/stats.jsx index 85ce10610..7f96618f9 100644 --- a/client/admin/brewUtils/stats/stats.jsx +++ b/client/admin/brewUtils/stats/stats.jsx @@ -1,11 +1,8 @@ -require('./stats.less'); const React = require('react'); const createClass = require('create-react-class'); -const cx = require('classnames'); const request = require('superagent'); - const Stats = createClass({ displayName : 'Stats', getDefaultProps(){ @@ -14,7 +11,8 @@ const Stats = createClass({ getInitialState(){ return { stats : { - totalBrews : 0 + totalBrews : 0, + totalPublishedBrews : 0 }, fetching : false }; @@ -29,11 +27,13 @@ const Stats = createClass({ .finally(()=>this.setState({ fetching: false })); }, render(){ - return
+ return

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 ; -} -); +}); 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
@@ -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)} - /> + />
@@ -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..dc2520373 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -102,6 +102,14 @@ const EditPage = createClass({ window.onbeforeunload = function(){}; document.removeEventListener('keydown', this.handleControlKeys); }, + componentDidUpdate : function(){ + const hasChange = this.hasChanges(); + if(this.state.isPending != hasChange){ + this.setState({ + isPending : hasChange + }); + } + }, handleControlKeys : function(e){ if(!(e.ctrlKey || e.metaKey)) return; @@ -138,15 +146,13 @@ const EditPage = createClass({ this.setState((prevState)=>({ brew : { ...prevState.brew, text: text }, - isPending : true, htmlErrors : htmlErrors, }), ()=>{if(this.state.autoSave) this.trySave();}); }, handleStyleChange : function(style){ this.setState((prevState)=>({ - brew : { ...prevState.brew, style: style }, - isPending : true + brew : { ...prevState.brew, style: style } }), ()=>{if(this.state.autoSave) this.trySave();}); }, @@ -158,8 +164,7 @@ const EditPage = createClass({ brew : { ...prevState.brew, ...metadata - }, - isPending : true, + } }), ()=>{if(this.state.autoSave) this.trySave();}); }, @@ -247,16 +252,17 @@ const EditPage = createClass({ }); if(!res) return; - this.savedBrew = res.body; + this.savedBrew = { + ...this.state.brew, + googleId : res.body.googleId ? res.body.googleId : null, + editId : res.body.editId, + shareId : res.body.shareId, + version : res.body.version + }; history.replaceState(null, null, `/edit/${this.savedBrew.editId}`); - this.setState((prevState)=>({ - brew : { ...prevState.brew, - googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null, - editId : this.savedBrew.editId, - shareId : this.savedBrew.shareId, - version : this.savedBrew.version - }, + this.setState(()=>({ + brew : this.savedBrew, isPending : false, isSaving : false, unsavedTime : new Date() @@ -311,7 +317,14 @@ const EditPage = createClass({ }, renderSaveButton : function(){ - if(this.state.autoSaveWarning && this.hasChanges()){ + + // #1 - Currently saving, show SAVING + if(this.state.isSaving){ + return saving...; + } + + // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING + if(this.state.isPending && this.state.autoSaveWarning){ this.setAutosaveWarning(); const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60); const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; @@ -324,18 +337,17 @@ const EditPage = createClass({ ; } - if(this.state.isSaving){ - return saving...; + // #3 - Unsaved changes exist, click to save, show SAVE NOW + // Use trySave(true) instead of save() to use debounced save function + if(this.state.isPending){ + return this.trySave(true)} color='blue' icon='fas fa-save'>Save Now; } - if(this.state.isPending && this.hasChanges()){ - return Save Now; - } - if(!this.state.isPending && !this.state.isSaving && this.state.autoSave){ + // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED + if(this.state.autoSave){ return auto-saved.; } - if(!this.state.isPending && !this.state.isSaving){ - return saved.; - } + // DEFAULT - No unsaved changes, show SAVED + return saved.; }, handleAutoSave : function(){ @@ -379,7 +391,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 +422,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 +455,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 a57084dc6..a3f028500 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,16 +10,16 @@ "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", + "@googleapis/drive": "^8.16.0", "body-parser": "^1.20.2", "classnames": "^2.5.1", "codemirror": "^5.65.6", "cookie-parser": "^1.4.7", - "core-js": "^3.40.0", + "core-js": "^3.41.0", "cors": "^2.8.5", "create-react-class": "^15.7.0", "dedent-tabs": "^0.10.3", @@ -35,20 +35,21 @@ "less": "^3.13.1", "lodash": "^4.17.21", "marked": "13.0.3", - "marked-emoji": "^1.4.3", - "marked-extended-tables": "^1.1.0", + "marked-alignment-paragraphs": "^1.0.0", + "marked-emoji": "^2.0.0", + "marked-extended-tables": "^2.0.0", "marked-gfm-heading-id": "^4.0.1", - "marked-justified-paragraphs": "^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.12.1", + "nanoid": "5.1.2", "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" @@ -56,15 +57,15 @@ "devDependencies": { "@stylistic/stylelint-plugin": "^3.1.2", "babel-plugin-transform-import-meta": "^2.3.2", - "eslint": "^9.20.0", + "eslint": "^9.21.0", "eslint-plugin-jest": "^28.11.0", "eslint-plugin-react": "^7.37.4", - "globals": "^15.14.0", + "globals": "^16.0.0", "jest": "^29.7.0", "jest-expect-message": "^1.1.3", "jsdom-global": "^3.0.2", "postcss-less": "^6.0.0", - "stylelint": "^16.14.1", + "stylelint": "^16.15.0", "stylelint-config-recess-order": "^6.0.0", "stylelint-config-recommended": "^15.0.0", "supertest": "^7.0.0" @@ -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", @@ -1863,12 +1864,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", - "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.4", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -1877,10 +1879,11 @@ } }, "node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -1889,10 +1892,11 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -1916,6 +1920,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -1924,9 +1929,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", - "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", + "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", "dev": true, "license": "MIT", "engines": { @@ -1934,21 +1939,23 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.10.0", + "@eslint/core": "^0.12.0", "levn": "^0.4.1" }, "engines": { @@ -1956,9 +1963,10 @@ } }, "node_modules/@googleapis/drive": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-8.14.0.tgz", - "integrity": "sha512-AOokfpP6pCdcJXWA8khaCEgbGpWYavWTdAAhL4idbbf2VCQcJ2f7vPalAYNu6a4Sfj0Ly4Ehnd1xw9J9TixB1A==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-8.16.0.tgz", + "integrity": "sha512-Xi2mMrUTQ+gsfyouRGd0pfnL+jjg4n4sjKsJruM1y4DknuRfdSBTk5E//WrL0YJ/CqpcBgyd7L8DvaPRtxZD3Q==", + "license": "Apache-2.0", "dependencies": { "googleapis-common": "^7.0.0" }, @@ -2016,10 +2024,11 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -2686,10 +2695,11 @@ } }, "node_modules/@keyv/serialize": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.2.tgz", - "integrity": "sha512-+E/LyaAeuABniD/RvUezWVXKpeuvwLEA9//nE9952zBaOdBd2mQ3pPoM8cUe2X6IcMByfuSLzmYqnYshG60+HQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.3.tgz", + "integrity": "sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==", "dev": true, + "license": "MIT", "dependencies": { "buffer": "^6.0.3" } @@ -2713,6 +2723,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -2872,12 +2883,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", @@ -2919,7 +2924,8 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { "version": "22.1.0", @@ -3131,10 +3137,11 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -3147,6 +3154,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -3200,6 +3208,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4172,9 +4181,9 @@ } }, "node_modules/bson": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.2.tgz", - "integrity": "sha512-5afhLTjqDSA3akH56E+/2J6kTDuSIlBxyXPdQslj9hcIgOUE378xdOfZvC/9q3LifJNI6KR/juZ+d0NRNYBwXg==", + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", + "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==", "license": "Apache-2.0", "engines": { "node": ">=16.20.1" @@ -4244,22 +4253,24 @@ } }, "node_modules/cacheable": { - "version": "1.8.7", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.8.7.tgz", - "integrity": "sha512-AbfG7dAuYNjYxFUtL1lAqmlWdxczCJ47w7cFjhGcnGnUdwSo6VgmSojfoW3tUI12HUkgTJ5kqj78yyq6TsFtlg==", + "version": "1.8.9", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.8.9.tgz", + "integrity": "sha512-FicwAUyWnrtnd4QqYAoRlNs44/a1jTL7XDKqm5gJ90wz1DQPlC7U2Rd1Tydpv+E7WAr4sQHuw8Q8M3nZMAyecQ==", "dev": true, + "license": "MIT", "dependencies": { - "hookified": "^1.6.0", - "keyv": "^5.2.3" + "hookified": "^1.7.1", + "keyv": "^5.3.1" } }, "node_modules/cacheable/node_modules/keyv": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.2.3.tgz", - "integrity": "sha512-AGKecUfzrowabUv0bH1RIR5Vf7w+l4S3xtQAypKaUpTdIR1EbrAcTxHCrpo9Q+IWeUlFE2palRtgIQcgm+PQJw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.1.tgz", + "integrity": "sha512-13hQT2q2VIwOoaJdJa7nY3J8UVbYtMTJFHnwm9LI+SaQRfUiM6Em9KZeOVTCKbMnGcRIL3NSUFpAdjZCq24nLQ==", "dev": true, + "license": "MIT", "dependencies": { - "@keyv/serialize": "^1.0.2" + "@keyv/serialize": "^1.0.3" } }, "node_modules/cached-path-relative": { @@ -4688,10 +4699,11 @@ } }, "node_modules/core-js": { - "version": "3.40.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz", - "integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==", + "version": "3.41.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.41.0.tgz", + "integrity": "sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==", "hasInstallScript": true, + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -5666,22 +5678,22 @@ "license": "MIT" }, "node_modules/eslint": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.0.tgz", - "integrity": "sha512-aL4F8167Hg4IvsW89ejnpTwx+B/UQRzJPGgbIOl+4XqffWsahVVsLEWoZvnrVuwpWmnRd7XeXmQI1zlKcFDteA==", + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz", + "integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.11.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.20.0", - "@eslint/plugin-kit": "^0.2.5", + "@eslint/config-array": "^0.19.2", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.0", + "@eslint/js": "9.21.0", + "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -5842,19 +5854,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/core": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", - "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5925,6 +5924,7 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", @@ -5942,6 +5942,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -6449,10 +6450,11 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", - "dev": true + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" }, "node_modules/for-each": { "version": "0.3.3", @@ -6814,10 +6816,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": "16.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz", + "integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -7136,10 +7139,11 @@ } }, "node_modules/hookified": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.6.0.tgz", - "integrity": "sha512-se7cpwTA+iA/eY548Bu03JJqBiEZAqU2jnyKdj5B5qurtBg64CZGHTgqCv4Yh7NWu6FGI09W61MCq+NoPj9GXA==", - "dev": true + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.7.1.tgz", + "integrity": "sha512-OXcdHsXeOiD7OJ5zvWj8Oy/6RCdLwntAX+wUrfemNcMGn6sux4xbEHi2QXwqePYhjQ/yvxxq2MvCRirdlHscBw==", + "dev": true, + "license": "MIT" }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", @@ -9426,7 +9430,8 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify": { "version": "0.0.1", @@ -9847,18 +9852,29 @@ "node": ">= 18" } }, + "node_modules/marked-alignment-paragraphs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/marked-alignment-paragraphs/-/marked-alignment-paragraphs-1.0.0.tgz", + "integrity": "sha512-e+X+vUt9oteG4I0+29ZvTjrNEvB2/uo/Dhczu+Nq0shITA3oqYmTercT25mq+2mfJQQn7xiJGXPmDmBSFgotIA==", + "license": "MIT", + "peerDependencies": { + "marked": ">=3 <16" + } + }, "node_modules/marked-emoji": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/marked-emoji/-/marked-emoji-1.4.3.tgz", - "integrity": "sha512-HDZx1VOmzu7XT2QNKWfrHGbNRMTWKj9XD78yrcH1madD30HpGLMODPOmKr/e7CA7NKKXkpXXNdndQn++ysXmHg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/marked-emoji/-/marked-emoji-2.0.0.tgz", + "integrity": "sha512-oTZ8fqbdVDHFQnqCE1tg4ND7zEd7cUVNHliR9Ldu4eys0J86uz/5Uksjd2mt5xcX16OOScDEr3MmPjajI/ZDHA==", + "license": "MIT", "peerDependencies": { "marked": ">=4 <16" } }, "node_modules/marked-extended-tables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/marked-extended-tables/-/marked-extended-tables-1.1.0.tgz", - "integrity": "sha512-xPQlnmr/mpIJEjwg9/guSKzxoKu7hyCD/sM59mcZc+nMIh2JuVM2se+kCa1Jo1UH+BEHcNlZ221ziJ/cOxAgCA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/marked-extended-tables/-/marked-extended-tables-2.0.0.tgz", + "integrity": "sha512-MWmxvFLkJYQ5K46MFieOP1uueMgfIpDPMkYLLgIyTl20HyvLIW4J37BAtIGfR8fDp2uE4Kyyev4s3dhoT2FQOA==", + "license": "MIT", "peerDependencies": { "marked": ">=3 <16" } @@ -9875,15 +9891,6 @@ "marked": ">=13 <16" } }, - "node_modules/marked-justified-paragraphs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/marked-justified-paragraphs/-/marked-justified-paragraphs-1.0.0.tgz", - "integrity": "sha512-TgTKij4HbYy85zWAZ0Va7JCOU/yh8d12Jq2J/jaBHNMa6gJDAsbLT42MFLU9gwLYxsg8hCJHJ0n0zYY6zo8jiA==", - "license": "MIT", - "peerDependencies": { - "marked": ">=3 <16" - } - }, "node_modules/marked-smartypants-lite": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/marked-smartypants-lite/-/marked-smartypants-lite-1.0.3.tgz", @@ -9893,6 +9900,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,14 +10264,14 @@ } }, "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.12.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.12.1.tgz", + "integrity": "sha512-UW22y8QFVYmrb36hm8cGncfn4ARc/XsYWQwRTaj0gxtQk1rDuhzDO1eBantS+hTTatfAIS96LlRCJrcNHvW5+Q==", "license": "MIT", "dependencies": { - "bson": "^6.10.1", + "bson": "^6.10.3", "kareem": "2.6.3", - "mongodb": "~6.13.0", + "mongodb": "~6.14.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -10331,13 +10347,13 @@ } }, "node_modules/mongoose/node_modules/mongodb": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.13.0.tgz", - "integrity": "sha512-KeESYR5TEaFxOuwRqkOm3XOsMqCSkdeDMjaW5u2nuKfX7rqaofp7JQGoi7sVqQcNJTKuveNbzZtWMstb8ABP6Q==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.14.2.tgz", + "integrity": "sha512-kMEHNo0F3P6QKDq17zcDuPeaywK/YaJVCEQRzPF3TOM/Bl9MFg64YE5Tu7ifj37qZJMhwU1tl2Ioivws5gRG5Q==", "license": "Apache-2.0", "dependencies": { "@mongodb-js/saslprep": "^1.1.9", - "bson": "^6.10.1", + "bson": "^6.10.3", "mongodb-connection-string-url": "^3.0.0" }, "engines": { @@ -10403,9 +10419,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.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.2.tgz", + "integrity": "sha512-b+CiXQCNMUGe0Ri64S9SXFcP9hogjAJ2Rd6GdVxhPLRm7mhGaM7VgOvCAJ1ZshfHbqVDI3uqTI5C8/GaKuLI7g==", "funding": [ { "type": "github", @@ -11268,9 +11284,9 @@ } }, "node_modules/postcss": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", - "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "dev": true, "funding": [ { @@ -11676,9 +11692,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", @@ -13062,9 +13078,9 @@ "license": "ISC" }, "node_modules/stylelint": { - "version": "16.14.1", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.14.1.tgz", - "integrity": "sha512-oqCL7AC3786oTax35T/nuLL8p2C3k/8rHKAooezrPGRvUX0wX+qqs5kMWh5YYT4PHQgVDobHT4tw55WgpYG6Sw==", + "version": "16.15.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.15.0.tgz", + "integrity": "sha512-OK6Rs7EPdcdmjqiDycadZY4fw3f5/TC1X6/tGjnF3OosbwCeNs7nG+79MCAtjEg7ckwqTJTsku08e0Rmaz5nUw==", "dev": true, "funding": [ { @@ -13091,7 +13107,7 @@ "debug": "^4.3.7", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", - "file-entry-cache": "^10.0.5", + "file-entry-cache": "^10.0.6", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", @@ -13105,14 +13121,14 @@ "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", - "postcss": "^8.5.1", + "postcss": "^8.5.3", "postcss-resolve-nested-selector": "^0.1.6", "postcss-safe-parser": "^7.0.1", - "postcss-selector-parser": "^7.0.0", + "postcss-selector-parser": "^7.1.0", "postcss-value-parser": "^4.2.0", "resolve-from": "^5.0.0", "string-width": "^4.2.3", - "supports-hyperlinks": "^3.1.0", + "supports-hyperlinks": "^3.2.0", "svg-tags": "^1.0.0", "table": "^6.9.0", "write-file-atomic": "^5.0.1" @@ -13227,23 +13243,25 @@ "license": "MIT" }, "node_modules/stylelint/node_modules/file-entry-cache": { - "version": "10.0.5", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.0.5.tgz", - "integrity": "sha512-umpQsJrBNsdMDgreSryMEXvJh66XeLtZUwA8Gj7rHGearGufUFv6rB/bcXRFsiGWw/VeSUgUofF4Rf2UKEOrTA==", + "version": "10.0.7", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.0.7.tgz", + "integrity": "sha512-txsf5fu3anp2ff3+gOJJzRImtrtm/oa9tYLN0iTuINZ++EyVR/nRrg2fKYwvG/pXDofcrvvb0scEbX3NyW/COw==", "dev": true, + "license": "MIT", "dependencies": { - "flat-cache": "^6.1.5" + "flat-cache": "^6.1.7" } }, "node_modules/stylelint/node_modules/flat-cache": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.5.tgz", - "integrity": "sha512-QR+2kN38f8nMfiIQ1LHYjuDEmZNZVjxuxY+HufbS3BW0EX01Q5OnH7iduOYRutmgiXb797HAKcXUeXrvRjjgSQ==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.7.tgz", + "integrity": "sha512-qwZ4xf1v1m7Rc9XiORly31YaChvKt6oNVHuqqZcoED/7O+ToyNVGobKsIAopY9ODcWpEDKEBAbrSOCBHtNQvew==", "dev": true, + "license": "MIT", "dependencies": { - "cacheable": "^1.8.7", - "flatted": "^3.3.2", - "hookified": "^1.6.0" + "cacheable": "^1.8.9", + "flatted": "^3.3.3", + "hookified": "^1.7.1" } }, "node_modules/stylelint/node_modules/ignore": { @@ -13256,9 +13274,9 @@ } }, "node_modules/stylelint/node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", "dependencies": { @@ -13405,10 +13423,11 @@ } }, "node_modules/supports-hyperlinks": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.1.0.tgz", - "integrity": "sha512-2rn0BZ+/f7puLOHZm1HOJfwBggfaHXUpPUSSG/SWM4TWp5KCfmNYwnC3hruy2rZlMnmWZ+QAGpZfchu3f3695A==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" @@ -13417,7 +13436,7 @@ "node": ">=14.18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" } }, "node_modules/supports-hyperlinks/node_modules/has-flag": { @@ -13425,6 +13444,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -13434,6 +13454,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -14145,6 +14166,7 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } diff --git a/package.json b/package.json index b93fe21f2..d9fb20334 100644 --- a/package.json +++ b/package.json @@ -84,16 +84,16 @@ ] }, "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", + "@googleapis/drive": "^8.16.0", "body-parser": "^1.20.2", "classnames": "^2.5.1", "codemirror": "^5.65.6", "cookie-parser": "^1.4.7", - "core-js": "^3.40.0", + "core-js": "^3.41.0", "cors": "^2.8.5", "create-react-class": "^15.7.0", "dedent-tabs": "^0.10.3", @@ -109,20 +109,21 @@ "less": "^3.13.1", "lodash": "^4.17.21", "marked": "13.0.3", - "marked-emoji": "^1.4.3", - "marked-extended-tables": "^1.1.0", + "marked-emoji": "^2.0.0", + "marked-extended-tables": "^2.0.0", "marked-gfm-heading-id": "^4.0.1", - "marked-justified-paragraphs": "^1.0.0", + "marked-alignment-paragraphs": "^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.12.1", + "nanoid": "5.1.2", "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" @@ -130,15 +131,15 @@ "devDependencies": { "@stylistic/stylelint-plugin": "^3.1.2", "babel-plugin-transform-import-meta": "^2.3.2", - "eslint": "^9.20.0", + "eslint": "^9.21.0", "eslint-plugin-jest": "^28.11.0", "eslint-plugin-react": "^7.37.4", - "globals": "^15.14.0", + "globals": "^16.0.0", "jest": "^29.7.0", "jest-expect-message": "^1.1.3", "jsdom-global": "^3.0.2", "postcss-less": "^6.0.0", - "stylelint": "^16.14.1", + "stylelint": "^16.15.0", "stylelint-config-recess-order": "^6.0.0", "stylelint-config-recommended": "^15.0.0", "supertest": "^7.0.0" diff --git a/server/admin.api.js b/server/admin.api.js index 1a39f020b..e70268afa 100644 --- a/server/admin.api.js +++ b/server/admin.api.js @@ -93,7 +93,7 @@ router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{ /* Cleans `` from the "text" field of a brew */ router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res)=>{ - console.log(`[ADMIN] Cleaning script tags from ShareID ${req.params.id}`); + console.log(`[ADMIN: ${req.account?.username || 'Not Logged In'}] Cleaning script tags from ShareID ${req.params.id}`); function cleanText(text){return text.replaceAll(/(<\/?s)cript/gi, '');}; @@ -114,6 +114,18 @@ router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin', return await HomebrewAPI.updateBrew(req, res); }); +/* Get list of a user's documents */ +router.get('/admin/user/list/:user', mw.adminOnly, async (req, res)=>{ + const username = req.params.user; + const fields = { _id: 0, text: 0, textBin: 0 }; // Remove unnecessary fields from document lists + + console.log(`[ADMIN: ${req.account?.username || 'Not Logged In'}] Get brew list for ${username}`); + + const brews = await HomebrewModel.getByUser(username, true, fields); + + return res.json(brews); +}); + /* Compresses the "text" field of a brew to binary */ router.put('/admin/compress/:id', (req, res)=>{ HomebrewModel.findOne({ _id: req.params.id }) @@ -135,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 e9ea49d46..e038fdc63 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -7,7 +7,8 @@ import MarkedExtendedTables from 'marked-extended-tables'; import { markedSmartypantsLite as MarkedSmartypantsLite } from 'marked-smartypants-lite'; import { gfmHeadingId as MarkedGFMHeadingId, resetHeadings as MarkedGFMResetHeadingIDs } from 'marked-gfm-heading-id'; import { markedEmoji as MarkedEmojis } from 'marked-emoji'; -import MarkedJustifiedParagraphs from 'marked-justified-paragraphs'; +import MarkedAlignedParagraphs from 'marked-alignment-paragraphs'; +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)}`; - } -}; - - const forcedParagraphBreaks = { name : 'hardBreaks', level : 'block', @@ -380,7 +352,7 @@ const forcedParagraphBreaks = { } }, renderer(token) { - return `
`.repeat(token.length).concat('\n'); + return `
\n`.repeat(token.length); } }; @@ -761,11 +733,12 @@ const tableTerminators = [ Marked.use(MarkedVariables()); Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, - nonbreakingSpaces, superSubScripts, mustacheSpans, mustacheDivs, mustacheInjectInline] }); -Marked.use(MarkedJustifiedParagraphs()); + nonbreakingSpaces, mustacheSpans, mustacheDivs, mustacheInjectInline] }); Marked.use(mustacheInjectBlock); +Marked.use(MarkedAlignedParagraphs()); +Marked.use(MarkedSubSuperText()); Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false }); -Marked.use(MarkedExtendedTables(tableTerminators), MarkedGFMHeadingId({ globalSlugs: true }), +Marked.use(MarkedExtendedTables({interruptPatterns : tableTerminators}), MarkedGFMHeadingId({ globalSlugs: true }), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions)); function cleanUrl(href) { 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
\n

Definition 1

`); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

Term 1

\n
\n
\n

Definition 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
\n

Line 2

`); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

Line 1

\n
\n
\n

Line 2

`); }); test('Ignored inside a code block', function() { diff --git a/themes/V3/5ePHB/snippets/monsterblock.gen.js b/themes/V3/5ePHB/snippets/monsterblock.gen.js index e61c206f0..dffb59d04 100644 --- a/themes/V3/5ePHB/snippets/monsterblock.gen.js +++ b/themes/V3/5ePHB/snippets/monsterblock.gen.js @@ -148,7 +148,7 @@ const genAction = function(){ 'Turnbuckle Roll' ]); - return `***${name}.*** *Melee Weapon Attack:* +4 to hit, reach 5ft., one target. *Hit* 5 (1d6 + 2) `; + return `***${name}.*** *Melee Weapon Attack:* +4 to hit, reach 5 ft., one target. *Hit:* 5 (1d6 + 2) `; }; @@ -161,8 +161,8 @@ module.exports = { *${getType()}, ${getAlignment()}* ___ **Armor Class** :: ${_.random(10, 20)} (chain mail, shield) - **Hit Points** :: ${_.random(1, 150)}(1d4 + 5) - **Speed** :: ${_.random(0, 50)}ft. + **Hit Points** :: ${_.random(1, 150)} (1d4 + 5) + **Speed** :: ${_.random(0, 50)} ft. ___ | STR | DEX | CON | INT | WIS | CHA | |:-----:|:-----:|:-----:|:-----:|:-----:|:-----:| 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 // *****************************/