Merge branch 'master' into issue_4975

This commit is contained in:
David Bolack
2026-09-23 22:08:10 -05:00
31 changed files with 1196 additions and 1205 deletions
+3
View File
@@ -82,6 +82,9 @@ jobs:
- run: - run:
name: Test - HTML sanitization name: Test - HTML sanitization
command: npm run test:safehtml command: npm run test:safehtml
- run:
name: Test - Helpers
command: npm run test:helpers
- run: - run:
name: Test - Coverage name: Test - Coverage
command: npm run test:coverage command: npm run test:coverage
+3 -4
View File
@@ -128,7 +128,7 @@ Fixes issue [#4858](https://github.com/naturalcrit/homebrewery/issues/4858)
Fixes part of issue [#4101](https://github.com/naturalcrit/homebrewery/issues/4101) Fixes part of issue [#4101](https://github.com/naturalcrit/homebrewery/issues/4101)
##### G-Ambatte ##### G-Ambatte
* [x] Fix editor panel shrinking when openingdev tools * [x] Fix editor panel shrinking when opening dev tools
Fixes issue [#4866](https://github.com/naturalcrit/homebrewery/issues/4866) Fixes issue [#4866](https://github.com/naturalcrit/homebrewery/issues/4866)
@@ -154,7 +154,7 @@ Fixes issue [#4904](https://github.com/naturalcrit/homebrewery/issues/4904)
##### 5e-Cleric, Gazook89 ##### 5e-Cleric, Gazook89
* [x] Fix various issues with Codemirror 6 * [x] Fix various issues with Codemirror 6
Fixes issues [#4771](https://github.com/naturalcrit/homebrewery/issues/4771), [#4583](https://github.com/naturalcrit/homebrewery/issues/4783) Fixes issues [#4771](https://github.com/naturalcrit/homebrewery/issues/4771), [#4783](https://github.com/naturalcrit/homebrewery/issues/4783)
}} }}
\page \page
@@ -170,7 +170,6 @@ Fixes issues [#4771](https://github.com/naturalcrit/homebrewery/issues/4771), [#
##### 5e-Cleric ##### 5e-Cleric
* [x] Add auto-suggest to tag entry input box * [x] Add auto-suggest to tag entry input box
* [x] Replace all example artwork with
* [x] Added tooltips to the {{openSans :fas_circle_info: **Properties**}} menu * [x] Added tooltips to the {{openSans :fas_circle_info: **Properties**}} menu
* [x] Removed {{openSans **SYSTEMS**}} checkboxes from {{openSans :fas_circle_info: **Properties**}} menu; instead {{openSans **TAGS**}} should be used for this purpose * [x] Removed {{openSans **SYSTEMS**}} checkboxes from {{openSans :fas_circle_info: **Properties**}} menu; instead {{openSans **TAGS**}} should be used for this purpose
* [x] Replace all AI-generated art with public domain art * [x] Replace all AI-generated art with public domain art
@@ -222,7 +221,7 @@ Fixes issue [#4559](https://github.com/naturalcrit/homebrewery/issues/4559)
##### G-Ambatte ##### G-Ambatte
* [x] Fix default save location failing on new documents * [x] Fix default save location failing on new documents
Fixes issue [#4437](https://github.com/naturalcrit/homebrewery/issues/3175) Fixes issue [#4437](https://github.com/naturalcrit/homebrewery/issues/4437)
* [x] Fix usernames with special symbols unable to open userpage * [x] Fix usernames with special symbols unable to open userpage
Fixes issue [#807](https://github.com/naturalcrit/homebrewery/issues/807) Fixes issue [#807](https://github.com/naturalcrit/homebrewery/issues/807)
@@ -6,7 +6,7 @@ import React, { useState, useRef, useMemo, useEffect } from 'react';
import _ from 'lodash'; import _ from 'lodash';
import MarkdownLegacy from '@shared/markdownLegacy.js'; import MarkdownLegacy from '@shared/markdownLegacy.js';
import { hbfm } from 'hbmarkedwrapper'; import { hbfm } from 'marked-hbfm';
import ErrorBar from './errorBar/errorBar.jsx'; import ErrorBar from './errorBar/errorBar.jsx';
import ToolBar from './toolBar/toolBar.jsx'; import ToolBar from './toolBar/toolBar.jsx';
@@ -54,7 +54,7 @@ const BrewPage = (props)=>{
// Observer for tracking which pages are at least 30% visible in the iframe // Observer for tracking which pages are at least 30% visible in the iframe
const visibleObserver = new IntersectionObserver( const visibleObserver = new IntersectionObserver(
(entries)=>entries.forEach((entry)=>{ (entries)=>entries.forEach((entry)=>{
props.onVisibilityChange(pageNum, entry.isIntersecting, false); // add page to array of visible pages. props.onVisibilityChange(pageNum, entry.isIntersecting, false); // add/remove page from array of visible pages.
}), }),
{ threshold: .3, rootMargin: '0px 0px 0px 0px' } // detect when >30% of page is within bounds. { threshold: .3, rootMargin: '0px 0px 0px 0px' } // detect when >30% of page is within bounds.
); );
@@ -102,8 +102,11 @@ const BrewRenderer = (props)=>{
...props ...props
}; };
const pagesRef = useRef(null);
const [visiblePages, setVisiblePages] = useState([]); const [visiblePages, setVisiblePages] = useState([]);
const [centerPage , setCenterPage ] = useState(1); const [centerPage , setCenterPage ] = useState(1);
const [headerState , setHeaderState ] = useState(false);
const [state, setState] = useState({ const [state, setState] = useState({
isMounted : false, isMounted : false,
@@ -125,10 +128,6 @@ const BrewRenderer = (props)=>{
toolbarState && setDisplayOptions(toolbarState); toolbarState && setDisplayOptions(toolbarState);
}, []); }, []);
const [headerState, setHeaderState] = useState(false);
const pagesRef = useRef(null);
if(props.renderer == 'legacy') { if(props.renderer == 'legacy') {
rawPages = props.text.split(PAGEBREAK_REGEX_LEGACY); rawPages = props.text.split(PAGEBREAK_REGEX_LEGACY);
} else { } else {
@@ -313,7 +312,7 @@ const BrewRenderer = (props)=>{
}; };
const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]); const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]);
renderedPages = useMemo(()=>renderPages(), [props.text, displayOptions]); renderedPages = useMemo(()=>renderPages(), [props.text, centerPage, displayOptions]);
return ( return (
<> <>
@@ -1,7 +1,7 @@
import './notificationPopup.less'; import './notificationPopup.less';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import { hbfm } from 'hbmarkedwrapper'; import { hbfm } from 'marked-hbfm';
import Dialog from '@components/dialog.jsx'; import Dialog from '@components/dialog.jsx';
+3 -4
View File
@@ -1,6 +1,5 @@
import 'core-js/es/string/to-well-formed.js'; // Polyfill for older browsers import 'core-js/es/string/to-well-formed.js'; // Polyfill for older browsers
import './homebrew.less'; import './homebrew.less';
import React from 'react';
import { BrowserRouter as Router, Routes, Route, useParams, useSearchParams } from 'react-router'; import { BrowserRouter as Router, Routes, Route, useParams, useSearchParams } from 'react-router';
import { updateLocalStorage } from './utils/updateLocalStorage/updateLocalStorageKeys.js'; import { updateLocalStorage } from './utils/updateLocalStorage/updateLocalStorageKeys.js';
@@ -47,7 +46,7 @@ const Homebrew = (props)=>{
global.enablev4 = enablev4; global.enablev4 = enablev4;
const backgroundObject = ()=>{ const backgroundObject = ()=>{
if(config?.deployment || (config?.local && config?.development)) { if(config?.deployment || config?.developmentStyle) {
const bgText = config?.deployment || 'Local'; const bgText = config?.deployment || 'Local';
return { return {
backgroundImage : `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='100px' width='200px'><text x='0' y='15' fill='%23fff7' font-size='20'>${bgText}</text></svg>")` backgroundImage : `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='100px' width='200px'><text x='0' y='15' fill='%23fff7' font-size='20'>${bgText}</text></svg>")`
@@ -61,7 +60,7 @@ const Homebrew = (props)=>{
if(brew.pureError) { if(brew.pureError) {
return ( return (
<Router> <Router>
<div className={`homebrew${(config?.deployment || config?.local) ? ' deployment' : ''}`} style={backgroundObject()}> <div className={`homebrew${(config?.deployment || config?.developmentStyle) ? ' deployment' : ''}`} style={backgroundObject()}>
<Routes> <Routes>
<Route path={brew.originalUrl} element={<WithRoute el={ErrorPage} brew={brew} />} /> <Route path={brew.originalUrl} element={<WithRoute el={ErrorPage} brew={brew} />} />
</Routes> </Routes>
@@ -73,7 +72,7 @@ const Homebrew = (props)=>{
return ( return (
<Router> <Router>
<div className={`homebrew${(config?.deployment || config?.local) ? ' deployment' : ''}`} style={backgroundObject()}> <div className={`homebrew${(config?.deployment || config?.developmentStyle) ? ' deployment' : ''}`} style={backgroundObject()}>
<Routes> <Routes>
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} 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='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} />
+1 -1
View File
@@ -4,7 +4,7 @@ import './editPage.less';
// Common imports // Common imports
import React, { useState, useEffect, useRef, useEffectEvent } from 'react'; import React, { useState, useEffect, useRef, useEffectEvent } from 'react';
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import { hbfm } from 'hbmarkedwrapper'; import { hbfm } from 'marked-hbfm';
import _ from 'lodash'; import _ from 'lodash';
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
@@ -1,7 +1,7 @@
import './errorPage.less'; import './errorPage.less';
import React from 'react'; import React from 'react';
import UIPage from '../basePages/uiPage/uiPage.jsx'; import UIPage from '../basePages/uiPage/uiPage.jsx';
import { hbfm } from 'hbmarkedwrapper'; import { hbfm } from 'marked-hbfm';
import ErrorIndex from './errors/errorIndex.js'; import ErrorIndex from './errors/errorIndex.js';
const ErrorPage = ({ brew })=>{ const ErrorPage = ({ brew })=>{
+1 -1
View File
@@ -4,7 +4,7 @@ import './homePage.less';
// Common imports // Common imports
import React, { useState, useEffect, useRef, useEffectEvent } from 'react'; import React, { useState, useEffect, useRef, useEffectEvent } from 'react';
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import { hbfm } from 'hbmarkedwrapper'; import { hbfm } from 'marked-hbfm';
import _ from 'lodash'; import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
+1 -1
View File
@@ -4,7 +4,7 @@ import './newPage.less';
// Common imports // Common imports
import React, { useState, useEffect, useRef, useEffectEvent } from 'react'; import React, { useState, useEffect, useRef, useEffectEvent } from 'react';
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import { hbfm } from 'hbmarkedwrapper'; import { hbfm } from 'marked-hbfm';
import _ from 'lodash'; import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
+41 -14
View File
@@ -12,12 +12,15 @@ const { both: RecentNavItem } = RecentNavItems;
import Account from '@navbar/account.navitem.jsx'; import Account from '@navbar/account.navitem.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import request from '../../utils/request-middleware.js';
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js'; import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
const SharePage = (props)=>{ const SharePage = (props)=>{
const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props; const { disableMeta = false } = props;
const [currentBrew, setCurrentBrew] = useState(props.brew || DEFAULT_BREW_LOAD);
const [themeBundle, setThemeBundle] = useState({}); const [themeBundle, setThemeBundle] = useState({});
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
@@ -35,9 +38,33 @@ const SharePage = (props)=>{
} }
}; };
const fetchUpdatedBrew = async ()=>{
const response = await request
.get(`/api/fetch/${currentBrew.shareId}`)
.catch((error)=>{
console.log('error at fetching updated brew: ', error);
});
if(response.ok && !!response.body.brew) {
setCurrentBrew(response.body.brew);
}
};
useEffect(()=>{ useEffect(()=>{
document.addEventListener('keydown', handleControlKeys); document.addEventListener('keydown', handleControlKeys);
fetchThemeBundle(undefined, setThemeBundle, brew.renderer, brew.theme); fetchThemeBundle(undefined, setThemeBundle, currentBrew.renderer, currentBrew.theme);
// listen for changes in the brew version
const eventSource = new EventSource('/stream');
eventSource.addEventListener('message', (evt)=>{
const messageData = JSON.parse(evt.data);
if(messageData.eventType == 'brewUpdated'){
if(messageData.shareId == currentBrew.shareId && messageData.version != currentBrew.version) {
console.log('should fetch brew');
fetchUpdatedBrew();
}
}
});
return ()=>{ return ()=>{
document.removeEventListener('keydown', handleControlKeys); document.removeEventListener('keydown', handleControlKeys);
@@ -45,13 +72,13 @@ const SharePage = (props)=>{
}, []); }, []);
const processShareId = ()=>{ const processShareId = ()=>{
return brew.googleId && !brew.stubbed ? brew.googleId + brew.shareId : brew.shareId; return currentBrew.googleId && !currentBrew.stubbed ? currentBrew.googleId + currentBrew.shareId : currentBrew.shareId;
}; };
const renderEditLink = ()=>{ const renderEditLink = ()=>{
if(!brew.editId) return null; if(!currentBrew.editId) return null;
const editLink = brew.googleId && ! brew.stubbed ? brew.googleId + brew.editId : brew.editId; const editLink = currentBrew.googleId && ! currentBrew.stubbed ? currentBrew.googleId + currentBrew.editId : currentBrew.editId;
return ( return (
<Nav.item color='orange' icon='fas fa-pencil-alt' href={`/edit/${editLink}`}> <Nav.item color='orange' icon='fas fa-pencil-alt' href={`/edit/${editLink}`}>
@@ -62,7 +89,7 @@ const SharePage = (props)=>{
const titleEl = ( const titleEl = (
<Nav.item className='brewTitle' style={disableMeta ? { cursor: 'default' } : {}}> <Nav.item className='brewTitle' style={disableMeta ? { cursor: 'default' } : {}}>
{brew.title} {currentBrew.title}
</Nav.item> </Nav.item>
); );
@@ -71,11 +98,11 @@ const SharePage = (props)=>{
<Meta name='robots' content='noindex, nofollow' /> <Meta name='robots' content='noindex, nofollow' />
<Navbar> <Navbar>
<Nav.section className='titleSection'> <Nav.section className='titleSection'>
{disableMeta ? titleEl : <MetadataNav brew={brew}>{titleEl}</MetadataNav>} {disableMeta ? titleEl : <MetadataNav brew={currentBrew}>{titleEl}</MetadataNav>}
</Nav.section> </Nav.section>
<Nav.section> <Nav.section>
{brew.shareId && ( {currentBrew.shareId && (
<> <>
<PrintNavItem /> <PrintNavItem />
<Nav.dropdown> <Nav.dropdown>
@@ -108,18 +135,18 @@ const SharePage = (props)=>{
</Nav.dropdown> </Nav.dropdown>
</> </>
)} )}
<RecentNavItem brew={brew} storageKey='view' /> <RecentNavItem brew={currentBrew} storageKey='view' />
<Account /> <Account />
</Nav.section> </Nav.section>
</Navbar> </Navbar>
<div className='content'> <div className='content'>
<BrewRenderer <BrewRenderer
text={brew.text} text={currentBrew.text}
style={brew.style} style={currentBrew.style}
lang={brew.lang} lang={currentBrew.lang}
renderer={brew.renderer} renderer={currentBrew.renderer}
theme={brew.theme} theme={currentBrew.theme}
themeBundle={themeBundle} themeBundle={themeBundle}
onPageChange={handleBrewRendererPageChange} onPageChange={handleBrewRendererPageChange}
currentBrewRendererPageNum={currentBrewRendererPageNum} currentBrewRendererPageNum={currentBrewRendererPageNum}
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"development": true, "development_style": false,
"host" : "homebrewery.local.naturalcrit.com:8000", "host" : "homebrewery.local.naturalcrit.com:8000",
"naturalcrit_url" : "local.naturalcrit.com:8010", "naturalcrit_url" : "local.naturalcrit.com:8010",
"secret" : "secret", "secret" : "secret",
+913 -1117
View File
File diff suppressed because it is too large Load Diff
+27 -26
View File
@@ -39,6 +39,7 @@
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace", "test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
"test:route": "jest tests/routes/static-pages.test.js --verbose", "test:route": "jest tests/routes/static-pages.test.js --verbose",
"test:safehtml": "jest tests/html/safeHTML.test.js --verbose", "test:safehtml": "jest tests/html/safeHTML.test.js --verbose",
"test:helpers": "jest tests/html/helpers.test.js --verbose",
"phb": "node --experimental-require-module scripts/phb.js", "phb": "node --experimental-require-module scripts/phb.js",
"prod": "set NODE_ENV=production && npm run build", "prod": "set NODE_ENV=production && npm run build",
"postinstall": "npm run build", "postinstall": "npm run build",
@@ -86,13 +87,13 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^8.0.1", "@babel/core": "^8.0.6",
"@babel/plugin-transform-runtime": "^8.0.1", "@babel/plugin-transform-runtime": "^8.0.6",
"@babel/preset-env": "^8.0.2", "@babel/preset-env": "^8.0.6",
"@babel/preset-react": "^8.0.1", "@babel/preset-react": "^8.0.1",
"@babel/runtime": "^8.0.0", "@babel/runtime": "^8.0.5",
"@codemirror/autocomplete": "^6.20.3", "@codemirror/autocomplete": "^6.20.3",
"@codemirror/commands": "^6.11.0", "@codemirror/commands": "^6.11.1",
"@codemirror/highlight": "^0.19.8", "@codemirror/highlight": "^0.19.8",
"@codemirror/lang-css": "^6.3.1", "@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-javascript": "^6.2.5",
@@ -100,17 +101,17 @@
"@codemirror/language": "^6.12.2", "@codemirror/language": "^6.12.2",
"@codemirror/language-data": "^6.5.2", "@codemirror/language-data": "^6.5.2",
"@codemirror/search": "^6.6.0", "@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.6.0", "@codemirror/state": "^6.7.5",
"@codemirror/view": "^6.43.9", "@codemirror/view": "^6.43.12",
"@dmsnell/diff-match-patch": "^1.1.0", "@dmsnell/diff-match-patch": "^1.1.0",
"@googleapis/drive": "^21.0.0", "@googleapis/drive": "^26.0.0",
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "^1.2.3",
"@oddbird/css-anchor-positioning": "^0.10.2", "@oddbird/css-anchor-positioning": "^0.10.2",
"@sanity/diff-match-patch": "^3.2.0", "@sanity/diff-match-patch": "^3.2.0",
"@vitejs/plugin-react": "^6.0.5", "@vitejs/plugin-react": "^6.0.5",
"body-parser": "^2.3.0", "body-parser": "^2.3.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"codemirror-5-themes": "^1.5.1", "codemirror-5-themes": "^1.5.3",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"core-js": "^3.50.0", "core-js": "^3.50.0",
"cors": "^2.8.5", "cors": "^2.8.5",
@@ -118,58 +119,58 @@
"dedent": "^1.7.2", "dedent": "^1.7.2",
"express": "^5.1.0", "express": "^5.1.0",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-static-gzip": "3.0.1", "express-static-gzip": "3.0.2",
"fflate": "^0.8.3", "fflate": "^0.8.3",
"fs-extra": "^11.3.5", "fs-extra": "^11.3.5",
"hash-wasm": "^4.12.0", "hash-wasm": "^4.12.0",
"hbmarkedwrapper": "^1.0.0",
"idb-keyval": "^6.2.5", "idb-keyval": "^6.2.5",
"js-yaml": "^5.3.0", "js-yaml": "^5.4.2",
"jwt-simple": "^0.5.6", "jwt-simple": "^0.5.6",
"less": "^4.8.1", "less": "^4.9.1",
"lodash": "^4.18.1", "lodash": "^4.18.1",
"marked": "15.0.12", "marked": "15.0.12",
"marked-alignment-paragraphs": "^1.0.0", "marked-alignment-paragraphs": "^1.0.0",
"marked-definition-lists": "^1.0.1", "marked-definition-lists": "^1.0.1",
"marked-diagrams-markdeep": "^1.0.1", "marked-diagrams-markdeep": "^1.0.1",
"marked-emoji": "^2.0.3", "marked-emoji": "^3.0.0",
"marked-extended-tables": "^2.0.1", "marked-extended-tables": "^2.0.1",
"marked-gfm-heading-id": "^4.1.4", "marked-gfm-heading-id": "^4.1.4",
"marked-hbfm": "^1.0.1",
"marked-nonbreaking-spaces": "^1.0.1", "marked-nonbreaking-spaces": "^1.0.1",
"marked-smartypants-lite": "^1.0.3", "marked-smartypants-lite": "^1.0.3",
"marked-subsuper-text": "^1.0.4", "marked-subsuper-text": "^1.0.4",
"marked-variables": "^1.0.5", "marked-variables": "^1.0.5",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1", "moment": "^2.31.0",
"mongoose": "^9.9.3", "mongoose": "^9.10.1",
"nanoid": "6.0.1", "nanoid": "6.0.1",
"nconf": "^0.13.0", "nconf": "^0.13.0",
"node": "^26.7.0", "node": "^26.9.0",
"prettier": "^3.8.1", "prettier": "^3.9.8",
"react": "^19.2.7", "react": "^19.3.0",
"react-dom": "^19.2.7", "react-dom": "^19.3.0",
"react-frame-component": "^5.3.2", "react-frame-component": "^5.3.2",
"react-router": "^8.3.0", "react-router": "^8.4.0",
"sanitize-filename": "1.6.4", "sanitize-filename": "1.6.4",
"superagent": "^10.2.1" "superagent": "^10.2.1"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/stylelint-plugin": "^5.3.0", "@stylistic/stylelint-plugin": "^5.3.0",
"babel-jest": "^30.4.1", "babel-jest": "^30.5.2",
"babel-plugin-transform-import-meta": "^3.0.0", "babel-plugin-transform-import-meta": "^3.0.0",
"eslint": "9.7", "eslint": "9.7",
"eslint-plugin-jest": "^29.15.1", "eslint-plugin-jest": "^29.15.1",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"globals": "^16.4.0", "globals": "^16.4.0",
"jest": "^30.4.2", "jest": "^30.5.2",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",
"jsdom": "^30.0.1", "jsdom": "^30.1.0",
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",
"stylelint": "^17.11.1", "stylelint": "^17.15.0",
"stylelint-config-recess-order": "^7.7.0", "stylelint-config-recess-order": "^7.7.0",
"stylelint-config-recommended": "^18.0.0", "stylelint-config-recommended": "^18.0.0",
"supertest": "^7.1.4", "supertest": "^7.1.4",
"vite": "^8.2.1" "vite": "^8.3.0"
} }
} }
+44 -8
View File
@@ -1,4 +1,4 @@
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/
// Set working directory to project root // Set working directory to project root
import { dirname } from 'path'; import { dirname } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
@@ -14,6 +14,7 @@ import express from 'express';
import config from './config.js'; import config from './config.js';
import path from 'path'; import path from 'path';
import fs from 'fs-extra'; import fs from 'fs-extra';
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
import api from './homebrew.api.js'; import api from './homebrew.api.js';
const { homebrewApi, getBrew, getCSS } = api; const { homebrewApi, getBrew, getCSS } = api;
@@ -30,6 +31,8 @@ import contentNegotiation from './middleware/content-negotiation.js';
import bodyParser from 'body-parser'; import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser'; import cookieParser from 'cookie-parser';
import forceSSL from './forcessl.mw.js'; import forceSSL from './forcessl.mw.js';
import Stream from './eventStreamSource.js';
import dbCheck from './middleware/dbCheck.js'; import dbCheck from './middleware/dbCheck.js';
import cors from 'cors'; import cors from 'cors';
@@ -124,15 +127,24 @@ export default async function createApp(vite) {
}; };
app.use(pageRoutes({ app.use(pageRoutes({
defaultMetaTags, defaultMetaTags,
HomebrewModel, HomebrewModel,
sanitizeBrew, sanitizeBrew,
})); }));
//Robots.txt //Robots.txt
app.get('/robots.txt', (req, res)=>{ app.get('/robots.txt', (req, res)=>{
return res.sendFile(`robots.txt`, { root: process.cwd() }); return res.sendFile(`robots.txt`, { root: process.cwd() });
}); });
//serve brew for sharepage rerender
app.get('/api/fetch/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res) => {
const { brew } = req;
brew.authors.includes(req.account?.username)
? sanitizeBrew(brew, 'shareAuthor')
: sanitizeBrew(brew, 'share');
splitTextStyleAndMetadata(brew);
res.json({ brew });
}));
//Serve brew metadata //Serve brew metadata
app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{ app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{
@@ -182,6 +194,7 @@ export default async function createApp(vite) {
} }
}); });
<<<<<<< HEAD
//Edit Page //Edit Page
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{ app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew; req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
@@ -316,6 +329,28 @@ export default async function createApp(vite) {
return next(); return next();
})); }));
=======
// Create Event Stream source for pages to listen to
app.get('/stream', (req, res)=>{
res.writeHead(200, {
'Content-Type' : 'text/event-stream',
'Cache-Control' : 'no-cache',
'Connection' : 'keep-alive',
'Content-Encoding' : 'none'
});
Stream.on('sendUpdate', (event, data)=>{
console.log('Event:', event, '\nData:', data);
res.write(`data: ${JSON.stringify({ ...data, eventType: event })}\n\n`);
});
});
// After Stream starts, send initStream event
setTimeout(()=>{
Stream.emit('sendUpdate', 'initStream', { time: new Date });
}, 1000);
>>>>>>> master
// Local only // Local only
if(isLocalEnvironment){ if(isLocalEnvironment){
@@ -347,11 +382,12 @@ export default async function createApp(vite) {
// Create configuration object // Create configuration object
const configuration = { const configuration = {
local : isLocalEnvironment, local : isLocalEnvironment,
publicUrl : config.get('publicUrl') ?? '', publicUrl : config.get('publicUrl') ?? '',
baseUrl : `${req.protocol}://${req.get('host')}`, baseUrl : `${req.protocol}://${req.get('host')}`,
environment : nodeEnv, environment : nodeEnv,
deployment : config.get('heroku_app_name') ?? '' deployment : config.get('heroku_app_name') ?? '',
developmentStyle : config.get('development_style')
}; };
const props = { const props = {
version : version, version : version,
+9
View File
@@ -0,0 +1,9 @@
import { EventEmitter } from 'events';
const Stream = new EventEmitter;
export default {
emit : function(event) {return Stream.emit(event, ...([...arguments].slice(1)));}, // Arguments doesn't work for arrow functions
on : (event, listener)=>{return Stream.on(event, listener);},
off : (event, listener)=>{return Stream.off(event, listener);}
};
+8 -5
View File
@@ -4,7 +4,7 @@ import { model as HomebrewModel } from './homebrew.model.js';
import express from 'express'; import express from 'express';
import zlib from 'zlib'; import zlib from 'zlib';
import GoogleActions from './googleActions.js'; import GoogleActions from './googleActions.js';
import { hbfm } from 'hbmarkedwrapper'; import { hbfm } from 'marked-hbfm';
import * as yaml from 'js-yaml'; import * as yaml from 'js-yaml';
import asyncHandler from 'express-async-handler'; import asyncHandler from 'express-async-handler';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
@@ -21,6 +21,8 @@ const router = express.Router();
import { DEFAULT_BREW, DEFAULT_BREW_LOAD } from './brewDefaults.js'; import { DEFAULT_BREW, DEFAULT_BREW_LOAD } from './brewDefaults.js';
import Themes from '../themes/themes.json' with { type: 'json' }; import Themes from '../themes/themes.json' with { type: 'json' };
import Stream from './eventStreamSource.js';
const isStaticTheme = (renderer, themeName)=>{ const isStaticTheme = (renderer, themeName)=>{
return Themes[renderer]?.[themeName] !== undefined; return Themes[renderer]?.[themeName] !== undefined;
}; };
@@ -168,8 +170,7 @@ const api = {
const googleBrew = await GoogleActions.getGoogleBrew(oAuth2Client, googleId, id, accessType) const googleBrew = await GoogleActions.getGoogleBrew(oAuth2Client, googleId, id, accessType)
.catch((googleError)=>{ .catch((googleError)=>{
const reason = googleError.errors?.[0].reason; if(googleError.code === 404 || googleError.status === 404)
if(reason == 'notFound')
throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username }; throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username };
else else
throw { ...googleError, HBErrorCode: '01' }; throw { ...googleError, HBErrorCode: '01' };
@@ -499,6 +500,8 @@ const api = {
saved.textBin = undefined; // Remove textBin from the saved object to save bandwidth saved.textBin = undefined; // Remove textBin from the saved object to save bandwidth
Stream.emit('sendUpdate', 'brewUpdated', { time: new Date, shareId: brew.shareId, version: brew.version });
res.status(200).send(saved); res.status(200).send(saved);
}, },
deleteGoogleBrew : async (account, id, editId, res)=>{ deleteGoogleBrew : async (account, id, editId, res)=>{
@@ -572,9 +575,9 @@ const api = {
router.use(dbCheck); router.use(dbCheck);
router.post('/api', checkClientVersion, asyncHandler(api.newBrew)); router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); //alt endpoint, unused
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew)); router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew)); //alt endpoint, unused
router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew)); router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew));
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle)); router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
+2 -1
View File
@@ -229,5 +229,6 @@ export {
printCurrentBrew, printCurrentBrew,
fetchThemeBundle, fetchThemeBundle,
brewSnippetsToJSON, brewSnippetsToJSON,
debugTextMismatch debugTextMismatch,
yamlSnippetsToText
}; };
+114
View File
@@ -0,0 +1,114 @@
import {
fetchThemeBundle,
brewSnippetsToJSON,
debugTextMismatch,
yamlSnippetsToText,
} from '../../shared/helpers.js';
import dedent from 'dedent';
// Marked.js adds line returns after closing tags on some default tokens.
// This removes those line returns for comparison sake.
String.prototype.trimReturns = function(){
return this.replace(/\r?\n|\r/g, '');
};
const emoji = 'df_d12_2';
const brewSnippetsThemeTest = [
{
name : 'Test Theme',
snippets : dedent `
\snippet First Theme Snippet
I am the first theme snippet!
\snippet Second Theme Snippet
I am the second theme Snippet!`,
}
];
const brewSnippetsBrewTest = dedent`
\snippet First Brew Snippet
I am the first brew snippet!
\snippet Second Brew Snippet
I am the second brew Snippet!`;
describe(`brewSnippetsToJSON`, ()=>{
it('converts raw brew snippets without theme snippets to JSON', function() {
const testMenuObject = {
groupName : 'Brew Snippets',
icon : 'fas fa-th-list',
view : 'text',
snippets : [{
name : 'Test Snippets JSON without theme snippets',
subsnippets : [
{
gen : 'I am the first brew snippet!\n',
name : 'First Brew Snippet'
}, {
gen : 'I am the second brew Snippet!',
name: 'Second Brew Snippet'
}
]}]
};
const rendered = brewSnippetsToJSON(`Test Snippets JSON without theme snippets`, brewSnippetsBrewTest, null, true);
expect(rendered).toStrictEqual(testMenuObject);
});
it('converts raw brew snippets with theme snippets to JSON', function() {
const testMenuObject = {
groupName : 'Brew Snippets',
icon : 'fas fa-th-list',
view : 'text',
snippets : [{
gen : '',
icon : '',
name : 'Test Theme',
subsnippets : [
{
gen : 'I am the first theme snippet!\n',
icon : '',
name : 'First Theme Snippet',
},
{
gen : 'I am the second theme Snippet!',
icon : '',
name : 'Second Theme Snippet',
},
]},
{
name : 'Test Snippets JSON with theme snippets',
subsnippets : [
{
gen : 'I am the first brew snippet!\n',
name : 'First Brew Snippet'
},
{
gen : 'I am the second brew Snippet!',
name: 'Second Brew Snippet'
}
]
}]};
const rendered = brewSnippetsToJSON(`Test Snippets JSON with theme snippets`, brewSnippetsBrewTest, brewSnippetsThemeTest, true);
expect(rendered).toStrictEqual(testMenuObject);
});
});
describe(`YAMLSnippetsToText`, ()=>{
it('converts brew snippet YAML to a string ', function() {
const brewSnippetsYAML = [{
subsnippets : [
{
gen : 'I am the first brew snippet!\n',
name : 'First Brew Snippet'
}, {
gen : 'I am the second brew Snippet!',
name: 'Second Brew Snippet'
}
]
}];
const rendered = yamlSnippetsToText(brewSnippetsYAML);
expect(rendered).toBe(`${brewSnippetsBrewTest}\n`);
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
import { hbfm } from 'hbmarkedwrapper'; import { hbfm } from 'marked-hbfm';
test('Processes the markdown within an HTML block if its just a class wrapper', function() { test('Processes the markdown within an HTML block if its just a class wrapper', function() {
const source = '<div>*Bold text*</div>'; const source = '<div>*Bold text*</div>';
+1 -1
View File
@@ -1,6 +1,6 @@
import { hbfm } from 'hbmarkedwrapper'; import { hbfm } from 'marked-hbfm';
describe('Inline Definition Lists', ()=>{ describe('Inline Definition Lists', ()=>{
test('No Term 1 Definition', function() { test('No Term 1 Definition', function() {
+1 -1
View File
@@ -1,4 +1,4 @@
import { hbfm } from 'hbmarkedwrapper'; import { hbfm } from 'marked-hbfm';
import dedent from 'dedent'; import dedent from 'dedent';
// Marked.js adds line returns after closing tags on some default tokens. // Marked.js adds line returns after closing tags on some default tokens.
+1 -1
View File
@@ -1,6 +1,6 @@
import { hbfm } from 'hbmarkedwrapper'; import { hbfm } from 'marked-hbfm';
describe('Hard Breaks', ()=>{ describe('Hard Breaks', ()=>{
test('Single Break', function() { test('Single Break', function() {
+1 -1
View File
@@ -1,7 +1,7 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import dedent from 'dedent'; import dedent from 'dedent';
import { hbfm } from 'hbmarkedwrapper'; import { hbfm } from 'marked-hbfm';
// Marked.js adds line returns after closing tags on some default tokens. // Marked.js adds line returns after closing tags on some default tokens.
// This removes those line returns for comparison sake. // This removes those line returns for comparison sake.
+1 -1
View File
@@ -1,6 +1,6 @@
import {hbfm} from 'hbmarkedwrapper'; import {hbfm} from 'marked-hbfm';
describe('Non-Breaking Spaces Interactions', ()=>{ describe('Non-Breaking Spaces Interactions', ()=>{
test('I am actually a single-line definition list!', function() { test('I am actually a single-line definition list!', function() {
@@ -1,6 +1,6 @@
import { hbfm } from 'hbmarkedwrapper'; import { hbfm } from 'marked-hbfm';
describe('Justification', ()=>{ describe('Justification', ()=>{
test('Left Justify', function() { test('Left Justify', function() {
+1 -1
View File
@@ -1,7 +1,7 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import dedent from 'dedent'; import dedent from 'dedent';
import { hbfm } from 'hbmarkedwrapper'; import { hbfm } from 'marked-hbfm';
// Marked.js adds line returns after closing tags on some default tokens. // Marked.js adds line returns after closing tags on some default tokens.
// This removes those line returns for comparison sake. // This removes those line returns for comparison sake.
+2 -1
View File
@@ -14,7 +14,8 @@
.note table tbody tr:nth-child(odd) { background : #FFFFFF; } .note table tbody tr:nth-child(odd) { background : #FFFFFF; }
/* DROP CAP */ /* DROP CAP */
.first-letter, .drop-cap p.first-letter::first-letter,
p.drop-cap::first-letter,
h1 + p::first-letter { h1 + p::first-letter {
color : black; color : black;
background-image : unset; background-image : unset;
+2 -1
View File
@@ -30,7 +30,8 @@ export default [
name : 'Tweak Drop Cap', name : 'Tweak Drop Cap',
icon : 'fas fa-sliders-h', icon : 'fas fa-sliders-h',
gen : dedent`/* Drop Cap settings */ gen : dedent`/* Drop Cap settings */
.page .first-letter, .page .drop-cap, .page p.first-letter::first-letter,
.page p.drop-cap::first-letter,
.page h1 + p::first-letter { .page h1 + p::first-letter {
font-family: SolberaImitationRemake; font-family: SolberaImitationRemake;
font-size: 3.5cm; font-size: 3.5cm;
+2 -1
View File
@@ -87,7 +87,8 @@
-moz-column-span : all; -moz-column-span : all;
& + p::first-line { font-variant : small-caps; } & + p::first-line { font-variant : small-caps; }
} }
.first-letter, .drop-cap, p.first-letter::first-letter,
p.drop-cap::first-letter,
h1 + p::first-letter { h1 + p::first-letter {
float : left; float : left;
padding-bottom : 2px; padding-bottom : 2px;
+1 -1
View File
@@ -1,4 +1,4 @@
import hbfm from 'hbmarkedwrapper'; import { hbfm } from 'marked-hbfm';
export default { export default {
createFooterFunc : function(headerSize=1){ createFooterFunc : function(headerSize=1){
+2 -1
View File
@@ -82,7 +82,8 @@
line-height : 1em; line-height : 1em;
& + p::first-line { font-variant : small-caps; } & + p::first-line { font-variant : small-caps; }
} }
.first-letter, .drop-cap, p.first-letter::first-letter,
p.drop-cap::first-letter,
h1 + p::first-letter { h1 + p::first-letter {
float : left; float : left;
padding-top : 0.3em; padding-top : 0.3em;