Merge branch 'master' into fix-brew-card-padding-5134

This commit is contained in:
Trevor Buckner
2026-10-29 15:07:36 -04:00
committed by GitHub
13 changed files with 475 additions and 492 deletions
+22 -1
View File
@@ -90,9 +90,30 @@ pre {
}
```
## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
### Tuesday 10/20/2026 - v3.24.2
{{taskList
##### G-Ambatte
* [x] Hotfix for memory leak on server
Fixes issue [#5090](https://github.com/naturalcrit/homebrewery/issues/5090)
##### Calculuschild
* [x] Fix brew variables not working across pages
* [x] Allow brew variable definitions to be on adjacent lines (no blank line required anymore)
##### 5e-Cleric
* [x] Partial rollout of some site UI changes (includes alternate views and darkmode in {{openSans **BREWS** :fas_beer_mug_empty:}} page, and changes to {{openSans **VAULT** :fas_dungeon:}})
##### Abquintic, Calculuschild, 5e-Cleric
* [x] Multiple background fixes and code cleanups
}}
### Tuesday 9/29/2026 - v3.24.1
{{taskList
##### calculuschild
@@ -169,7 +190,7 @@ Fixes issue [#3840](https://github.com/naturalcrit/homebrewery/issues/3840)
Fixes issue [#2037](https://github.com/naturalcrit/homebrewery/issues/2037)
* [x] Remember editor theme across tabs
* [x] Remember editor theme across tabs, and new {{openSans **SETTINGS :fas_gear:**}} tab with editor settings
Fixes issue [#4858](https://github.com/naturalcrit/homebrewery/issues/4858)
+28 -24
View File
@@ -156,6 +156,31 @@ const CodeEditor = forwardRef(
return folds;
};
const foldAll = (view = viewRef.current)=>{
if(!view) return false;
const doc = view.state.doc;
const pages = pageMap.current;
const effects = pages.map((start, i)=>{
const next = pages[i + 1] || doc.length;
const from = i ? doc.line(doc.lineAt(start).number + 1).from : 0;
const to = doc.line(doc.lineAt(next).number).from - 1;
return to > from ? foldEffect.of({ from, to }) : null;
}).filter(Boolean);
view.dispatch({ effects });
return true;
};
const unfoldAll = (view = viewRef.current)=>{
if(!view) return false;
view.dispatch(unfoldAllCmd(view));
return true;
};
const createExtensions = ({ onChange, language, editorTheme })=>{
const setEventListeners = EditorView.updateListener.of((update)=>{
if(update.docChanged) {
@@ -199,7 +224,7 @@ const CodeEditor = forwardRef(
//keyboard shortcut
keymap.of([...defaultKeymap, foldKeymap, ...searchKeymap]),
generalKeymap,
generalKeymap(foldAll, unfoldAll),
...(tab === 'brewStyles' ? [cssKeymap] : [markdownKeymap]),
//multiple cursors and selections
@@ -404,29 +429,8 @@ const CodeEditor = forwardRef(
undone : redoDepth(view.state),
};
},
foldAll : ()=>{
const view = viewRef.current;
if(!view) return;
const doc = view.state.doc;
const pages = pageMap.current;
const effects = pages.map((start, i)=>{
const next = pages[i + 1] || doc.length;
const from = i ? doc.line(doc.lineAt(start).number + 1).from : 0;
const to = doc.line(doc.lineAt(next).number).from - 1;
return to > from ? foldEffect.of({ from, to }) : null;
}).filter(Boolean);
view.dispatch({ effects });
},
unfoldAll : ()=>{
const view = viewRef.current;
if(!view) return;
view.dispatch(unfoldAllCmd(view));
},
foldAll : ()=>foldAll(),
unfoldAll : ()=>unfoldAll(),
focus : ()=>viewRef.current.focus(),
}));
@@ -215,13 +215,15 @@ const newBreak = (type)=>(view)=>forEachSelection(view, (state, from, to, select
};
});
export const generalKeymap = Prec.high(keymap.of([
export const generalKeymap = (foldAll, unfoldAll) => Prec.high(keymap.of([
{ key: 'Tab', run: insertTab },
{ key: 'Shift-Tab', run: indentLess },
{ key: 'Mod-z', run: undo }, //it may be unnecessary
{ key: 'Mod-Shift-z', run: redo },
{ key: 'Mod-y', run: redo }, //user asked, so double keybind
{ key: 'Mod-d', run: deleteLine }, //annoyingly overrides "selectNextOccurrence" because users asked
{ key: 'Mod-Shift-[', run: foldAll },
{ key: 'Mod-Shift-]', run: unfoldAll },
]));
export const cssKeymap = Prec.highest(keymap.of([
@@ -2,13 +2,14 @@
import '../uiEditor.less';
import React, { useState } from 'react';
import _ from 'lodash';
import request from '../../utils/request-middleware.js';
import Combobox from '@components/combobox.jsx';
import TagInput from '../tagInput/tagInput.jsx';
import request from '../../utils/request-middleware.js';
import Themes from '@themes/themes.json';
import validations from './validations.js';
import Combobox from '@components/combobox.jsx';
import TagInput from '../tagInput/tagInput.jsx';
import homebreweryThumbnail from '../../thumbnail.png';
const callIfExists = (val, fn, ...args)=>{
@@ -19,32 +20,25 @@ const callIfExists = (val, fn, ...args)=>{
const MetadataEditor = ({
metadata = {
editId : null,
shareId : null,
editId : null,
title : '',
description : '',
thumbnail : '',
tags : [],
published : false,
authors : [],
renderer : 'legacy',
theme : '5ePHB',
lang : 'en',
renderer : 'legacy',
authors : [],
published : false,
thumbnail : '',
theme : '5ePHB',
},
onChange = ()=>{},
reportError = ()=>{},
...props
userThemes = {},
themeBundle = {}
})=>{
const [showThumbnail, setShowThumbnail] = useState(true);
const toggleThumbnailDisplay = ()=>{
setShowThumbnail(!showThumbnail);
};
const renderThumbnail = ()=>{
if(!showThumbnail) return;
return <img className='thumbnail-preview' src={metadata.thumbnail || homebreweryThumbnail}></img>;
};
const handleFieldChange = (name, e)=>{
// load validation rules, and check input value against them
const inputRules = validations[name] ?? [];
@@ -90,13 +84,11 @@ const MetadataEditor = ({
const handleTheme = (theme)=>{
metadata.renderer = theme.renderer;
metadata.theme = theme.path;
onChange(metadata, 'theme');
};
const handleThemeWritein = (e)=>{
const shareId = e.target.value.split('/').pop(); //Extract just the ID if a URL was pasted in
metadata.theme = shareId;
onChange(metadata, 'theme');
};
const handleLanguage = (languageCode)=>{
@@ -141,18 +133,6 @@ const MetadataEditor = ({
</button>;
}
};
const renderDelete = ()=>{
if(!metadata.editId) return;
return <div className='field delete'>
<label htmlFor='delete-button'>delete</label>
<div className='value'>
<button id='delete-button' onClick={handleDelete}>
<i className='fas fa-trash-alt' /> delete brew
</button>
</div>
</div>;
};
const renderAuthors = ()=>{
const authors = metadata.authors;
const isOwner = global.account?.username && global.account?.username === authors[0];
@@ -204,7 +184,7 @@ const MetadataEditor = ({
);
};
const renderThemeDropdown = ()=>{
const mergedThemes = _.merge(Themes, props.userThemes);
const mergedThemes = _.merge(Themes, userThemes);
const listThemes = (renderer)=>{
return _.map(_.values(mergedThemes[renderer]), (theme)=>{
@@ -225,7 +205,7 @@ const MetadataEditor = ({
};
const currentRenderer = metadata.renderer;
const currentThemeDisplay = props.themeBundle?.name ? `${props.themeBundle.author ?? currentRenderer} : ${props.themeBundle.name}` : 'No Theme Selected';
const currentThemeDisplay = themeBundle?.name ? `${themeBundle.author ?? currentRenderer} : ${themeBundle.name}` : 'No Theme Selected';
let dropdown;
if(currentRenderer == 'legacy') {
@@ -296,7 +276,6 @@ const MetadataEditor = ({
}}
/>
</div>
</div>;
};
const renderRenderOptions = ()=>{
@@ -364,15 +343,14 @@ const MetadataEditor = ({
/>
<button
className='display'
onClick={toggleThumbnailDisplay}
onClick={()=>setShowThumbnail(!showThumbnail)}
aria-label={`${showThumbnail ? 'hide thumbnail' : 'show thumbnail'}`}>
<i className={`fas fa-caret-${showThumbnail ? 'right' : 'left'}`} />
</button>
</div>
</div>
{renderThumbnail()}
{showThumbnail && <img className='thumbnail-preview' src={metadata.thumbnail || homebreweryThumbnail}></img>}
</fieldset>
<div className='field tags'>
<label htmlFor='combobox-tags'>Tags</label>
<div className='value'>
@@ -388,17 +366,12 @@ const MetadataEditor = ({
/>
</div>
</div>
{renderLanguageDropdown()}
{renderThemeDropdown()}
{renderRenderOptions()}
<h2>Authors</h2>
{renderAuthors()}
<div className='field invitedAuthors'>
<label htmlFor='combobox-invited-authors'>Invited authors</label>
<div className='value'>
@@ -418,7 +391,6 @@ const MetadataEditor = ({
</div>
<h2>Privacy</h2>
<div className='field publish'>
<label htmlFor='publish-button'>publish</label>
<div className='value'>
@@ -430,8 +402,14 @@ const MetadataEditor = ({
</small>
</div>
</div>
{renderDelete()}
{metadata.editId && <div className='field delete'>
<label htmlFor='delete-button'>delete</label>
<div className='value'>
<button id='delete-button' onClick={handleDelete}>
<i className='fas fa-trash-alt' /> delete brew
</button>
</div>
</div>}
</div>
);
};
+18 -14
View File
@@ -69,25 +69,29 @@ const Homebrew = (props)=>{
);
}
return (
<Router>
<div className={`homebrew${(config?.deployment || config?.developmentStyle) ? ' deployment' : ''}`} style={backgroundObject()}>
<Routes>
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} />
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} />
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={brew} userThemes={userThemes}/>} />
<Route path='/new' element={<WithRoute el={NewPage} userThemes={userThemes}/> } />
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={brews} />} />
<Route path='/vault' element={<WithRoute el={VaultPage}/>}/>
<Route path='/changelog' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
<Route path='/faq' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
<Route path='/migrate' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
<Route path='/account' element={<WithRoute el={AccountPage} brew={brew} accountDetails={brew.accountDetails} />} />
<Route path='/' element={<WithRoute el={HomePage} brew={brew} />} />
<Route path='/*' element={<WithRoute el={HomePage} brew={brew} />} />
<Route path='/legacy' element={<WithRoute el={HomePage} brew={brew} />} />
<Route path='/error' element={<WithRoute el={ErrorPage} brew={brew} />} />
<Route path='/' element={<WithRoute el={HomePage} brew={brew} />} />
<Route path='/*' element={<WithRoute el={HomePage} brew={brew} />} />
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={brew} userThemes={userThemes}/>} />
<Route path='/new' element={<WithRoute el={NewPage} userThemes={userThemes}/> } />
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} />
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} />
<Route path='/changelog' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
<Route path='/faq' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
<Route path='/migrate' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
<Route path='/account' element={<WithRoute el={AccountPage} brew={brew} accountDetails={brew.accountDetails} />} />
<Route path='/error' element={<WithRoute el={ErrorPage} brew={brew} />} />
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={brews} />} />
<Route path='/vault' element={<WithRoute el={VaultPage}/>}/>
</Routes>
</div>
</Router>
@@ -112,12 +112,15 @@ const EditorPage = (props)=>{
const snipStorage = localStorage.getItem(SNIPKEY);
const metaStorage = JSON.parse(localStorage.getItem(METAKEY));
brew.text = brewStorage ?? brew.text;
brew.style = styleStorage ?? brew.style;
brew.snippets = snipStorage ?? brew.snippets;
brew.renderer = metaStorage?.renderer ?? brew.renderer;
brew.theme = metaStorage?.theme ?? brew.theme;
brew.lang = metaStorage?.lang ?? brew.lang;
brew.title = metaStorage?.title ?? brew.title;
brew.description = metaStorage?.description ?? brew.description;
brew.tags = metaStorage?.tags ?? brew.tags;
brew.text = brewStorage ?? brew.text;
brew.style = styleStorage ?? brew.style;
brew.snippets = snipStorage ?? brew.snippets;
brew.renderer = metaStorage?.renderer ?? brew.renderer;
brew.theme = metaStorage?.theme ?? brew.theme;
brew.lang = metaStorage?.lang ?? brew.lang;
}
const SAVEKEY = `${SAVEKEY_PREFIX}${global.account?.username}`;
@@ -130,7 +133,7 @@ const EditorPage = (props)=>{
localStorage.setItem(BREWKEY, brew.text);
if(brew.style) localStorage.setItem(STYLEKEY, brew.style);
if(brew.snippets) localStorage.setItem(SNIPKEY, brew.snippets);
localStorage.setItem(METAKEY, JSON.stringify({ renderer: brew.renderer, theme: brew.theme, lang: brew.lang }));
localStorage.setItem(METAKEY, JSON.stringify({ title: brew.title, description: brew.description, tags: brew.tags, renderer: brew.renderer, theme: brew.theme, lang: brew.lang }));
if(window.location.pathname !== '/new')
window.history.replaceState({}, window.location.title, '/new/');
};
@@ -177,9 +180,12 @@ const EditorPage = (props)=>{
if(field == 'style') localStorage.setItem(STYLEKEY, value);
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
renderer : value.renderer,
theme : value.theme,
lang : value.lang
title : value.title,
description : value.description,
tags : value.tags,
renderer : value.renderer,
theme : value.theme,
lang : value.lang
}));
}
};
-3
View File
@@ -6,14 +6,11 @@
name="viewport"
content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
<meta name="twitter:card" content="summary" />
<title>The Homebrewery - NaturalCrit</title>
</head>
<body>
<main id="reactRoot"></main>
<script type="module">
const props = window.__INITIAL_PROPS__ || {};
const url = props.config?.baseUrl;
+183 -175
View File
@@ -1,21 +1,21 @@
{
"name": "homebrewery",
"version": "3.24.1",
"version": "3.24.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "homebrewery",
"version": "3.24.1",
"version": "3.24.2",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^8.0.7",
"@babel/core": "^8.0.9",
"@babel/plugin-transform-runtime": "^8.0.6",
"@babel/preset-env": "^8.0.7",
"@babel/preset-react": "^8.0.1",
"@babel/runtime": "^8.0.7",
"@codemirror/autocomplete": "^6.20.3",
"@babel/runtime": "^8.0.8",
"@codemirror/autocomplete": "^6.20.4",
"@codemirror/commands": "^6.11.1",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.12",
@@ -24,17 +24,17 @@
"@codemirror/language-data": "^6.5.2",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.7.6",
"@codemirror/view": "^6.43.14",
"@googleapis/drive": "^26.0.2",
"@codemirror/view": "^6.43.15",
"@googleapis/drive": "^27.0.0",
"@lezer/highlight": "^1.2.4",
"@oddbird/css-anchor-positioning": "^0.10.2",
"@sanity/diff-match-patch": "^3.2.1",
"@vitejs/plugin-react": "^6.1.2",
"body-parser": "^2.3.0",
"body-parser": "^2.4.0",
"classnames": "^2.5.1",
"codemirror-5-themes": "^1.5.3",
"cookie-parser": "^1.4.7",
"core-js": "^3.50.0",
"core-js": "^3.51.0",
"cors": "^2.8.5",
"create-react-class": "^15.7.0",
"dedent": "^1.7.2",
@@ -49,18 +49,18 @@
"jwt-simple": "^0.5.6",
"less": "^4.9.1",
"lodash": "^4.18.1",
"marked-hbfm": "^1.0.1",
"marked-hbfm": "^1.0.3",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.31.0",
"mongoose": "^9.11.2",
"mongoose": "^9.11.4",
"nanoid": "6.0.2",
"nconf": "^0.13.0",
"node": "^26.12.0",
"node": "^26.12.1",
"prettier": "^3.9.10",
"react": "^19.3.1",
"react-dom": "^19.3.1",
"react-frame-component": "^5.3.2",
"react-router": "^8.4.0",
"react-router": "^8.5.0",
"sanitize-filename": "1.6.4",
"superagent": "^10.4.1"
},
@@ -78,10 +78,10 @@
"jsdom-global": "^3.0.2",
"postcss-less": "^6.0.0",
"stylelint": "^17.16.0",
"stylelint-config-recess-order": "^7.7.0",
"stylelint-config-recess-order": "^7.9.0",
"stylelint-config-recommended": "^18.0.0",
"supertest": "^7.3.1",
"vite": "^8.3.4"
"vite": "^8.3.5"
},
"engines": {
"node": ">=26.4.0",
@@ -144,19 +144,19 @@
}
},
"node_modules/@babel/core": {
"version": "8.0.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-8.0.7.tgz",
"integrity": "sha512-ZIsZWBG6Y1oa+8fkQVp0xeCdjCHBkXFCCIWrmg9SvejVvfcVQgKKRmGrBDWfM7rilhvnRoEuaqa5dkK7KB3pHA==",
"version": "8.0.9",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-8.0.9.tgz",
"integrity": "sha512-wXtyKlpR/pXf/u0vmib+OBakQaIzLglLcRoKpWxCHW/whqq7YfQYPD0IAAl9H/evi40MyLIjYYdvb6N+9DI2yA==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^8.0.6",
"@babel/generator": "^8.0.6",
"@babel/helper-compilation-targets": "^8.0.7",
"@babel/helpers": "^8.0.7",
"@babel/parser": "^8.0.7",
"@babel/helpers": "^8.0.8",
"@babel/parser": "^8.0.8",
"@babel/template": "^8.0.0",
"@babel/traverse": "^8.0.7",
"@babel/types": "^8.0.6",
"@babel/types": "^8.0.8",
"@types/gensync": "^1.0.5",
"convert-source-map": "^2.0.0",
"empathic": "^2.0.1",
@@ -422,9 +422,9 @@
}
},
"node_modules/@babel/helper-string-parser": {
"version": "8.0.6",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.6.tgz",
"integrity": "sha512-+6HYEBnsDRZHJtUTAEfMQd0zMDqdFHS7C+5vAsEZqBUr3vEMWOWlHJ0Jz1gS8jJIlCvLsuA6ivmeoqzH936YcA==",
"version": "8.0.8",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.8.tgz",
"integrity": "sha512-fVM+gSuKbdv9cpmyRNh5EvsDHUZCdxQH3FdnFKSkNYHpbsn95bbrxm+ES3MVeQjcqwi2O5ur/DjLfmTQaO60ng==",
"license": "MIT",
"engines": {
"node": "^22.18.0 || >=24.11.0"
@@ -463,25 +463,25 @@
}
},
"node_modules/@babel/helpers": {
"version": "8.0.7",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-8.0.7.tgz",
"integrity": "sha512-5dMj5kFwS2AY7Dt4PZp4px3c5ThMbbEa7tZfuHTncCGSFYq8uoA1ufDE1IS/lYpQedE+3+GQ3cPZUiplIu195w==",
"version": "8.0.8",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-8.0.8.tgz",
"integrity": "sha512-FBmhiDWs625wHg03OKiLQDeT6LOc2lWBbW97Yqs2iI1tZcucjpmYnO29NHsQ83XxKvredcvUymW/AAn2UtnakA==",
"license": "MIT",
"dependencies": {
"@babel/template": "^8.0.0",
"@babel/types": "^8.0.6"
"@babel/types": "^8.0.8"
},
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"node_modules/@babel/parser": {
"version": "8.0.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.7.tgz",
"integrity": "sha512-UkDwEbPO1px2JQSrdh4qIgaH1JgG7GO/pFeJkpxhDxIETkT/QvGpnigXIw75ViHYPtIgKITmVUJqLjiO+JuWjA==",
"version": "8.0.8",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.8.tgz",
"integrity": "sha512-Gq65JogAr0S2p0BzvfSJboyuiRnxDQApyOqHkKmkrvm+VlLK4gC3pKsiOKEdnhFBemrFbSnOu90mZZWDpiJnmQ==",
"license": "MIT",
"dependencies": {
"@babel/types": "^8.0.6"
"@babel/types": "^8.0.8"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -1607,9 +1607,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "8.0.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-8.0.7.tgz",
"integrity": "sha512-k0hagDDq/6w4v+0Sp4wFvIz/yswH5O7Va6aRTnD7N62Tnborp4X+YB+H3XUcWVSR4aE47WPEXiIIZchBs0GmYg==",
"version": "8.0.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-8.0.8.tgz",
"integrity": "sha512-dEu8nGlRqjsJsvviUUoSMfcRHpwomCiRlXsc2mPfJ79+ffTpDm+JtfUtTMzUrqCApHQnCsVPD3gT1ZT8RgBRCQ==",
"license": "MIT"
},
"node_modules/@babel/template": {
@@ -1645,12 +1645,12 @@
}
},
"node_modules/@babel/types": {
"version": "8.0.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.6.tgz",
"integrity": "sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==",
"version": "8.0.8",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.8.tgz",
"integrity": "sha512-iXSSZAKQWmaeAwGtFw10v9fg5hUiUs0L+w+Z2K11kym3pDNK+SF0VcAQPsEFvWcTSZ7LMgClMXDBvTbH0IlICg==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^8.0.6",
"@babel/helper-string-parser": "^8.0.8",
"@babel/helper-validator-identifier": "^8.0.6"
},
"engines": {
@@ -1739,9 +1739,9 @@
}
},
"node_modules/@codemirror/autocomplete": {
"version": "6.20.3",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz",
"integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==",
"version": "6.20.4",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.4.tgz",
"integrity": "sha512-yF8RhOf2SXlg00+qtupzLhrglcas61boqaONiNcbPIebHCpJocpqTBA6dyrtUrQO47hqyTdeob/3XnYilT7TaQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
@@ -2135,9 +2135,9 @@
"license": "MIT"
},
"node_modules/@codemirror/view": {
"version": "6.43.14",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.14.tgz",
"integrity": "sha512-hC45YnmJNR1rz42OF57BLoQNFMV12/13drnvibDsR2quNDmldcb4WvYVV81hFQg56i9lcqQMEDqilhX2QDcGQQ==",
"version": "6.43.15",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.15.tgz",
"integrity": "sha512-HHsfpqFeHyXLs983pSRe38Xajs0vFWZQ2BuX0m7Ki8w3jZ5iD8cuR3lBz2/5/X53Dhf32SBHKh6rtSTB6tKeGw==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.7.0",
@@ -2559,9 +2559,9 @@
"license": "MIT"
},
"node_modules/@googleapis/drive": {
"version": "26.0.2",
"resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-26.0.2.tgz",
"integrity": "sha512-VeKVZvzgH4ioczJZrZ5sy73RJbHrYLI1XZIZ3ZqGKQhC4U03XsOOOiBHKqvH4F9vbXrxR2LawmktoIJuTJhzYg==",
"version": "27.0.0",
"resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-27.0.0.tgz",
"integrity": "sha512-q+K1+T4Kbz3VUOww15lhV+jq+/IzuxzkfiASIXzxVG6X894QUPdhBgU9hKFNSKh+e58o0g6mX/06XoLsS1JE6Q==",
"license": "Apache-2.0",
"dependencies": {
"googleapis-common": "^9.0.0"
@@ -3723,9 +3723,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.153.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.153.0.tgz",
"integrity": "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==",
"version": "0.155.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz",
"integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/oxc-project"
@@ -4063,9 +4063,9 @@
"license": "MIT"
},
"node_modules/@rolldown/binding-android-arm-eabi": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.13.tgz",
"integrity": "sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==",
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz",
"integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==",
"cpu": [
"arm"
],
@@ -4079,9 +4079,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.13.tgz",
"integrity": "sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==",
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz",
"integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==",
"cpu": [
"arm64"
],
@@ -4095,9 +4095,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.13.tgz",
"integrity": "sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==",
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz",
"integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==",
"cpu": [
"arm64"
],
@@ -4111,9 +4111,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.13.tgz",
"integrity": "sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==",
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz",
"integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==",
"cpu": [
"x64"
],
@@ -4127,9 +4127,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.13.tgz",
"integrity": "sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==",
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz",
"integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==",
"cpu": [
"x64"
],
@@ -4143,9 +4143,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.13.tgz",
"integrity": "sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==",
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz",
"integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==",
"cpu": [
"arm"
],
@@ -4159,9 +4159,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.13.tgz",
"integrity": "sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==",
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz",
"integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==",
"cpu": [
"arm64"
],
@@ -4178,9 +4178,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.13.tgz",
"integrity": "sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==",
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz",
"integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==",
"cpu": [
"arm64"
],
@@ -4197,9 +4197,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.13.tgz",
"integrity": "sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==",
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz",
"integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==",
"cpu": [
"ppc64"
],
@@ -4216,9 +4216,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.13.tgz",
"integrity": "sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==",
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz",
"integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==",
"cpu": [
"s390x"
],
@@ -4235,9 +4235,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.13.tgz",
"integrity": "sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==",
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz",
"integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==",
"cpu": [
"x64"
],
@@ -4254,9 +4254,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.13.tgz",
"integrity": "sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==",
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz",
"integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==",
"cpu": [
"x64"
],
@@ -4273,9 +4273,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.13.tgz",
"integrity": "sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==",
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz",
"integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==",
"cpu": [
"arm64"
],
@@ -4289,9 +4289,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.13.tgz",
"integrity": "sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==",
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz",
"integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==",
"cpu": [
"arm64"
],
@@ -4305,9 +4305,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.13.tgz",
"integrity": "sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==",
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz",
"integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==",
"cpu": [
"x64"
],
@@ -5985,18 +5985,18 @@
}
},
"node_modules/body-parser": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
"integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.4.0.tgz",
"integrity": "sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
"content-type": "^2.0.0",
"content-type": "^2.1.0",
"debug": "^4.4.3",
"http-errors": "^2.0.1",
"iconv-lite": "^0.7.2",
"iconv-lite": "^0.7.3",
"on-finished": "^2.4.1",
"qs": "^6.15.2",
"qs": "^6.16.1",
"raw-body": "^3.0.2",
"type-is": "^2.1.0"
},
@@ -6462,9 +6462,9 @@
}
},
"node_modules/core-js": {
"version": "3.50.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz",
"integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==",
"version": "3.51.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.51.0.tgz",
"integrity": "sha512-7bDKx/5jm5MxzsXxc93g6Z2+4ZsuT/MddrfRmTDJNAkWsYWHU8EVNEsBiIEWnMAMCHMmRfUEjkuo3jeNBOIHRw==",
"hasInstallScript": true,
"license": "MIT",
"engines": {
@@ -11538,9 +11538,9 @@
}
},
"node_modules/marked-emoji": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/marked-emoji/-/marked-emoji-2.0.3.tgz",
"integrity": "sha512-fChW/AfUqCHgoEC1nFDgiw3OR/qsi71/QXH/HTo05yd6B5+T+VHh1SqCpn/HpeGLDxkA+MK4+hr4eULB2/A8Jw==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/marked-emoji/-/marked-emoji-3.0.0.tgz",
"integrity": "sha512-4IODef6jiQfuRsWOXSH/q9YiXmpVemvm1raj4WmObkWeAPg5bMMPTvbDpxbn4BIvbM245a1Dhq6RAzCS0WFGOw==",
"license": "MIT",
"peerDependencies": {
"marked": ">=4 <19"
@@ -11568,25 +11568,25 @@
}
},
"node_modules/marked-hbfm": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/marked-hbfm/-/marked-hbfm-1.0.1.tgz",
"integrity": "sha512-mqz1ALIk0POeRCGjAGDTRhyKrMe+i5ycsxLV/8MlG13xC/oTEXA66VHZvIMNLotdM8jNNnLC9TFLCWECN4QHug==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/marked-hbfm/-/marked-hbfm-1.0.3.tgz",
"integrity": "sha512-j+TLQqHv1PGb/ypxh8r+kGlgto1t3373GxxDn1gTBk473QC85uV4PteZfLLq8U5H2OLODbbyEGzxVvYdnOT0lQ==",
"license": "MIT",
"dependencies": {
"eslint-plugin-jest": "^29.16.2",
"eslint-plugin-react": "^7.37.5",
"lodash": "^4.18.1",
"marked": ">=3 <19",
"marked-alignment-paragraphs": "^1.0.0",
"marked-definition-lists": "^1.0.1",
"marked-diagrams-markdeep": "^1.0.1",
"marked-emoji": "^2.0.3",
"marked-extended-tables": "^2.0.1",
"marked-gfm-heading-id": "^4.1.4",
"marked-nonbreaking-spaces": "^1.0.1",
"marked-smartypants-lite": "^1.0.3",
"marked-subsuper-text": "^1.0.4",
"marked-variables": "^1.0.7"
"marked-alignment-paragraphs": "1.0.0",
"marked-definition-lists": "1.0.1",
"marked-diagrams-markdeep": "1.0.1",
"marked-emoji": "3.0.0",
"marked-extended-tables": "2.0.1",
"marked-gfm-heading-id": "4.1.4",
"marked-nonbreaking-spaces": "1.0.1",
"marked-smartypants-lite": "1.0.3",
"marked-subsuper-text": "1.0.4",
"marked-variables": "2.0.0"
}
},
"node_modules/marked-nonbreaking-spaces": {
@@ -11617,9 +11617,9 @@
}
},
"node_modules/marked-variables": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/marked-variables/-/marked-variables-1.0.7.tgz",
"integrity": "sha512-b7ZxKQJ9TEF/xi7dpP//IyIvK10minaYqmQNX7K91K9189jSQ8grpxY9yOIAXpPmVfReY0aZbfxugolTBIpXAA==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/marked-variables/-/marked-variables-2.0.0.tgz",
"integrity": "sha512-U3LQ7mXfXpM0H2jExenENOvuFrP4JzSksRUr0yyENxZEkK3jmSlUEdT9LBuYxjOqWlMMhqiFVPOmtm1/QJnS+A==",
"license": "MIT",
"dependencies": {
"expr-eval": "^2.0.2",
@@ -11893,16 +11893,16 @@
}
},
"node_modules/mongoose": {
"version": "9.11.2",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.11.2.tgz",
"integrity": "sha512-hFkl/2dccpD58coRAya3a/ht8ooAx9YPLzBfxVX9bEsgcrjsNkvuWQe8gplGfDdmzpw2RfFX1IB/G8PiFwyvwg==",
"version": "9.11.4",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.11.4.tgz",
"integrity": "sha512-+xME0fduXl1sWF+NZpJHZaNcwQSt+X2vbJ/h7GRSZ/ltLycAkGZSEABhzic0+q3IMF66KqF0viEE5+frchKCAQ==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"kareem": "3.4.1",
"mongodb": "~7.7",
"mpath": "0.9.0",
"mquery": "6.0.0",
"mquery": "6.0.1",
"ms": "2.1.3",
"sift": "17.1.3"
},
@@ -11970,9 +11970,9 @@
}
},
"node_modules/mquery": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz",
"integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.1.tgz",
"integrity": "sha512-/Nijmq3JcpHwDM+5CwBp5yEl6fUa6jLsIhPsoQywlGXU/ZrKIvSpBzfaGnW9BLqcne/CoUwd1ch7MRqptDgFTA==",
"license": "MIT",
"engines": {
"node": ">=20.19.0"
@@ -12138,9 +12138,9 @@
}
},
"node_modules/node": {
"version": "26.12.0",
"resolved": "https://registry.npmjs.org/node/-/node-26.12.0.tgz",
"integrity": "sha512-/P4rDRs4lOBFW0UUDb1Yuy02aDd9oh/RHZfl3haBnatqUcDnY0np4fo9EgK7zhX09bzZ/uroJfyzW+Tgo5kyiA==",
"version": "26.12.1",
"resolved": "https://registry.npmjs.org/node/-/node-26.12.1.tgz",
"integrity": "sha512-SQVeTwweXXfQfpe5gM1648UTnN640HYyqdu06+NxQjkr8d+ppo8exq5BmhS3lk+wYLwVPrqfIAuwGrM7txcBfg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -12713,9 +12713,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.29",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz",
"integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==",
"version": "8.5.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz",
"integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==",
"funding": [
{
"type": "opencollective",
@@ -12732,7 +12732,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.19",
"nanoid": "^3.3.20",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.2"
},
@@ -12813,9 +12813,9 @@
"license": "MIT"
},
"node_modules/postcss/node_modules/nanoid": {
"version": "3.3.19",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
"integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==",
"version": "3.3.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
"integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
"funding": [
{
"type": "github",
@@ -13028,9 +13028,9 @@
"license": "MIT"
},
"node_modules/qs": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
"integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz",
"integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==",
"license": "BSD-3-Clause",
"dependencies": {
"es-define-property": "^1.0.1",
@@ -13130,9 +13130,9 @@
"license": "MIT"
},
"node_modules/react-router": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-8.4.0.tgz",
"integrity": "sha512-JtydAkBvU9UTEe5qNC+POhu+ZBNM7rlKY2IegwXc7Idj7xfRG16x5RZrw3mju+XlqBxfvb2ky9P3jUp9WyWC1g==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-8.5.0.tgz",
"integrity": "sha512-9nEd/deifcTJIIcUaNIyQGkyXD0D6QVB9XD2O5GgHpP3xu3U5RXm87YQ8++Wn8JQWgMBM/5+PjWPM/7n7O7WHA==",
"license": "MIT",
"dependencies": {
"@remix-run/route-pattern": "^0.22.1",
@@ -13142,10 +13142,18 @@
"node": ">=22.22.0"
},
"peerDependencies": {
"@types/react": ">=19.2.0",
"@types/react-dom": ">=19.2.0",
"react": ">=19.2.7",
"react-dom": ">=19.2.7"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
},
"react-dom": {
"optional": true
}
@@ -13322,12 +13330,12 @@
}
},
"node_modules/rolldown": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.13.tgz",
"integrity": "sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==",
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz",
"integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==",
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.153.0",
"@oxc-project/types": "=0.155.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@@ -13337,21 +13345,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm-eabi": "1.2.13",
"@rolldown/binding-android-arm64": "1.2.13",
"@rolldown/binding-darwin-arm64": "1.2.13",
"@rolldown/binding-darwin-x64": "1.2.13",
"@rolldown/binding-freebsd-x64": "1.2.13",
"@rolldown/binding-linux-arm-gnueabihf": "1.2.13",
"@rolldown/binding-linux-arm64-gnu": "1.2.13",
"@rolldown/binding-linux-arm64-musl": "1.2.13",
"@rolldown/binding-linux-ppc64-gnu": "1.2.13",
"@rolldown/binding-linux-s390x-gnu": "1.2.13",
"@rolldown/binding-linux-x64-gnu": "1.2.13",
"@rolldown/binding-linux-x64-musl": "1.2.13",
"@rolldown/binding-openharmony-arm64": "1.2.13",
"@rolldown/binding-win32-arm64-msvc": "1.2.13",
"@rolldown/binding-win32-x64-msvc": "1.2.13"
"@rolldown/binding-android-arm-eabi": "1.2.15",
"@rolldown/binding-android-arm64": "1.2.15",
"@rolldown/binding-darwin-arm64": "1.2.15",
"@rolldown/binding-darwin-x64": "1.2.15",
"@rolldown/binding-freebsd-x64": "1.2.15",
"@rolldown/binding-linux-arm-gnueabihf": "1.2.15",
"@rolldown/binding-linux-arm64-gnu": "1.2.15",
"@rolldown/binding-linux-arm64-musl": "1.2.15",
"@rolldown/binding-linux-ppc64-gnu": "1.2.15",
"@rolldown/binding-linux-s390x-gnu": "1.2.15",
"@rolldown/binding-linux-x64-gnu": "1.2.15",
"@rolldown/binding-linux-x64-musl": "1.2.15",
"@rolldown/binding-openharmony-arm64": "1.2.15",
"@rolldown/binding-win32-arm64-msvc": "1.2.15",
"@rolldown/binding-win32-x64-msvc": "1.2.15"
}
},
"node_modules/romans": {
@@ -14173,9 +14181,9 @@
}
},
"node_modules/stylelint-config-recess-order": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/stylelint-config-recess-order/-/stylelint-config-recess-order-7.8.0.tgz",
"integrity": "sha512-ObYusdkXavm8ulgb5xvmU1l8wgCdveNYG12hOTUEdjS8bNhSRggl4or0mE0JaOnQz+Ve2xZemVpwqERCgJbntg==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/stylelint-config-recess-order/-/stylelint-config-recess-order-7.9.0.tgz",
"integrity": "sha512-Pu3zhefjou7PYNkEtx4uLnsCSmXpiNIx8ALd20r+PGO1LApcf2Gowh2pKEaY9h8gdR8Wp6rUGqhqEIAhdyoHxg==",
"dev": true,
"license": "ISC",
"peerDependencies": {
@@ -15114,15 +15122,15 @@
}
},
"node_modules/vite": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz",
"integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==",
"version": "8.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz",
"integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==",
"license": "MIT",
"dependencies": {
"lightningcss": "^1.33.0",
"picomatch": "^4.0.7",
"postcss": "^8.5.29",
"rolldown": "~1.2.12",
"postcss": "^8.5.30",
"rolldown": "~1.2.14",
"tinyglobby": "^0.2.17"
},
"bin": {
@@ -15139,7 +15147,7 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.7.1",
"@vitejs/devtools": "^0.7.1 || ^0.8.0",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
+15 -14
View File
@@ -1,7 +1,7 @@
{
"name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.24.1",
"version": "3.24.2",
"type": "module",
"engines": {
"npm": ">=10.8 <12",
@@ -14,6 +14,7 @@
"scripts": {
"start": "node server.js",
"build": "vite build",
"build:clean": "node -e \"const fs=require('fs'); fs.rmSync('.vite',{recursive:true,force:true}); fs.rmSync('node_modules/.vite',{recursive:true,force:true});\" && vite build",
"lint": "eslint --fix",
"lint:dry": "eslint",
"stylelint": "stylelint --fix **/*.{less}",
@@ -88,12 +89,12 @@
]
},
"dependencies": {
"@babel/core": "^8.0.7",
"@babel/core": "^8.0.9",
"@babel/plugin-transform-runtime": "^8.0.6",
"@babel/preset-env": "^8.0.7",
"@babel/preset-react": "^8.0.1",
"@babel/runtime": "^8.0.7",
"@codemirror/autocomplete": "^6.20.3",
"@babel/runtime": "^8.0.8",
"@codemirror/autocomplete": "^6.20.4",
"@codemirror/commands": "^6.11.1",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.12",
@@ -102,17 +103,17 @@
"@codemirror/language-data": "^6.5.2",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.7.6",
"@codemirror/view": "^6.43.14",
"@googleapis/drive": "^26.0.2",
"@codemirror/view": "^6.43.15",
"@googleapis/drive": "^27.0.0",
"@lezer/highlight": "^1.2.4",
"@oddbird/css-anchor-positioning": "^0.10.2",
"@sanity/diff-match-patch": "^3.2.1",
"@vitejs/plugin-react": "^6.1.2",
"body-parser": "^2.3.0",
"body-parser": "^2.4.0",
"classnames": "^2.5.1",
"codemirror-5-themes": "^1.5.3",
"cookie-parser": "^1.4.7",
"core-js": "^3.50.0",
"core-js": "^3.51.0",
"cors": "^2.8.5",
"create-react-class": "^15.7.0",
"dedent": "^1.7.2",
@@ -127,18 +128,18 @@
"jwt-simple": "^0.5.6",
"less": "^4.9.1",
"lodash": "^4.18.1",
"marked-hbfm": "^1.0.1",
"marked-hbfm": "^1.0.3",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.31.0",
"mongoose": "^9.11.2",
"mongoose": "^9.11.4",
"nanoid": "6.0.2",
"nconf": "^0.13.0",
"node": "^26.12.0",
"node": "^26.12.1",
"prettier": "^3.9.10",
"react": "^19.3.1",
"react-dom": "^19.3.1",
"react-frame-component": "^5.3.2",
"react-router": "^8.4.0",
"react-router": "^8.5.0",
"sanitize-filename": "1.6.4",
"superagent": "^10.4.1"
},
@@ -156,9 +157,9 @@
"jsdom-global": "^3.0.2",
"postcss-less": "^6.0.0",
"stylelint": "^17.16.0",
"stylelint-config-recess-order": "^7.7.0",
"stylelint-config-recess-order": "^7.9.0",
"stylelint-config-recommended": "^18.0.0",
"supertest": "^7.3.1",
"vite": "^8.3.4"
"vite": "^8.3.5"
}
}
+78 -135
View File
@@ -2,7 +2,6 @@
import _ from 'lodash';
import { model as HomebrewModel } from './homebrew.model.js';
import express from 'express';
import zlib from 'zlib';
import GoogleActions from './googleActions.js';
import { hbfm } from 'marked-hbfm';
import * as yaml from 'js-yaml';
@@ -27,17 +26,8 @@ const isStaticTheme = (renderer, themeName)=>{
return Themes[renderer]?.[themeName] !== undefined;
};
// const getTopBrews = (cb) => {
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
// cb(brews);
// });
// };
const migrateSystemsToTags = (brew)=>{
if(!('systems' in brew)) return brew;
if(!Array.isArray(brew.systems) || brew.systems.length === 0) {
if(!('systems' in brew) || !Array.isArray(brew.systems) || brew.systems.length === 0) {
brew.systems = undefined;
return brew;
}
@@ -108,19 +98,17 @@ const api = {
const brews = await HomebrewModel.getByUser(username, true, fields, { tags: { $in: ['meta:theme', 'meta:Theme'] } });
if(brews) {
for (const brew of brews) {
userThemes[brew.renderer] ??= {};
userThemes[brew.renderer][brew.shareId] = {
name : brew.title,
renderer : brew.renderer,
baseTheme : brew.theme,
baseSnippets : false,
author : brew.authors[0],
path : brew.shareId,
thumbnail : brew.thumbnail || '/assets/naturalCritLogoWhite.svg'
};
}
for (const brew of brews) {
userThemes[brew.renderer] ??= {};
userThemes[brew.renderer][brew.shareId] = {
name : brew.title,
renderer : brew.renderer,
baseTheme : brew.theme,
baseSnippets : false,
author : brew.authors[0],
path : brew.shareId,
thumbnail : brew.thumbnail || '/assets/naturalCritLogoWhite.svg'
};
}
return userThemes;
@@ -189,18 +177,14 @@ const api = {
stub.renderer = stub.renderer || undefined; // Clear empty strings
stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
const fixedStub = migrateSystemsToTags(stub);
req.brew = fixedStub;
req.brew = migrateSystemsToTags(stub);
splitTextStyleAndMetadata(req.brew);
next();
};
},
getCSS : async (req, res)=>{
const { brew } = req;
if(!brew) return res.status(404).send('');
if(!brew.style) return res.status(404).send('');
if(!brew?.style) return res.status(404).send('');
res.set({
'Cache-Control' : 'no-cache',
@@ -216,19 +200,15 @@ const api = {
`\`\`\`\n\n` +
`${text}`;
}
const metadata = _.pick(brew, ['title', 'description', 'tags', 'renderer', 'theme']);
const metadata = _.pick(brew, ['title', 'description', 'tags', 'renderer', 'theme', 'columns', 'columnGutter', 'license', 'legalAuthors']);
const snippetsArray = brewSnippetsToJSON('brew_snippets', brew.snippets, null, false).snippets;
metadata.snippets = snippetsArray.length > 0 ? snippetsArray : undefined;
metadata.bleedSize = { top: brew?.bleedSize?.top, bottom: brew?.bleedSize?.bottom, inner: brew?.bleedSize?.inner, outer: brew?.bleedSize?.outer };
metadata.safetySpace = { top: brew?.safetySpace?.top, bottom: brew?.safetySpace?.bottom, outer: brew?.safetySpace?.outer, inner: brew?.safetySpace?.inner };
metadata.trimSize = { width: brew?.trimSize?.width, height: brew?.trimSize?.height };
metadata.columns = brew?.columns;
metadata.columnGutter = brew?.columnGutter;
metadata.license = brew?.license;
metadata.legalAuthors = brew?.legalAuthors;
text = `\`\`\`metadata\n` +
`${yaml.dump(metadata)}\n` +
`${yaml.dump(metadata)}` +
`\`\`\`\n\n` +
`${text}`;
return text;
@@ -238,28 +218,14 @@ const api = {
return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title')
.slice(0, MAX_TITLE_LENGTH);
},
excludePropsFromUpdate : (brew)=>{
// Remove undesired properties
const modified = _.clone(brew);
const propsToExclude = ['_id', 'views', 'lastViewed'];
for (const prop of propsToExclude) {
delete modified[prop];
}
return modified;
excludePropsFromUpdate : (brew)=>{ //Remove props that should not update/overwrite when editing
return _.omit(brew, ['_id', 'views', 'lastViewed']);
},
excludeGoogleProps : (brew)=>{
const modified = _.clone(brew);
const propsToExclude = ['version', 'tags', 'systems', 'published', 'authors', 'owner', 'views', 'thumbnail'];
for (const prop of propsToExclude) {
delete modified[prop];
}
return modified;
excludeGoogleProps : (brew)=>{ //Remove props that should not be saved to Google (only stored to Homebrewery stub)
return _.omit(brew, ['version', 'tags', 'systems', 'published', 'authors', 'invitedAuthors', 'owner', 'views', 'thumbnail']);
},
excludeStubProps : (brew)=>{
const propsToExclude = ['text', 'textBin'];
for (const prop of propsToExclude) {
brew[prop] = undefined;
}
excludeStubProps : (brew)=>{ //Remove props that should not be saved to stub-only brews
brew.text = brew.textBin = undefined; // Can't use _.omit because this brew is a Mongoose object
return brew;
},
beforeNewSave : (account, brew)=>{
@@ -278,9 +244,7 @@ const api = {
newGoogleBrew : async (account, brew, res)=>{
const oAuth2Client = GoogleActions.authCheck(account, res);
const newBrew = api.excludeGoogleProps(brew);
return await GoogleActions.newGoogleBrew(oAuth2Client, newBrew);
return await GoogleActions.newGoogleBrew(oAuth2Client, api.excludeGoogleProps(brew));
},
newBrew : async (req, res)=>{
const brew = req.body;
@@ -292,7 +256,7 @@ const api = {
api.beforeNewSave(req.account, brew);
const newHomebrew = new HomebrewModel(brew);
let newHomebrew = new HomebrewModel(brew);
newHomebrew.editId = nanoid(12);
newHomebrew.shareId = nanoid(12);
@@ -301,15 +265,10 @@ const api = {
googleId = await api.newGoogleBrew(req.account, newHomebrew, res);
if(!googleId) return;
api.excludeStubProps(newHomebrew);
newHomebrew = api.excludeStubProps(newHomebrew);
newHomebrew.googleId = googleId;
} else {
// Compress brew text to binary before saving
newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
// Delete the non-binary text field since it's not needed anymore
newHomebrew.text = undefined;
}
saved = await newHomebrew.save()
.catch((err)=>{
console.error(err, err.toString(), err.stack);
@@ -380,7 +339,6 @@ const api = {
return res.status(200).send(returnObj);
},
updateBrew : async (req, res)=>{
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
const brewFromClient = api.excludePropsFromUpdate(req.body);
const brewFromServer = req.brew;
@@ -406,16 +364,17 @@ const api = {
const patches = parsePatch(brewFromClient.patches);
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
result = applyPatches(patches, encodeURI(brewFromServer.text));
const failedPatches = patches.map((patch, index)=>{if(!result[1][index]){ return patch; }});
if(failedPatches > 0){
throw (`Patch failure: ${failedPatches}/${result[1].length} did not apply`);
const failedPatchCount = result[1].filter((patchApplied)=>!patchApplied).length; //Count how many patches failed to apply
if(failedPatchCount){
throw (`Patch failure: ${failedPatchCount}/${result[1].length} did not apply`);
}
if(decodeURI(result[0]) != brewFromClient.text){
const patchedText = decodeURI(result[0]);
if(patchedText !== brewFromClient.text){
debugTextMismatch(brewFromClient.text, patchedText, `edit/${brewFromClient.editId}`);
throw ('Patches did not apply cleanly, text mismatch detected');
}
// brew.text = applyPatches(patches, brewFromServer.text)[0];
} catch (err) {
debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
console.error('Failed to apply patches:', {
// patches : brewFromClient.patches,
// result : result,
@@ -426,77 +385,63 @@ const api = {
// throw err; // rethrow to preserve the 500 behavior
}
let brew = _.assign(brewFromServer, brewFromClient);
migrateSystemsToTags(brew);
let brew = _.assign(brewFromServer, brewFromClient);
brew = migrateSystemsToTags(brew);
brew.title = brew.title.trim();
brew.description = brew.description.trim() || '';
brew.text = api.mergeBrewText(brew);
const googleId = brew.googleId;
const { saveToGoogle, removeFromGoogle } = req.query;
let afterSave = async ()=>true;
if(brew.googleId && removeFromGoogle) {
// If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined
afterSave = async ()=>{
return await api.deleteGoogleBrew(req.account, googleId, brew.editId, res)
.catch((err)=>{
console.error(err);
res.status(err?.status || err?.response?.status || 500).send(err.message || err);
});
};
brew.googleId = undefined;
} else if(!brew.googleId && saveToGoogle) {
// If we don't have a google id and the user wants to save to google, create the google brew and set the google id on the brew
brew.googleId = await api.newGoogleBrew(req.account, api.excludeGoogleProps(brew), res);
if(!brew.googleId) return;
} else if(brew.googleId) {
// If the google id exists and no other actions are being performed, update the google brew
const updated = await GoogleActions.updateGoogleBrew(api.excludeGoogleProps(brew), req.ip);
if(!updated) return;
}
if(brew.googleId) {
// If the google id exists after all those actions, exclude the props that are stored in google and aren't needed for rendering the brew items
api.excludeStubProps(brew);
} else {
// Compress brew text to binary before saving
brew.textBin = zlib.deflateRawSync(brew.text);
// Delete the non-binary text field since it's not needed anymore
brew.text = undefined;
}
brew.updatedAt = new Date();
brew.version = (brew.version || 1) + 1;
brew.updatedAt = new Date();
brew.version = (brew.version || 1) + 1;
if(req.account) {
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
brew.invitedAuthors = _.uniq(_.filter(brew.invitedAuthors, (a)=>req.account.username !== a));
}
// define a function to catch our save errors
const saveError = (err)=>{
console.error(err);
res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database');
};
let saved;
if(!brew._id) {
// if the brew does not have a stub id, create and save it, then write the new value back to the brew.
saved = await new HomebrewModel(brew).save().catch(saveError);
} else {
// if the brew does have a stub id, update it using the stub id as the key.
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
saved = await brew.save()
.catch(saveError);
//=====----- Update to or remove from Google Drive -----=========//
const originalGoogleId = brew.googleId;
const { saveToGoogle, removeFromGoogle } = req.query;
if(brew.googleId && removeFromGoogle) { // If removing from google, remove the brew's googleId
brew.googleId = undefined;
}
else if(!brew.googleId && saveToGoogle) { // If adding to google, create new google document and record the googleId
brew.googleId = await api.newGoogleBrew(req.account, brew, res);
if(!brew.googleId) return;
}
else if(brew.googleId) { // If already on google, update the google brew
const updated = await GoogleActions.updateGoogleBrew(api.excludeGoogleProps(brew), req.ip);
if(!updated) return;
}
//=====----- Update to Mongo -----=====//
if(brew.googleId) {
// If google id exists after above actions, Mongo will store only a stub; exclude non-stub properties from update
brew = api.excludeStubProps(brew);
}
if(brew._id) // if the brew has a stub id, fetch and merge into Mongo stub document
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
else // if not, create a new Mongo document
brew = new HomebrewModel(brew);
const saved = await brew.save()
.catch((err)=>{
console.error(err);
res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database');
});
if(!saved) return;
// Call and wait for afterSave to complete
const after = await afterSave();
if(!after) return;
if(originalGoogleId && removeFromGoogle) {
const deleted = await api.deleteGoogleBrew(req.account, originalGoogleId, brew.editId, res)
.catch((err)=>{
console.error(err);
res.status(err?.status || err?.response?.status || 500).send(err.message || err);
});
if(!deleted) return;
}
saved.textBin = undefined; // Remove textBin from the saved object to save bandwidth
@@ -549,8 +494,6 @@ const api = {
if(shouldDeleteGoogleBrew) {
// When there are still authors remaining, we delete the google brew but store the full brew in the Homebrewery database
brew.googleId = undefined;
brew.textBin = zlib.deflateRawSync(brew.text);
brew.text = undefined;
}
brew.markModified('authors'); //Mongo will not properly update arrays without markModified()
await brew.save()
+70 -63
View File
@@ -1,6 +1,7 @@
/* eslint-disable max-lines */
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
import dedent from 'dedent';
describe('Tests for api', ()=>{
let api;
@@ -493,35 +494,35 @@ describe('Tests for api', ()=>{
},
});
expect(result).toEqual(`\`\`\`metadata
title: some title
description: this is a description
tags:
- something
- fun
renderer: v3
theme: phb
bleedSize:
top: .125in
bottom: .125in
inner: .125in
outer: .125in
safetySpace:
top: .25in
bottom: .25in
outer: .25in
inner: .5in
trimSize:
width: 8.5in
height: 11in
columns: '2'
columnGutter: .125in
license: None
legalAuthors: ''
expect(result).toEqual(dedent`
\`\`\`metadata
title: some title
description: this is a description
tags:
- something
- fun
renderer: v3
theme: phb
columns: '2'
columnGutter: .125in
license: None
legalAuthors: ''
bleedSize:
top: .125in
bottom: .125in
inner: .125in
outer: .125in
safetySpace:
top: .25in
bottom: .25in
outer: .25in
inner: .5in
trimSize:
width: 8.5in
height: 11in
\`\`\`
\`\`\`
brew`);
brew`);
});
it('should set metadata and style', ()=>{
@@ -556,39 +557,39 @@ brew`);
},
});
expect(result).toEqual(`\`\`\`metadata
title: some title
description: this is a description
tags:
- something
- fun
renderer: v3
theme: phb
bleedSize:
top: .125in
bottom: .125in
inner: .125in
outer: .125in
safetySpace:
top: .25in
bottom: .25in
outer: .25in
inner: .5in
trimSize:
width: 8.5in
height: 11in
columns: '2'
columnGutter: .125in
license: None
legalAuthors: ''
expect(result).toEqual(dedent`
\`\`\`metadata
title: some title
description: this is a description
tags:
- something
- fun
renderer: v3
theme: phb
columns: '2'
columnGutter: .125in
license: None
legalAuthors: ''
bleedSize:
top: .125in
bottom: .125in
inner: .125in
outer: .125in
safetySpace:
top: .25in
bottom: .25in
outer: .25in
inner: .5in
trimSize:
width: 8.5in
height: 11in
\`\`\`
\`\`\`
\`\`\`css
hello yes i am css
\`\`\`
\`\`\`css
hello yes i am css
\`\`\`
brew`);
brew`);
});
});
describe('exclusion methods', ()=>{
@@ -700,8 +701,15 @@ brew`);
shareId : expect.any(String),
style : undefined,
tags : [],
text : undefined,
textBin : expect.objectContaining({}),
text : '```metadata\n' +
'title: asdf\n' +
'bleedSize: {}\n' +
'safetySpace: {}\n' +
'trimSize: {}' +
'\n' +
'```\n' +
'\n' +
'asdf',
theme : '5ePHB',
thumbnail : '',
title : 'asdf',
@@ -1233,8 +1241,7 @@ brew`);
expect(saveFunc).toHaveBeenCalled();
expect(saved.authors).toEqual(['test2']);
expect(saved.googleId).toEqual(undefined);
expect(saved.text).toEqual(undefined);
expect(saved.textBin).not.toEqual(undefined);
expect(saved.text).not.toEqual(undefined);
});
it('should retain google brew and update stub when multiple authors and extra author requests deletion', async ()=>{
+11 -3
View File
@@ -32,14 +32,21 @@ const HomebrewSchema = mongoose.Schema({
lock : { type: Object, index: true }
}, { versionKey: false });
HomebrewSchema.pre('save', function() {
if(this.text !== undefined) {
this.textBin = zlib.deflateRawSync(this.text);
this.text = undefined;
}
});
HomebrewSchema.statics.increaseView = async function(query) {
const brew = await Homebrew.findOne(query).exec();
brew.lastViewed = new Date();
brew.views = brew.views + 1;
await brew.save()
.catch((err)=>{
return err;
});
.catch((err)=>{
return err;
});
return brew;
};
@@ -51,6 +58,7 @@ HomebrewSchema.statics.get = async function(query, fields=null){
if(!_.isNil(brew.textBin)) { // Uncompress zipped text field
const unzipped = zlib.inflateRawSync(brew.textBin);
brew.text = unzipped.toString();
brew.textBin = undefined;
}
return brew;
};
+7 -3
View File
@@ -28,8 +28,10 @@ export function generateAssetsPlugin(isDev = false) {
// Compile Legacy themes
const themes = { Legacy: {}, V3: {} };
const legacyDirs = fs.readdirSync('./themes/Legacy');
for (const dir of legacyDirs) {
const legacyDirs = fs.readdirSync('./themes/Legacy', { withFileTypes: true })
.filter(entry => entry.isDirectory())
.map(entry => entry.name);
for (const dir of legacyDirs) {
const themeData = JSON.parse(fs.readFileSync(`./themes/Legacy/${dir}/settings.json`, 'utf-8'));
themeData.path = dir;
themes.Legacy[dir] = themeData;
@@ -41,7 +43,9 @@ export function generateAssetsPlugin(isDev = false) {
}
// Compile V3 themes
const v3Dirs = fs.readdirSync('./themes/V3');
const v3Dirs = fs.readdirSync('./themes/V3', { withFileTypes: true })
.filter(entry => entry.isDirectory())
.map(entry => entry.name);
for (const dir of v3Dirs) {
const themeData = JSON.parse(fs.readFileSync(`./themes/V3/${dir}/settings.json`, 'utf-8'));
themeData.path = dir;