0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-27 16:03:07 +00:00

Compare commits

..

8 Commits

Author SHA1 Message Date
Trevor Buckner
e4efc9f653 Make autosave common/move save button into common format 2025-08-30 16:28:54 -04:00
Trevor Buckner
cea342d7f6 Moving mount/unmount over. Missing states and imports. 2025-08-20 16:31:12 -04:00
Trevor Buckner
d443ecabae Move most common "handler" functions (textChange/splitMoves/etc.) 2025-08-20 16:30:44 -04:00
Trevor Buckner
91b6b9d91b Move most state over to common BaseEditPage 2025-08-20 16:29:29 -04:00
Trevor Buckner
7ca2123506 Move Splitpane / Editor / BrewRenderer into common BasePage
(not on /edit yet. That one is a beast.)
2025-08-20 16:28:05 -04:00
Trevor Buckner
4af33f6e75 Change fetchThemeBundle to not require this passed in.
`this` doesn't work with functional components
2025-08-20 16:25:54 -04:00
Trevor Buckner
90ceb52ffc Remove unused import 2025-08-11 21:30:51 -04:00
Trevor Buckner
6f9caf0590 Refactor to use BaseEditPage for shared layout
Some Nav buttons missing on the different pages should now appear in all three pages. Unique buttons are still only on those pages for now (/share nav button only appears on the /edit page, etc.)
2025-08-10 22:16:07 -04:00
57 changed files with 1603 additions and 2808 deletions

View File

@@ -47,7 +47,9 @@ Make an changes you need to `config/docker.json` then build the image. If it doe
"naturalcrit_url" : "local.naturalcrit.com:8010", "naturalcrit_url" : "local.naturalcrit.com:8010",
"secret" : "secret", "secret" : "secret",
"web_port" : 8000, "web_port" : 8000,
"enable_v3" : true,
"mongodb_uri": "mongodb://172.17.0.2/homebrewery", "mongodb_uri": "mongodb://172.17.0.2/homebrewery",
"enable_themes" : true,
} }
``` ```
@@ -88,13 +90,6 @@ docker run --name homebrewery-mongodb -d --restart unless-stopped -v mongodata:/
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v $(pwd)/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v $(pwd)/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
``` ```
**NOTE:** If you are running from the Windows command line, this will not work as `$(pwd)` is not valid syntax. Use this command instead:
```shell
# Make sure you run this in the homebrewery directory
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v %cd%/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
```
## Updating the Image ## Updating the Image
When Homebrewery code updates, your docker container will not automatically follow the changes. To do so you will need to rebuild your homebrewery image. When Homebrewery code updates, your docker container will not automatically follow the changes. To do so you will need to rebuild your homebrewery image.
@@ -122,9 +117,3 @@ docker-compose build homebrewery
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v $(pwd)/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v $(pwd)/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
``` ```
**NOTE:** If you are running from the Windows command line, this will not work as `$(pwd)` is not valid syntax. Use this command instead:
```shell
# Make sure you run this in the homebrewery directory
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v %cd%/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
```

View File

@@ -75,9 +75,8 @@ it using the two commands:
1. `npm install` 1. `npm install`
1. `npm start` 1. `npm start`
When the Homebrewery server is started for the first time, it will modify the database to create the indexes required for better Homebrewery performance. This may take a few moments to complete for each index, dependent on how much content is in your local database - a brand new, empty database should be done in seconds. You should now be able to go to [http://localhost:8000](http://localhost:8000)
in your browser and use The Homebrewery offline.
On completion, you should be able to go to [http://localhost:8000](http://localhost:8000) in your browser and use The Homebrewery offline.
If you had any issue at all, here are some links that may be useful: If you had any issue at all, here are some links that may be useful:
- [Course](https://learn.mongodb.com/courses/m103-basic-cluster-administration) on cluster administration, useful for beginners - [Course](https://learn.mongodb.com/courses/m103-basic-cluster-administration) on cluster administration, useful for beginners
@@ -146,4 +145,3 @@ your contribution to the project, please join our [gitter chat][gitter-url].
[github-pr-docs-url]: https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request [github-pr-docs-url]: https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request
[gitter-url]: https://gitter.im/naturalcrit/Lobby [gitter-url]: https://gitter.im/naturalcrit/Lobby

View File

@@ -7,17 +7,15 @@ import LockTools from './lockTools/lockTools.jsx';
const tabGroups = ['brew', 'notifications', 'authors', 'locks']; const tabGroups = ['brew', 'notifications', 'authors', 'locks'];
const ADMIN_TAB = 'HB_adminPage_currentTab';
const Admin = ()=>{ const Admin = ()=>{
const [currentTab, setCurrentTab] = useState(''); const [currentTab, setCurrentTab] = useState('');
useEffect(()=>{ useEffect(()=>{
setCurrentTab(localStorage.getItem(ADMIN_TAB) || 'brew'); setCurrentTab(localStorage.getItem('hbAdminTab') || 'brew');
}, []); }, []);
useEffect(()=>{ useEffect(()=>{
localStorage.setItem(ADMIN_TAB, currentTab); localStorage.setItem('hbAdminTab', currentTab);
}, [currentTab]); }, [currentTab]);
return ( return (

View File

@@ -2,8 +2,7 @@ require('./splitPane.less');
const React = require('react'); const React = require('react');
const { useState, useEffect } = React; const { useState, useEffect } = React;
const PANE_WIDTH_KEY = 'HB_editor_splitWidth'; const storageKey = 'naturalcrit-pane-split';
const LIVE_SCROLL_KEY = 'HB_editor_liveScroll';
const SplitPane = (props)=>{ const SplitPane = (props)=>{
const { const {
@@ -19,9 +18,9 @@ const SplitPane = (props)=>{
const [liveScroll, setLiveScroll] = useState(false); const [liveScroll, setLiveScroll] = useState(false);
useEffect(()=>{ useEffect(()=>{
const savedPos = window.localStorage.getItem(PANE_WIDTH_KEY); const savedPos = window.localStorage.getItem(storageKey);
setDividerPos(savedPos ? limitPosition(savedPos, 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)) : window.innerWidth / 2); setDividerPos(savedPos ? limitPosition(savedPos, 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)) : window.innerWidth / 2);
setLiveScroll(window.localStorage.getItem(LIVE_SCROLL_KEY) === 'true'); setLiveScroll(window.localStorage.getItem('liveScroll') === 'true');
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
return ()=>window.removeEventListener('resize', handleResize); return ()=>window.removeEventListener('resize', handleResize);
@@ -30,13 +29,13 @@ const SplitPane = (props)=>{
const limitPosition = (x, min = 1, max = window.innerWidth - 13)=>Math.round(Math.min(max, Math.max(min, x))); const limitPosition = (x, min = 1, max = window.innerWidth - 13)=>Math.round(Math.min(max, Math.max(min, x)));
//when resizing, the divider should grow smaller if less space is given, then grow back if the space is restored, to the original position //when resizing, the divider should grow smaller if less space is given, then grow back if the space is restored, to the original position
const handleResize = ()=>setDividerPos(limitPosition(window.localStorage.getItem(PANE_WIDTH_KEY), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13))); const handleResize = ()=>setDividerPos(limitPosition(window.localStorage.getItem(storageKey), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)));
const handleUp =(e)=>{ const handleUp =(e)=>{
e.preventDefault(); e.preventDefault();
if(isDragging) { if(isDragging) {
onDragFinish(dividerPos); onDragFinish(dividerPos);
window.localStorage.setItem(PANE_WIDTH_KEY, dividerPos); window.localStorage.setItem(storageKey, dividerPos);
} }
setIsDragging(false); setIsDragging(false);
}; };
@@ -53,7 +52,7 @@ const SplitPane = (props)=>{
}; };
const liveScrollToggle = ()=>{ const liveScrollToggle = ()=>{
window.localStorage.setItem(LIVE_SCROLL_KEY, String(!liveScroll)); window.localStorage.setItem('liveScroll', String(!liveScroll));
setLiveScroll(!liveScroll); setLiveScroll(!liveScroll);
}; };

View File

@@ -24,8 +24,6 @@ const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m;
const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m; const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m;
const PAGE_HEIGHT = 1056; const PAGE_HEIGHT = 1056;
const TOOLBAR_STATE_KEY = 'HB_renderer_toolbarState';
const INITIAL_CONTENT = dedent` const INITIAL_CONTENT = dedent`
<!DOCTYPE html><html><head> <!DOCTYPE html><html><head>
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" /> <link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
@@ -42,7 +40,7 @@ const BrewPage = (props)=>{
...props ...props
}; };
const pageRef = useRef(null); const pageRef = useRef(null);
const cleanText = safeHTML(props.contents); const cleanText = safeHTML(`${props.contents}\n<div class="columnSplit"></div>\n`);
useEffect(()=>{ useEffect(()=>{
if(!pageRef.current) return; if(!pageRef.current) return;
@@ -124,7 +122,7 @@ const BrewRenderer = (props)=>{
//useEffect to store or gather toolbar state from storage //useEffect to store or gather toolbar state from storage
useEffect(()=>{ useEffect(()=>{
const toolbarState = JSON.parse(window.localStorage.getItem(TOOLBAR_STATE_KEY)); const toolbarState = JSON.parse(window.localStorage.getItem('hb_toolbarState'));
toolbarState && setDisplayOptions(toolbarState); toolbarState && setDisplayOptions(toolbarState);
}, []); }, []);
@@ -286,7 +284,7 @@ const BrewRenderer = (props)=>{
const handleDisplayOptionsChange = (newDisplayOptions)=>{ const handleDisplayOptionsChange = (newDisplayOptions)=>{
setDisplayOptions(newDisplayOptions); setDisplayOptions(newDisplayOptions);
localStorage.setItem(TOOLBAR_STATE_KEY, JSON.stringify(newDisplayOptions)); localStorage.setItem('hb_toolbarState', JSON.stringify(newDisplayOptions));
}; };
const pagesStyle = { const pagesStyle = {
@@ -295,6 +293,12 @@ const BrewRenderer = (props)=>{
rowGap : `${displayOptions.rowGap}px` rowGap : `${displayOptions.rowGap}px`
}; };
const styleObject = {};
if(global.config.deployment) {
styleObject.backgroundImage = `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='40px' width='200px'><text x='0' y='15' fill='%23fff7' font-size='20'>${global.config.deployment}</text></svg>")`;
}
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, displayOptions]);
@@ -323,9 +327,10 @@ const BrewRenderer = (props)=>{
contentDidMount={frameDidMount} contentDidMount={frameDidMount}
onClick={()=>{emitClick();}} onClick={()=>{emitClick();}}
> >
<div className='brewRenderer' <div className={`brewRenderer ${global.config.deployment && 'deployment'}`}
onKeyDown={handleControlKeys} onKeyDown={handleControlKeys}
tabIndex={-1} tabIndex={-1}
style={ styleObject }
> >
{/* Apply CSS from Style tab and render pages from Markdown tab */} {/* Apply CSS from Style tab and render pages from Markdown tab */}

View File

@@ -6,6 +6,7 @@
overflow-y : scroll; overflow-y : scroll;
will-change : transform; will-change : transform;
&:has(.facing, .flow) { padding : 60px 30px; } &:has(.facing, .flow) { padding : 60px 30px; }
&.deployment { background-color : darkred; }
:where(.pages) { :where(.pages) {
&.facing { &.facing {
display : grid; display : grid;

View File

@@ -9,8 +9,6 @@ import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anch
const MAX_ZOOM = 300; const MAX_ZOOM = 300;
const MIN_ZOOM = 10; const MIN_ZOOM = 10;
const TOOLBAR_VISIBILITY = 'HB_renderer_toolbarVisibility';
const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPages, headerState, setHeaderState })=>{ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPages, headerState, setHeaderState })=>{
const [pageNum, setPageNum] = useState(1); const [pageNum, setPageNum] = useState(1);
@@ -23,8 +21,8 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
}, [visiblePages]); }, [visiblePages]);
useEffect(()=>{ useEffect(()=>{
const Visibility = localStorage.getItem(TOOLBAR_VISIBILITY); const Visibility = localStorage.getItem('hb_toolbarVisibility');
if(Visibility) setToolsVisible(Visibility === 'true'); if (Visibility) setToolsVisible(Visibility === 'true');
}, []); }, []);
@@ -102,7 +100,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
<div className='toggleButton'> <div className='toggleButton'>
<button title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{ <button title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{
setToolsVisible(!toolsVisible); setToolsVisible(!toolsVisible);
localStorage.setItem(TOOLBAR_VISIBILITY, !toolsVisible); localStorage.setItem('hb_toolbarVisibility', !toolsVisible);
}}><i className='fas fa-glasses' /></button> }}><i className='fas fa-glasses' /></button>
<button title={`${headerState ? 'Hide' : 'Show'} Header Navigation`} onClick={()=>{setHeaderState(!headerState);}}><i className='fas fa-rectangle-list' /></button> <button title={`${headerState ? 'Hide' : 'Show'} Header Navigation`} onClick={()=>{setHeaderState(!headerState);}}><i className='fas fa-rectangle-list' /></button>
</div> </div>

View File

@@ -10,7 +10,7 @@ const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
const SnippetBar = require('./snippetbar/snippetbar.jsx'); const SnippetBar = require('./snippetbar/snippetbar.jsx');
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx'); const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
const EDITOR_THEME_KEY = 'HB_editor_theme'; const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/; const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
@@ -40,7 +40,10 @@ const Editor = createClass({
style : '' style : ''
}, },
onBrewChange : ()=>{}, onTextChange : ()=>{},
onStyleChange : ()=>{},
onMetaChange : ()=>{},
onSnipChange : ()=>{},
reportError : ()=>{}, reportError : ()=>{},
onCursorPageChange : ()=>{}, onCursorPageChange : ()=>{},
@@ -326,8 +329,8 @@ const Editor = createClass({
const currentPos = brewRenderer.scrollTop; const currentPos = brewRenderer.scrollTop;
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top; const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
const checkIfScrollComplete = ()=>{
let scrollingTimeout; let scrollingTimeout;
const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
scrollingTimeout = setTimeout(()=>{ scrollingTimeout = setTimeout(()=>{
isJumping = false; isJumping = false;
@@ -369,8 +372,8 @@ const Editor = createClass({
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top; let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true); let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
const checkIfScrollComplete = ()=>{
let scrollingTimeout; let scrollingTimeout;
const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
scrollingTimeout = setTimeout(()=>{ scrollingTimeout = setTimeout(()=>{
isJumping = false; isJumping = false;
@@ -409,6 +412,7 @@ const Editor = createClass({
//Called when there are changes to the editor's dimensions //Called when there are changes to the editor's dimensions
update : function(){ update : function(){
this.codeEditor.current?.updateSize();
const snipHeight = document.querySelector('.editor > .snippetBar').offsetHeight; const snipHeight = document.querySelector('.editor > .snippetBar').offsetHeight;
if(snipHeight !== this.state.snippetbarHeight) if(snipHeight !== this.state.snippetbarHeight)
this.setState({ snippetbarHeight: snipHeight }); this.setState({ snippetbarHeight: snipHeight });
@@ -434,7 +438,7 @@ const Editor = createClass({
language='gfm' language='gfm'
view={this.state.view} view={this.state.view}
value={this.props.brew.text} value={this.props.brew.text}
onChange={this.props.onBrewChange('text')} onChange={this.props.onTextChange}
editorTheme={this.state.editorTheme} editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} /> style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
@@ -447,7 +451,7 @@ const Editor = createClass({
language='css' language='css'
view={this.state.view} view={this.state.view}
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT} value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
onChange={this.props.onBrewChange('style')} onChange={this.props.onStyleChange}
enableFolding={true} enableFolding={true}
editorTheme={this.state.editorTheme} editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} rerenderParent={this.rerenderParent}
@@ -463,7 +467,7 @@ const Editor = createClass({
<MetadataEditor <MetadataEditor
metadata={this.props.brew} metadata={this.props.brew}
themeBundle={this.props.themeBundle} themeBundle={this.props.themeBundle}
onChange={this.props.onBrewChange('metadata')} onChange={this.props.onMetaChange}
reportError={this.props.reportError} reportError={this.props.reportError}
userThemes={this.props.userThemes}/> userThemes={this.props.userThemes}/>
</>; </>;
@@ -477,7 +481,7 @@ const Editor = createClass({
language='gfm' language='gfm'
view={this.state.view} view={this.state.view}
value={this.props.brew.snippets} value={this.props.brew.snippets}
onChange={this.props.onBrewChange('snippets')} onChange={this.props.onSnipChange}
enableFolding={true} enableFolding={true}
editorTheme={this.state.editorTheme} editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} rerenderParent={this.rerenderParent}

View File

@@ -207,6 +207,8 @@ const MetadataEditor = createClass({
}, },
renderThemeDropdown : function(){ renderThemeDropdown : function(){
if(!global.enable_themes) return;
const mergedThemes = _.merge(Themes, this.props.userThemes); const mergedThemes = _.merge(Themes, this.props.userThemes);
const listThemes = (renderer)=>{ const listThemes = (renderer)=>{
@@ -305,6 +307,8 @@ const MetadataEditor = createClass({
}, },
renderRenderOptions : function(){ renderRenderOptions : function(){
if(!global.enable_v3) return;
return <div className='field systems'> return <div className='field systems'>
<label>Renderer</label> <label>Renderer</label>
<div className='value'> <div className='value'>

View File

@@ -18,7 +18,7 @@ module.exports = {
try { try {
Boolean(new URL(value)); Boolean(new URL(value));
return null; return null;
} catch { } catch (e) {
return 'Must be a valid URL'; return 'Must be a valid URL';
} }
} }

View File

@@ -4,8 +4,6 @@ import './homebrew.less';
import React from 'react'; import React from 'react';
import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router'; import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router';
import { updateLocalStorage } from './utils/updateLocalStorage/updateLocalStorageKeys.js';
import HomePage from './pages/homePage/homePage.jsx'; import HomePage from './pages/homePage/homePage.jsx';
import EditPage from './pages/editPage/editPage.jsx'; import EditPage from './pages/editPage/editPage.jsx';
import UserPage from './pages/userPage/userPage.jsx'; import UserPage from './pages/userPage/userPage.jsx';
@@ -19,6 +17,7 @@ const WithRoute = ({ el: Element, ...rest })=>{
const params = useParams(); const params = useParams();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const queryParams = Object.fromEntries(searchParams?.entries() || []); const queryParams = Object.fromEntries(searchParams?.entries() || []);
return <Element {...rest} {...params} query={queryParams} />; return <Element {...rest} {...params} query={queryParams} />;
}; };
@@ -27,6 +26,8 @@ const Homebrew = (props)=>{
url = '', url = '',
version = '0.0.0', version = '0.0.0',
account = null, account = null,
enable_v3 = false,
enable_themes,
config, config,
brew = { brew = {
title : '', title : '',
@@ -43,22 +44,13 @@ const Homebrew = (props)=>{
global.account = account; global.account = account;
global.version = version; global.version = version;
global.enable_v3 = enable_v3;
global.enable_themes = enable_themes;
global.config = config; global.config = config;
const backgroundObject = ()=>{
if(global.config.deployment || (config.local && config.development)){
const bgText = global.config.deployment || 'Local';
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>")`
};
}
return null;
};
updateLocalStorage();
return ( return (
<Router location={url}> <Router location={url}>
<div className={`homebrew${(config.deployment || config.local) ? ' deployment' : ''}`} style={backgroundObject()}> <div className='homebrew'>
<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} />} />

View File

@@ -1,14 +1,12 @@
@import 'naturalcrit/styles/core.less'; @import 'naturalcrit/styles/core.less';
.homebrew { .homebrew {
height : 100%; height : 100%;
background-color:@steel;
&.deployment { background-color : darkred; }
.sitePage { .sitePage {
display : flex; display : flex;
flex-direction : column; flex-direction : column;
height : 100%; height : 100%;
overflow-y : hidden; overflow-y : hidden;
background-color : @steel;
.content { .content {
position : relative; position : relative;
flex : auto; flex : auto;

View File

@@ -1,21 +1,39 @@
require('./error-navitem.less'); require('./error-navitem.less');
const React = require('react'); const React = require('react');
const Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');
const createClass = require('create-react-class');
const ErrorNavItem = ({ error = '', clearError })=>{ const ErrorNavItem = createClass({
getDefaultProps : function() {
return {
error : '',
parent : null
};
},
render : function() {
const clearError = ()=>{
const state = {
error : null
};
if(this.props.parent.state.isSaving) {
state.isSaving = false;
}
this.props.parent.setState(state);
};
const error = this.props.error;
const response = error.response; const response = error.response;
const errorCode = error.code;
const status = response?.status; const status = response?.status;
const errorCode = error.code
const HBErrorCode = response?.body?.HBErrorCode; const HBErrorCode = response?.body?.HBErrorCode;
const message = response?.body?.message; const message = response?.body?.message;
let errMsg = ''; let errMsg = '';
try { try {
errMsg += `${error.toString()}\n\n`; errMsg += `${error.toString()}\n\n`;
errMsg += `\`\`\`\n${error.stack}\n`; errMsg += `\`\`\`\n${error.stack}\n`;
errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``; errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``;
console.log(errMsg); console.log(errMsg);
} catch {} } catch (e){}
if(status === 409) { if(status === 409) {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'> return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
@@ -112,15 +130,6 @@ const ErrorNavItem = ({ error = '', clearError })=>{
</Nav.item>; </Nav.item>;
} }
if(HBErrorCode === '13') {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
Server has lost connection to the database.
</div>
</Nav.item>;
}
if(errorCode === 'ECONNABORTED') { if(errorCode === 'ECONNABORTED') {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'> return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops! Oops!
@@ -142,6 +151,7 @@ const ErrorNavItem = ({ error = '', clearError })=>{
</a>. </a>.
</div> </div>
</Nav.item>; </Nav.item>;
}; }
});
module.exports = ErrorNavItem; module.exports = ErrorNavItem;

View File

@@ -39,9 +39,6 @@
flex-grow : 1; flex-grow : 1;
min-width : 300px; min-width : 300px;
} }
>.brewTitle {
cursor:auto;
}
} }
// "NaturalCrit" logo // "NaturalCrit" logo
.navLogo { .navLogo {

View File

@@ -10,40 +10,28 @@ const METAKEY = 'homebrewery-new-meta';
const NewBrew = ()=>{ const NewBrew = ()=>{
const handleFileChange = (e)=>{ const handleFileChange = (e)=>{
const file = e.target.files[0]; const file = e.target.files[0];
if(!file) return; if(file) {
const currentNew = localStorage.getItem(BREWKEY);
if(currentNew && !confirm(
`You have some text in the new brew space, if you load a file that text will be lost, are you sure you want to load the file?`
)) return;
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e)=>{ reader.onload = (e)=>{
const fileContent = e.target.result; const fileContent = e.target.result;
const newBrew = { text: fileContent, style: '' }; const newBrew = {
text : fileContent,
style : ''
};
if(fileContent.startsWith('```metadata')) { if(fileContent.startsWith('```metadata')) {
splitTextStyleAndMetadata(newBrew); splitTextStyleAndMetadata(newBrew); // Modify newBrew directly
localStorage.setItem(BREWKEY, newBrew.text); localStorage.setItem(BREWKEY, newBrew.text);
localStorage.setItem(STYLEKEY, newBrew.style); localStorage.setItem(STYLEKEY, newBrew.style);
localStorage.setItem(METAKEY, JSON.stringify( localStorage.setItem(METAKEY, JSON.stringify(_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])));
_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])
));
window.location.href = '/new'; window.location.href = '/new';
return; } else {
alert('This file is invalid, please, enter a valid file');
} }
const type = file.name.split('.').pop().toLowerCase();
alert(`This file is invalid: ${!type ? 'Missing file extension' :`.${type} files are not supported`}. Only .txt files exported from the Homebrewery are allowed.`);
console.log(file);
}; };
reader.readAsText(file); reader.readAsText(file);
}
}; };
return ( return (
<Nav.dropdown> <Nav.dropdown>
<Nav.item <Nav.item

View File

@@ -5,8 +5,8 @@ const Moment = require('moment');
const Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');
const EDIT_KEY = 'HB_nav_recentlyEdited'; const EDIT_KEY = 'homebrewery-recently-edited';
const VIEW_KEY = 'HB_nav_recentlyViewed'; const VIEW_KEY = 'homebrewery-recently-viewed';
const RecentItems = createClass({ const RecentItems = createClass({

View File

@@ -1,35 +0,0 @@
import React from 'react';
import dedent from 'dedent-tabs';
import Nav from 'naturalcrit/nav/nav.jsx';
const getShareId = (brew)=>(
brew.googleId && !brew.stubbed
? brew.googleId + brew.shareId
: brew.shareId
);
const getRedditLink = (brew)=>{
const text = dedent`
Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
**[Homebrewery Link](${global.config.baseUrl}/share/${getShareId(brew)})**`;
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(brew.title.toWellFormed())}&text=${encodeURIComponent(text)}`;
};
export default ({ brew })=>(
<Nav.dropdown>
<Nav.item color='teal' icon='fas fa-share-alt'>
share
</Nav.item>
<Nav.item color='blue' href={`/share/${getShareId(brew)}`}>
view
</Nav.item>
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${getShareId(brew)}`);}}>
copy url
</Nav.item>
<Nav.item color='blue' href={getRedditLink(brew)} newTab rel='noopener noreferrer'>
post to reddit
</Nav.item>
</Nav.dropdown>
);

View File

@@ -13,7 +13,7 @@ const AccountPage = (props)=>{
// initialize save location from local storage based on user id // initialize save location from local storage based on user id
React.useEffect(()=>{ React.useEffect(()=>{
if(!saveLocation && accountDetails.username) { if(!saveLocation && accountDetails.username) {
SAVEKEY = `HB_editor_defaultSave_${accountDetails.username}`; SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${accountDetails.username}`;
// if no SAVEKEY in local storage, default save location to Google Drive if user has Google account. // if no SAVEKEY in local storage, default save location to Google Drive if user has Google account.
let saveLocation = window.localStorage.getItem(SAVEKEY); let saveLocation = window.localStorage.getItem(SAVEKEY);
saveLocation = saveLocation ?? (accountDetails.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY'); saveLocation = saveLocation ?? (accountDetails.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');

View File

@@ -0,0 +1,309 @@
require('./editPage.less');
const React = require('react');
const _ = require('lodash');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../../navbar/navbar.jsx');
const NewBrewItem = require('../../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../../navbar/help.navitem.jsx');
const PrintNavItem = require('../../../navbar/print.navitem.jsx');
const ErrorNavItem = require('../../../navbar/error-navitem.jsx');
const AccountNavItem = require('../../../navbar/account.navitem.jsx');
const RecentNavItem = require('../../../navbar/recent.navitem.jsx').both;
const VaultNavItem = require('../../../navbar/vault.navitem.jsx');
const SplitPane = require('client/components/splitPane/splitPane.jsx');
const Editor = require('../../../editor/editor.jsx');
const BrewRenderer = require('../../../brewRenderer/brewRenderer.jsx');
const { fetchThemeBundle } = require('../../../../../shared/helpers.js');
import { useEffect, useState, useRef } from 'react';
import Markdown from 'naturalcrit/markdown.js';
const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
const METAKEY = 'homebrewery-new-meta';
const SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
const SAVE_TIMEOUT = 10000;
const BaseEditPage = (props)=>{
const [brew, setBrew] = useState(() => props.brew);
const [savedBrew, setSavedBrew] = useState(brew);
const [isSaving, setIsSaving] = useState(false);
const [lastSavedTime, setLastSavedTime] = useState(new Date());
const [saveGoogle, setSaveGoogle] = useState(() => (global.account?.googleId ? true : false));
const [welcomeText, setWelcomeText] = useState(() => props.brew?.text ?? '');
const [error, setError] = useState(undefined);
const [htmlErrors, setHTMLErrors] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle, setThemeBundle] = useState({});
const [unsavedChanges, setUnsavedChanges] = useState(false);
const [autoSaveEnabled, setAutoSaveEnabled] = useState(true);
const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(false);
const editorRef = useRef(null);
let lastSavedBrew = useRef(JSON.parse(JSON.stringify(this.propcopys.brew))); //Deep copy
const saveTimeout = useRef(null);
const unsavedChangesTimer = useRef(null);
const handleSplitMove = ()=>{
editorRef.current.update();
};
const handleEditorViewPageChange = (pageNumber)=>{
setCurrentEditorViewPageNum(pageNumber);
};
const handleEditorCursorPageChange = (pageNumber)=>{
setCurrentEditorCursorPageNum(pageNumber);
};
const handleBrewRendererPageChange = (pageNumber)=>{
setCurrentBrewRendererPageNum(pageNumber);
};
const handleTextChange = (text)=>{
//If there are HTML errors, run the validator on every change to give quick feedback
if(htmlErrors.length)
htmlErrors = Markdown.validate(text);
setHTMLErrors(htmlErrors);
setBrew((prevBrew) => ({ ...prevBrew, text }));
// TODO: ONLY ON /NEW PAGE
localStorage.setItem(BREWKEY, text);
};
const handleStyleChange = (style)=>{
setBrew((prevBrew) => ({ ...prevBrew, style }));
if(props.useLocalStorage)
localStorage.setItem(STYLEKEY, style);
};
const handleSnipChange = (snippet)=>{
//If there are HTML errors, run the validator on every change to give quick feedback
if(htmlErrors.length)
htmlErrors = Markdown.validate(text);
setHTMLErrors(htmlErrors);
setBrew((prevBrew) => ({ ...prevBrew, snippets: snippet }));
};
const handleMetaChange = (metadata, field=undefined)=>{
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme);
setBrew((prevBrew) => ({ ...prevBrew, ...metadata }));
if(props.useLocalStorage)
localStorage.setItem(METAKEY, JSON.stringify({
'renderer' : metadata.renderer,
'theme' : metadata.theme,
'lang' : metadata.lang
}));
};
const updateBrew = (newData) => {
setBrew(prevBrew => ({ //TODO: May be able to just directly use setBrew instead of a wrapper, if its safe to assume we want all the data from newData
...prevBrew, //OR: Somehow combine handleTextChange, handleStyleChange, handleMetaChange, and handleSnipChange into one function that calls this
style: newData.style,
text: newData.text,
snippets: newData.snippets
}));
};
const clearError = ()=>{
setError(null);
setIsSaving(false);
};
const save = async (immediate=false)=>{
if(isSaving) return;
if(!unsavedChanges && !immediate) return;
clearTimeout(saveTimeout.current);
const timeout = immediate ? 0 : 10000;
saveTimeout.current = setTimeout(async () => {
setIsSaving(true);
await props.performSave(brew, saveGoogle)
.catch((err)=>{
setError(err);
});
setIsSaving(false);
setLastSavedTime(new Date());
setTimeout(setWarnUnsavedChanges(true), 900000); // 15 minutes between unsaved work warnings
}, timeout);
};
const handleControlKeys = (e)=>{
if(!(e.ctrlKey || e.metaKey)) return;
const S_KEY = 83;
const P_KEY = 80;
if(e.keyCode == S_KEY) save();
if(e.keyCode == P_KEY) BrewRenderer.printCurrentBrew();
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
e.stopPropagation();
e.preventDefault();
}
};
const hasChanges =()=>{
return !_.isEqual(brew, savedBrew);
};
useEffect(() => {
props.loadBrew?.(brew, setBrew, setSaveGoogle); //Initial load from localStorage/etc.
//Load settings
setAutoSaveEnabled(JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true);
setHTMLErrors(Markdown.validate(brew.text));
document.addEventListener('keydown', handleControlKeys);
window.onbeforeunload = ()=>{
if(isSaving || unsavedChanges)
return 'You have unsaved changes!';
};
return () => {
document.removeEventListener('keydown', handleControlKeys);
window.onbeforeunload = null;
}
}, []);
useEffect(() => {
fetchThemeBundle(setError, setThemeBundle, brew.renderer, brew.theme);
}, [brew.renderer, brew.theme]);
useEffect(() => {
const hasChange = hasChanges();
if(unsavedChanges !== hasChange)
setUnsavedChanges(hasChange);
if(autoSaveEnabled) save();
}, [brew]);
const resetUnsavedChangesWarning = ()=>{
setTimeout(setWarnUnsavedChanges(false), 4000); // Display warning for 4 seconds
setTimeout(setWarnUnsavedChanges(true) , 90000); // 15 minutes between warnings
};
const toggleAutoSave = ()=>{
if(unsavedChangesTimer.current) clearTimeout(unsavedChangesTimer.current);
localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled));
setAutoSaveEnabled(!autoSaveEnabled);
setWarnUnsavedChanges(false);
};
const renderSaveButton = ()=>{
// #1 - Currently saving, show SAVING
if(isSaving)
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
if(unsavedChanges && warnUnsavedChanges){
resetUnsavedChangesWarning();
const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
Reminder...
<div className='errorContainer'>{text}</div>
</Nav.item>;
}
// #3 - Unsaved changes exist, click to save, show SAVE NOW
if(unsavedChanges)
return <Nav.item className='save' onClick={()=>save(true)} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
if(autoSaveEnabled)
return <Nav.item className='save saved'>auto-saved.</Nav.item>;
// DEFAULT - No unsaved changes, show SAVED
return <Nav.item className='save saved'>saved.</Nav.item>;
};
const renderAutoSaveButton = ()=>{
return <Nav.item onClick={toggleAutoSave}>
Autosave <i className={autosaveEnabled ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
</Nav.item>;
};
return (
<div className={`sitePage ${props.className || ''}`}>
<Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{props.brew.title}</Nav.item>
</Nav.section>
<Nav.section>
{error
? <ErrorNavItem error={error} clearError={clearError}></ErrorNavItem>
: <Nav.dropdown className='save-menu'>
{renderSaveButton()}
{renderAutoSaveButton()}
</Nav.dropdown>
}
{props.renderUniqueNav?.()}
</Nav.section>
<Nav.section>
<PrintNavItem />
<NewBrewItem />
<HelpNavItem />
<VaultNavItem />
<RecentNavItem brew={props.brew} storageKey={props.recentStorageKey} />
<AccountNavItem />
</Nav.section>
</Navbar>
<div className='content'>
<SplitPane onDragFinish={handleSplitMove}>
<Editor
ref={editorRef}
brew={brew}
onTextChange={handleTextChange}
onStyleChange={handleStyleChange}
onMetaChange={handleMetaChange}
onSnipChange={handleSnipChange}
reportError={this.errorReported}
renderer={brew.renderer}
showEditButtons={false} //FALSE FOR HOME PAGE
userThemes={props.userThemes}
themeBundle={themeBundle}
updateBrew={updateBrew}
onCursorPageChange={handleEditorCursorPageChange}
onViewPageChange={handleEditorViewPageChange}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
/>
<BrewRenderer
text={brew.text}
style={brew.style}
renderer={brew.renderer}
theme={brew.theme}
errors={htmlErrors}
lang={brew.lang}
onPageChange={handleBrewRendererPageChange}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
themeBundle={themeBundle}
allowPrint={true} // FALSE FOR HOME PAGE
/>
</SplitPane>
</div>
{props.children?.(welcomeText, brew, save)}
</div>
);
};
module.exports = BaseEditPage;

View File

@@ -7,9 +7,7 @@ const moment = require('moment');
const BrewItem = require('./brewItem/brewItem.jsx'); const BrewItem = require('./brewItem/brewItem.jsx');
const USERPAGE_SORT_DIR = 'HB_listPage_sortDir'; const USERPAGE_KEY_PREFIX = 'HOMEBREWERY-LISTPAGE';
const USERPAGE_SORT_TYPE = 'HB_listPage_sortType';
const USERPAGE_GROUP_VISIBILITY_PREFIX = 'HB_listPage_visibility_group';
const DEFAULT_SORT_TYPE = 'alpha'; const DEFAULT_SORT_TYPE = 'alpha';
const DEFAULT_SORT_DIR = 'asc'; const DEFAULT_SORT_DIR = 'asc';
@@ -52,12 +50,12 @@ const ListPage = createClass({
// LOAD FROM LOCAL STORAGE // LOAD FROM LOCAL STORAGE
if(typeof window !== 'undefined') { if(typeof window !== 'undefined') {
const newSortType = (this.state.sortType ?? (localStorage.getItem(USERPAGE_SORT_TYPE) || DEFAULT_SORT_TYPE)); const newSortType = (this.state.sortType ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`) || DEFAULT_SORT_TYPE));
const newSortDir = (this.state.sortDir ?? (localStorage.getItem(USERPAGE_SORT_DIR) || DEFAULT_SORT_DIR)); const newSortDir = (this.state.sortDir ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`) || DEFAULT_SORT_DIR));
this.updateUrl(this.state.filterString, newSortType, newSortDir); this.updateUrl(this.state.filterString, newSortType, newSortDir);
const brewCollection = this.props.brewCollection.map((brewGroup)=>{ const brewCollection = this.props.brewCollection.map((brewGroup)=>{
brewGroup.visible = (localStorage.getItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroup.class}`) ?? 'true')=='true'; brewGroup.visible = (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`) ?? 'true')=='true';
return brewGroup; return brewGroup;
}); });
@@ -75,10 +73,10 @@ const ListPage = createClass({
saveToLocalStorage : function() { saveToLocalStorage : function() {
this.state.brewCollection.map((brewGroup)=>{ this.state.brewCollection.map((brewGroup)=>{
localStorage.setItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroup.class}`, `${brewGroup.visible}`); localStorage.setItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`, `${brewGroup.visible}`);
}); });
localStorage.setItem(USERPAGE_SORT_TYPE, this.state.sortType); localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`, this.state.sortType);
localStorage.setItem(USERPAGE_SORT_DIR, this.state.sortDir); localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`, this.state.sortDir);
}, },
renderBrews : function(brews){ renderBrews : function(brews){

View File

@@ -29,7 +29,6 @@
&::before { &::before {
margin-right : 5px; margin-right : 5px;
font-family : 'Font Awesome 6 Free'; font-family : 'Font Awesome 6 Free';
font-weight : 900;
content : '\f00c'; content : '\f00c';
} }
} }

View File

@@ -1,415 +1,253 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import './editPage.less'; require('./editPage.less');
const React = require('react');
// Common imports const _ = require('lodash');
import React, { useState, useEffect, useRef } from 'react'; const createClass = require('create-react-class');
import request from '../../utils/request-middleware.js'; import {makePatches, applyPatches, stringifyPatches, parsePatches} from '@sanity/diff-match-patch';
import Markdown from 'naturalcrit/markdown.js';
import _ from 'lodash';
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
import SplitPane from 'client/components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import Nav from 'naturalcrit/nav/nav.jsx';
import Navbar from '../../navbar/navbar.jsx';
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
import AccountNavItem from '../../navbar/account.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import VaultNavItem from '../../navbar/vault.navitem.jsx';
import PrintNavItem from '../../navbar/print.navitem.jsx';
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
// Page specific imports
import { Meta } from 'vitreum/headtags';
import { md5 } from 'hash-wasm'; import { md5 } from 'hash-wasm';
import { gzipSync, strToU8 } from 'fflate'; import { gzipSync, strToU8 } from 'fflate';
import { makePatches, stringifyPatches } from '@sanity/diff-match-patch';
import ShareNavItem from '../../navbar/share.navitem.jsx'; import request from '../../utils/request-middleware.js';
import LockNotification from './lockNotification/lockNotification.jsx'; const { Meta } = require('vitreum/headtags');
const Nav = require('naturalcrit/nav/nav.jsx');
const BaseEditPage = require('../basePages/editPage/editPage.jsx');
const LockNotification = require('./lockNotification/lockNotification.jsx');
import Markdown from 'naturalcrit/markdown.js';
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js'; import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
import googleDriveIcon from '../../googleDrive.svg';
const SAVE_TIMEOUT = 10000; const googleDriveIcon = require('../../googleDrive.svg');
const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes
const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds
const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; const EditPage = createClass({
const BREWKEY = 'HB_newPage_content'; displayName : 'EditPage',
const STYLEKEY = 'HB_newPage_style'; getDefaultProps : function() {
const SNIPKEY = 'HB_newPage_snippets'; return {
const METAKEY = 'HB_newPage_meta'; brew : DEFAULT_BREW_LOAD
const useLocalStorage = false;
const neverSaved = false;
const EditPage = (props)=>{
props = {
brew : DEFAULT_BREW_LOAD,
...props
}; };
},
const [currentBrew , setCurrentBrew ] = useState(props.brew); getInitialState : function() {
const [isSaving , setIsSaving ] = useState(false); return {
const [lastSavedTime , setLastSavedTime ] = useState(new Date()); alertTrashedGoogleBrew : this.props.brew.trashed,
const [saveGoogle , setSaveGoogle ] = useState(!!props.brew.googleId); alertLoginToTransfer : false,
const [error , setError ] = useState(null); saveGoogle : this.props.brew.googleId ? true : false,
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text)); confirmGoogleTransfer : false,
const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1); error : null,
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); displayLockMessage : this.props.brew.lock || false,
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle ] = useState({});
const [unsavedChanges , setUnsavedChanges ] = useState(false);
const [alertTrashedGoogleBrew , setAlertTrashedGoogleBrew ] = useState(props.brew.trashed);
const [alertLoginToTransfer , setAlertLoginToTransfer ] = useState(false);
const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false);
const [autoSaveEnabled , setAutoSaveEnabled ] = useState(true);
const [warnUnsavedChanges , setWarnUnsavedChanges ] = useState(true);
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
const saveTimeout = useRef(null);
const warnUnsavedTimeout = useRef(null);
const trySaveRef = useRef(trySave); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew
const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges
useEffect(()=>{
const autoSavePref = JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true);
setAutoSaveEnabled(autoSavePref);
setWarnUnsavedChanges(!autoSavePref);
setHTMLErrors(Markdown.validate(currentBrew.text));
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
const handleControlKeys = (e)=>{
if(!(e.ctrlKey || e.metaKey)) return;
if(e.keyCode === 83) trySaveRef.current(true);
if(e.keyCode === 80) printCurrentBrew();
if([83, 80].includes(e.keyCode)) {
e.stopPropagation();
e.preventDefault();
}
}; };
},
document.addEventListener('keydown', handleControlKeys); componentDidMount : function(){
window.onbeforeunload = ()=>{
if(unsavedChangesRef.current)
return 'You have unsaved changes!';
};
return ()=>{
document.removeEventListener('keydown', handleControlKeys);
window.onBeforeUnload = null;
};
}, []);
useEffect(()=>{ },
trySaveRef.current = trySave;
unsavedChangesRef.current = unsavedChanges;
});
useEffect(()=>{ handleGoogleClick : function(){
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
setUnsavedChanges(hasChange);
if(autoSaveEnabled) trySave(false, hasChange);
}, [currentBrew]);
const handleSplitMove = ()=>{
editorRef.current?.update();
};
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
if(subfield == 'renderer' || subfield == 'theme')
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
//If there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
setHTMLErrors(Markdown.validate(value));
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value);
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
}));
}
};
const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({
...prevBrew,
style : newData.style,
text : newData.text,
snippets : newData.snippets
}));
const resetWarnUnsavedTimer = ()=>{
setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds
clearTimeout(warnUnsavedTimeout.current);
warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings
};
const handleGoogleClick = ()=>{
if(!global.account?.googleId) { if(!global.account?.googleId) {
setAlertLoginToTransfer(true); this.setState({
alertLoginToTransfer : true
});
return; return;
} }
this.setState((prevState)=>({
confirmGoogleTransfer : !prevState.confirmGoogleTransfer,
error : null
}));
setConfirmGoogleTransfer((prev)=>!prev); },
setError(null);
};
const closeAlerts = (e)=>{ closeAlerts : function(event){
e.stopPropagation(); //Only handle click once so alert doesn't reopen event.stopPropagation(); //Only handle click once so alert doesn't reopen
setAlertTrashedGoogleBrew(false); this.setState({
setAlertLoginToTransfer(false); alertTrashedGoogleBrew : false,
setConfirmGoogleTransfer(false); alertLoginToTransfer : false,
}; confirmGoogleTransfer : false
const toggleGoogleStorage = ()=>{
setSaveGoogle((prev)=>!prev);
setError(null);
trySave(true);
};
const trySave = (immediate = false, hasChanges = true)=>{
clearTimeout(saveTimeout.current);
if(isSaving) return;
if(!hasChanges && !immediate) return;
const newTimeout = immediate ? 0 : SAVE_TIMEOUT;
saveTimeout.current = setTimeout(async ()=>{
setIsSaving(true);
setError(null);
await save(currentBrew, saveGoogle)
.catch((err)=>{
setError(err);
}); });
setIsSaving(false); },
setLastSavedTime(new Date());
if(!autoSaveEnabled) resetWarnUnsavedTimer();
}, newTimeout);
};
const save = async (brew, saveToGoogle)=>{ toggleGoogleStorage : function(){
setHTMLErrors(Markdown.validate(brew.text)); this.setState((prevState)=>({
saveGoogle : !prevState.saveGoogle,
error : null
}), ()=>this.trySave(true));
},
await updateHistory(brew).catch(console.error); save : async function(){
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
const brewState = this.state.brew; // freeze the current state
const preSaveSnapshot = { ...brewState };
this.setState((prevState)=>({
isSaving : true,
error : null,
htmlErrors : Markdown.validate(prevState.brew.text)
}));
await updateHistory(this.state.brew).catch(console.error);
await versionHistoryGarbageCollection().catch(console.error); await versionHistoryGarbageCollection().catch(console.error);
//Prepare content to send to server //Prepare content to send to server
const brewToSave = { const brew = { ...brewState };
...brew, brew.text = brew.text.normalize('NFC');
text : brew.text.normalize('NFC'), this.savedBrew.text = this.savedBrew.text.normalize('NFC');
pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1, brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
patches : stringifyPatches(makePatches(encodeURI(lastSavedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))), brew.patches = stringifyPatches(makePatches(encodeURI(this.savedBrew.text), encodeURI(brew.text)));
hash : await md5(lastSavedBrew.current.text), brew.hash = await md5(this.savedBrew.text);
textBin : undefined, //brew.text = undefined; - Temporary parallel path
version : lastSavedBrew.current.version brew.textBin = undefined;
};
const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave))); const compressedBrew = gzipSync(strToU8(JSON.stringify(brew)));
const transfer = saveToGoogle === _.isNil(brew.googleId);
const params = transfer ? `?${saveToGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : '';
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`;
const res = await request const res = await request
.put(`/api/update/${brewToSave.editId}${params}`) .put(`/api/update/${brew.editId}${params}`)
.set('Content-Encoding', 'gzip') .set('Content-Encoding', 'gzip')
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.send(compressedBrew) .send(compressedBrew)
.catch((err)=>{ .catch((err)=>{
console.error('Error Updating Local Brew'); console.log('Error Updating Local Brew');
setError(err); this.setState({ error: err });
}); });
if(!res) return; if(!res) return;
const updatedFields = { this.savedBrew = {
googleId : res.body.googleId ?? null, ...preSaveSnapshot,
googleId : res.body.googleId ? res.body.googleId : null,
editId : res.body.editId, editId : res.body.editId,
shareId : res.body.shareId, shareId : res.body.shareId,
version : res.body.version version : res.body.version
}; };
lastSavedBrew.current = { this.setState((prevState) => ({
...brew, brew: {
...updatedFields ...prevState.brew,
}; googleId : res.body.googleId ? res.body.googleId : null,
editId : res.body.editId,
shareId : res.body.shareId,
version : res.body.version
},
isSaving : false,
}), ()=>{
this.setState({ unsavedChanges : this.hasChanges() });
});
setCurrentBrew((prevBrew)=>({ history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
...prevBrew, },
...updatedFields
}));
history.replaceState(null, null, `/edit/${res.body.editId}`); renderGoogleDriveIcon : function(){
}; return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
<img src={googleDriveIcon} className={this.state.saveGoogle ? '' : 'inactive'} alt='Google Drive icon'/>
const renderGoogleDriveIcon = ()=>( {this.state.confirmGoogleTransfer &&
<Nav.item className='googleDriveStorage' onClick={handleGoogleClick}> <div className='errorContainer' onClick={this.closeAlerts}>
<img src={googleDriveIcon} className={saveGoogle ? '' : 'inactive'} alt='Google Drive icon' /> { this.state.saveGoogle
? `Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?`
{confirmGoogleTransfer && ( : `Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?`
<div className='errorContainer' onClick={closeAlerts}> }
{saveGoogle
? 'Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?'
: 'Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?'}
<br /> <br />
<div className='confirm' onClick={toggleGoogleStorage}> Yes </div> <div className='confirm' onClick={this.toggleGoogleStorage}>
<div className='deny'> No </div> Yes
</div> </div>
)} <div className='deny'>
No
{alertLoginToTransfer && (
<div className='errorContainer' onClick={closeAlerts}>
You must be signed in to a Google account to transfer between the homebrewery and Google Drive!
<a target='_blank' rel='noopener noreferrer' href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
<div className='confirm'> Sign In </div>
</a>
<div className='deny'> Not Now </div>
</div> </div>
)}
{alertTrashedGoogleBrew && (
<div className='errorContainer' onClick={closeAlerts}>
This brew is currently in your Trash folder on Google Drive!<br />
If you want to keep it, make sure to move it before it is deleted permanently!<br />
<div className='confirm'> OK </div>
</div> </div>
)}
</Nav.item>
);
const renderSaveButton = ()=>{
// #1 - Currently saving, show SAVING
if(isSaving)
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
if(unsavedChanges && warnUnsavedChanges) {
resetWarnUnsavedTimer();
const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
const text = elapsedTime === 0
? 'Autosave is OFF.'
: `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
Reminder...
<div className='errorContainer'>{text}</div>
</Nav.item>;
} }
// #3 - Unsaved changes exist, click to save, show SAVE NOW {this.state.alertLoginToTransfer &&
if(unsavedChanges) <div className='errorContainer' onClick={this.closeAlerts}>
return <Nav.item className='save' onClick={()=>trySave(true)} color='blue' icon='fas fa-save'>save now</Nav.item>; You must be signed in to a Google account to transfer
between the homebrewery and Google Drive!
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${window.Location.href}`}>
<div className='confirm'>
Sign In
</div>
</a>
<div className='deny'>
Not Now
</div>
</div>
}
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED {this.state.alertTrashedGoogleBrew &&
if(autoSaveEnabled) <div className='errorContainer' onClick={this.closeAlerts}>
return <Nav.item className='save saved'>auto-saved</Nav.item>; This brew is currently in your Trash folder on Google Drive!<br />If you want to keep it, make sure to move it before it is deleted permanently!<br />
<div className='confirm'>
OK
</div>
</div>
}
</Nav.item>;
},
// #5 - No unsaved changes, and has never been saved, hide the button processShareId : function() {
if(neverSaved) return this.state.brew.googleId && !this.state.brew.stubbed ?
return <Nav.item className='save neverSaved'>save now</Nav.item>; this.state.brew.googleId + this.state.brew.shareId :
this.state.brew.shareId;
},
// DEFAULT - No unsaved changes, show SAVED getRedditLink : function(){
return <Nav.item className='save saved'>saved</Nav.item>; const shareLink = this.processShareId();
}; const systems = this.props.brew.systems.length > 0 ? ` [${this.props.brew.systems.join(' - ')}]` : '';
const title = `${this.props.brew.title} ${systems}`;
const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
const toggleAutoSave = ()=>{ **[Homebrewery Link](${global.config.baseUrl}/share/${shareLink})**`;
clearTimeout(warnUnsavedTimeout.current);
clearTimeout(saveTimeout.current);
localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(!autoSaveEnabled));
setAutoSaveEnabled(!autoSaveEnabled);
setWarnUnsavedChanges(autoSaveEnabled);
};
const renderAutoSaveButton = ()=>( return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`;
<Nav.item onClick={toggleAutoSave}> },
Autosave <i className={autoSaveEnabled ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
renderNavbar : function(){
const shareLink = this.processShareId();
return <>
<Nav.section>
{this.renderGoogleDriveIcon()}
<Nav.dropdown>
<Nav.item color='teal' icon='fas fa-share-alt'>
share
</Nav.item> </Nav.item>
); <Nav.item color='blue' href={`/share/${shareLink}`}>
view
const clearError = ()=>{ </Nav.item>
setError(null); <Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${shareLink}`);}}>
setIsSaving(false); copy url
}; </Nav.item>
<Nav.item color='blue' href={this.getRedditLink()} newTab={true} rel='noopener noreferrer'>
const renderNavbar = ()=>{ post to reddit
return <Navbar> </Nav.item>
<Nav.section> </Nav.dropdown>
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
</Nav.section> </Nav.section>
</>;
},
<Nav.section> render : function(){
{renderGoogleDriveIcon()} return <BaseEditPage
{error className="editPage"
? <ErrorNavItem error={error} clearError={clearError} /> errorState={this.state.error}
: <Nav.dropdown className='save-menu'> parent={this}
{renderSaveButton()} brew={this.state.brew}
{renderAutoSaveButton()} renderUniqueNav={this.renderNavbar}
</Nav.dropdown>} performSave={this.save}
<NewBrewItem /> recentStorageKey='edit'>
<PrintNavItem /> {(welcomeText, brew, save) => {
<HelpNavItem /> return <>
<VaultNavItem />
<ShareNavItem brew={currentBrew} />
<RecentNavItem brew={currentBrew} storageKey='edit' />
<AccountNavItem/>
</Nav.section>
</Navbar>;
};
return (
<div className='editPage sitePage'>
<Meta name='robots' content='noindex, nofollow' /> <Meta name='robots' content='noindex, nofollow' />
{this.props.brew.lock && <LockNotification shareId={brew.shareId} message={brew.lock.editMessage} reviewRequested={brew.lock.reviewRequested} />}
{renderNavbar()} </>
}}
{currentBrew.lock && <LockNotification shareId={currentBrew.shareId} message={currentBrew.lock.editMessage} reviewRequested={currentBrew.lock.reviewRequested}/>} </BaseEditPage>;
}
<div className='content'> });
<SplitPane onDragFinish={handleSplitMove}>
<Editor
ref={editorRef}
brew={currentBrew}
onBrewChange={handleBrewChange}
reportError={setError}
renderer={currentBrew.renderer}
userThemes={props.userThemes}
themeBundle={themeBundle}
updateBrew={updateBrew}
onCursorPageChange={setCurrentEditorCursorPageNum}
onViewPageChange={setCurrentEditorViewPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
/>
<BrewRenderer
text={currentBrew.text}
style={currentBrew.style}
renderer={currentBrew.renderer}
theme={currentBrew.theme}
themeBundle={themeBundle}
errors={HTMLErrors}
lang={currentBrew.lang}
onPageChange={setCurrentBrewRendererPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
allowPrint={true}
/>
</SplitPane>
</div>
</div>
);
};
module.exports = EditPage; module.exports = EditPage;

View File

@@ -196,12 +196,6 @@ const errorIndex = (props)=>{
**Brew ID:** ${props.brew.brewId}`, **Brew ID:** ${props.brew.brewId}`,
// Database Connection Lost
'13' : dedent`
## Database connection has been lost.
The server could not communicate with the database.`,
//account page when account is not defined //account page when account is not defined
'50' : dedent` '50' : dedent`
## You are not signed in ## You are not signed in

View File

@@ -1,224 +1,54 @@
/* eslint-disable max-lines */ require('./homePage.less');
import './homePage.less'; const React = require('react');
const createClass = require('create-react-class');
// Common imports const cx = require('classnames');
import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js'; const { Meta } = require('vitreum/headtags');
import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
import SplitPane from 'client/components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import Nav from 'naturalcrit/nav/nav.jsx'; const BaseEditPage = require('../basePages/editPage/editPage.jsx');
import Navbar from '../../navbar/navbar.jsx';
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
import AccountNavItem from '../../navbar/account.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import VaultNavItem from '../../navbar/vault.navitem.jsx';
import PrintNavItem from '../../navbar/print.navitem.jsx';
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
// Page specific imports const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
import { Meta } from 'vitreum/headtags';
const BREWKEY = 'homebrewery-new'; const HomePage = createClass({
const STYLEKEY = 'homebrewery-new-style'; displayName : 'HomePage',
const SNIPKEY = 'homebrewery-new-snippets'; getDefaultProps : function() {
const METAKEY = 'homebrewery-new-meta'; return {
brew : DEFAULT_BREW
const useLocalStorage = false;
const neverSaved = true;
const HomePage =(props)=>{
props = {
brew : DEFAULT_BREW,
ver : '0.0.0',
...props
}; };
},
const [currentBrew , setCurrentBrew] = useState(props.brew); save : function(brew){
const [error , setError] = useState(undefined); return request
const [HTMLErrors , setHTMLErrors] = useState(Markdown.validate(props.brew.text)); .post('/api')
const [currentEditorViewPageNum , setCurrentEditorViewPageNum] = useState(1); .send(brew)
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); .then((res) => {
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle] = useState({});
const [unsavedChanges , setUnsavedChanges] = useState(false);
const [isSaving , setIsSaving] = useState(false);
const [autoSaveEnabled , setAutoSaveEnable] = useState(false);
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
useEffect(()=>{
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
const handleControlKeys = (e)=>{
if(!(e.ctrlKey || e.metaKey)) return;
if(e.keyCode === 83) trySaveRef.current(true);
if(e.keyCode === 80) printCurrentBrew();
if([83, 80].includes(e.keyCode)) {
e.stopPropagation();
e.preventDefault();
}
};
document.addEventListener('keydown', handleControlKeys);
return ()=>{
document.removeEventListener('keydown', handleControlKeys);
};
}, []);
const save = ()=>{
request.post('/api')
.send(currentBrew)
.end((err, res)=>{
if(err) {
setError(err);
return;
}
const saved = res.body; const saved = res.body;
window.location = `/edit/${saved.editId}`; window.location = `/edit/${saved.editId}`;
}); });
}; },
useEffect(()=>{ render : function(){
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current); return <BaseEditPage
setUnsavedChanges(hasChange); {...this.props}
className="homePage"
if(autoSaveEnabled) trySave(false, hasChange); parent={this}
}, [currentBrew]); performSave={this.save}>
{(welcomeText, brew, save) => {
const handleSplitMove = ()=>{ return <>
editorRef.current.update();
};
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
if(subfield == 'renderer' || subfield == 'theme')
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
//If there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
setHTMLErrors(Markdown.validate(value));
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value);
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
}));
}
};
const renderSaveButton = ()=>{
// #1 - Currently saving, show SAVING
if(isSaving)
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
// if(unsavedChanges && warnUnsavedChanges) {
// resetWarnUnsavedTimer();
// const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
// const text = elapsedTime === 0
// ? 'Autosave is OFF.'
// : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
// return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
// Reminder...
// <div className='errorContainer'>{text}</div>
// </Nav.item>;
// }
// #3 - Unsaved changes exist, click to save, show SAVE NOW
if(unsavedChanges)
return <Nav.item className='save' onClick={save} color='blue' icon='fas fa-save'>save now</Nav.item>;
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
if(autoSaveEnabled)
return <Nav.item className='save saved'>auto-saved</Nav.item>;
// #5 - No unsaved changes, and has never been saved, hide the button
if(neverSaved)
return <Nav.item className='save neverSaved'>save now</Nav.item>;
// DEFAULT - No unsaved changes, show SAVED
return <Nav.item className='save saved'>saved</Nav.item>;
};
const clearError = ()=>{
setError(null);
setIsSaving(false);
};
const renderNavbar = ()=>{
return <Navbar ver={props.ver}>
<Nav.section>
{error
? <ErrorNavItem error={error} clearError={clearError} />
: renderSaveButton()}
<NewBrewItem />
<PrintNavItem />
<HelpNavItem />
<VaultNavItem />
<RecentNavItem />
<AccountNavItem />
</Nav.section>
</Navbar>;
};
return (
<div className='homePage sitePage'>
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' /> <Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
{renderNavbar()} <div className={cx('floatingSaveButton', { show: welcomeText != brew.text })} onClick={save}>
<div className='content'>
<SplitPane onDragFinish={handleSplitMove}>
<Editor
ref={editorRef}
brew={currentBrew}
onBrewChange={handleBrewChange}
renderer={currentBrew.renderer}
showEditButtons={false}
themeBundle={themeBundle}
onCursorPageChange={setCurrentEditorCursorPageNum}
onViewPageChange={setCurrentEditorViewPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
/>
<BrewRenderer
text={currentBrew.text}
style={currentBrew.style}
renderer={currentBrew.renderer}
onPageChange={setCurrentBrewRendererPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
themeBundle={themeBundle}
/>
</SplitPane>
</div>
<div className={`floatingSaveButton${unsavedChanges ? ' show' : ''}`} onClick={save}>
Save current <i className='fas fa-save' /> Save current <i className='fas fa-save' />
</div> </div>
<a href='/new' className='floatingNewButton'> <a href='/new' className='floatingNewButton'>
Create your own <i className='fas fa-magic' /> Create your own <i className='fas fa-magic' />
</a> </a>
</div> </>
); }}
}; </BaseEditPage>
}
});
module.exports = HomePage; module.exports = HomePage;

View File

@@ -34,13 +34,7 @@
} }
.navItem.save { .navItem.save {
.fadeInRight();
.transition(opacity);
background-color : @orange; background-color : @orange;
&:hover { background-color : @green; } &:hover { background-color : @green; }
&.neverSaved {
.fadeOutRight();
opacity: 0;
}
} }
} }

View File

@@ -1,86 +1,31 @@
/* eslint-disable max-lines */ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
import './newPage.less'; require('./newPage.less');
const React = require('react');
// Common imports const createClass = require('create-react-class');
import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js';
import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; const Nav = require('naturalcrit/nav/nav.jsx');
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
import SplitPane from 'client/components/splitPane/splitPane.jsx'; const BaseEditPage = require('../basePages/editPage/editPage.jsx');
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import Nav from 'naturalcrit/nav/nav.jsx'; const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
import Navbar from '../../navbar/navbar.jsx';
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
import AccountNavItem from '../../navbar/account.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import VaultNavItem from '../../navbar/vault.navitem.jsx';
import PrintNavItem from '../../navbar/print.navitem.jsx';
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
// Page specific imports const BREWKEY = 'homebrewery-new';
import { Meta } from 'vitreum/headtags'; const STYLEKEY = 'homebrewery-new-style';
const METAKEY = 'homebrewery-new-meta';
const SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
const BREWKEY = 'HB_newPage_content'; const NewPage = createClass({
const STYLEKEY = 'HB_newPage_style'; displayName : 'NewPage',
const METAKEY = 'HB_newPage_metadata'; getDefaultProps : function() {
const SNIPKEY = 'HB_newPage_snippets'; return {
const SAVEKEYPREFIX = 'HB_editor_defaultSave_'; brew : DEFAULT_BREW
const useLocalStorage = true;
const neverSaved = true;
const NewPage = (props)=>{
props = {
brew : DEFAULT_BREW,
...props
}; };
},
const [currentBrew , setCurrentBrew ] = useState(props.brew); loadBrew : function(brew, setBrew, setSaveGoogle) {
const [isSaving , setIsSaving ] = useState(false);
const [saveGoogle , setSaveGoogle ] = useState(global.account?.googleId ? true : false);
const [error , setError ] = useState(null);
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle ] = useState({});
const [unsavedChanges , setUnsavedChanges ] = useState(false);
const [autoSaveEnabled , setAutoSaveEnabled ] = useState(false);
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
useEffect(()=>{
loadBrew();
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
const handleControlKeys = (e)=>{
if(!(e.ctrlKey || e.metaKey)) return;
if(e.keyCode === 83) trySaveRef.current(true);
if(e.keyCode === 80) printCurrentBrew();
if([83, 80].includes(e.keyCode)) {
e.stopPropagation();
e.preventDefault();
}
};
document.addEventListener('keydown', handleControlKeys);
return ()=>{
document.removeEventListener('keydown', handleControlKeys);
};
}, []);
const loadBrew = ()=>{
const brew = { ...currentBrew };
if(!brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser if(!brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser
//TODO: Move localstorage handling to BaseEditPage?
const brewStorage = localStorage.getItem(BREWKEY); const brewStorage = localStorage.getItem(BREWKEY);
const styleStorage = localStorage.getItem(STYLEKEY); const styleStorage = localStorage.getItem(STYLEKEY);
const metaStorage = JSON.parse(localStorage.getItem(METAKEY)); const metaStorage = JSON.parse(localStorage.getItem(METAKEY));
@@ -92,179 +37,44 @@ const NewPage = (props)=>{
brew.lang = metaStorage?.lang ?? brew.lang; brew.lang = metaStorage?.lang ?? brew.lang;
} }
const SAVEKEY = `${SAVEKEYPREFIX}${global.account?.username}`;
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY'; const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
setCurrentBrew(brew); setBrew(brew);
lastSavedBrew.current = brew; setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle);
setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && saveGoogle);
localStorage.setItem(BREWKEY, brew.text); localStorage.setItem(BREWKEY, brew.text);
if(brew.style) if(brew.style)
localStorage.setItem(STYLEKEY, brew.style); localStorage.setItem(STYLEKEY, brew.style);
localStorage.setItem(METAKEY, JSON.stringify({ renderer: brew.renderer, theme: brew.theme, lang: brew.lang })); localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
if(window.location.pathname !== '/new') if(window.location.pathname != '/new') {
window.history.replaceState({}, window.location.title, '/new/'); window.history.replaceState({}, window.location.title, '/new/');
};
useEffect(()=>{
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
setUnsavedChanges(hasChange);
if(autoSaveEnabled) trySave(false, hasChange);
}, [currentBrew]);
const handleSplitMove = ()=>{
editorRef.current.update();
};
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
if(subfield == 'renderer' || subfield == 'theme')
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
//If there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
setHTMLErrors(Markdown.validate(value));
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value);
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
}));
} }
}; },
const save = async ()=>{ save : async function(brew, saveGoogle){
setIsSaving(true); brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
return request
const updatedBrew = { ...currentBrew };
splitTextStyleAndMetadata(updatedBrew);
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm;
updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1;
const res = await request
.post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`) .post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`)
.send(updatedBrew) .send(brew)
.catch((err)=>{ .then((res) => {
setIsSaving(false); //TODO: Move localstorage handling to BaseEditPage?
setError(err);
});
setIsSaving(false);
if(!res) return;
const savedBrew = res.body;
localStorage.removeItem(BREWKEY); localStorage.removeItem(BREWKEY);
localStorage.removeItem(STYLEKEY); localStorage.removeItem(STYLEKEY);
localStorage.removeItem(METAKEY); localStorage.removeItem(METAKEY);
window.location = `/edit/${savedBrew.editId}`; const saved = res.body;
}; window.location = `/edit/${saved.editId}`;
});
},
const renderSaveButton = ()=>{ render : function(){
// #1 - Currently saving, show SAVING return <BaseEditPage
if(isSaving) {...this.props}
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>; className="newPage"
parent={this}
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING performSave={this.save}
// if(unsavedChanges && warnUnsavedChanges) { loadBrew={this.loadBrew}>
// resetWarnUnsavedTimer(); </BaseEditPage>;
// const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); }
// const text = elapsedTime === 0 });
// ? 'Autosave is OFF.'
// : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
// return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
// Reminder...
// <div className='errorContainer'>{text}</div>
// </Nav.item>;
// }
// #3 - Unsaved changes exist, click to save, show SAVE NOW
if(unsavedChanges)
return <Nav.item className='save' onClick={save} color='blue' icon='fas fa-save'>save now</Nav.item>;
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
if(autoSaveEnabled)
return <Nav.item className='save saved'>auto-saved</Nav.item>;
// #5 - No unsaved changes, and has never been saved, hide the button
if(neverSaved)
return <Nav.item className='save neverSaved'>save now</Nav.item>;
// DEFAULT - No unsaved changes, show SAVED
return <Nav.item className='save saved'>saved</Nav.item>;
};
const clearError = ()=>{
setError(null);
setIsSaving(false);
};
const renderNavbar = ()=>(
<Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
</Nav.section>
<Nav.section>
{error
? <ErrorNavItem error={error} clearError={clearError} />
: renderSaveButton()}
<NewBrewItem />
<PrintNavItem />
<HelpNavItem />
<VaultNavItem />
<RecentNavItem />
<AccountNavItem />
</Nav.section>
</Navbar>
);
return (
<div className='newPage sitePage'>
{renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={handleSplitMove}>
<Editor
ref={editorRef}
brew={currentBrew}
onBrewChange={handleBrewChange}
renderer={currentBrew.renderer}
userThemes={props.userThemes}
themeBundle={themeBundle}
onCursorPageChange={setCurrentEditorCursorPageNum}
onViewPageChange={setCurrentEditorViewPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
/>
<BrewRenderer
text={currentBrew.text}
style={currentBrew.style}
renderer={currentBrew.renderer}
theme={currentBrew.theme}
themeBundle={themeBundle}
errors={HTMLErrors}
lang={currentBrew.lang}
onPageChange={setCurrentBrewRendererPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
allowPrint={true}
/>
</SplitPane>
</div>
</div>
);
};
module.exports = NewPage; module.exports = NewPage;

View File

@@ -1,12 +1,6 @@
.newPage { .newPage {
.navItem.save { .navItem.save {
.fadeInRight();
.transition(opacity);
background-color : @orange; background-color : @orange;
&:hover { background-color : @green; } &:hover { background-color : @green; }
&.neverSaved {
.fadeOutRight();
opacity: 0;
}
} }
} }

View File

@@ -17,11 +17,15 @@ const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpe
const SharePage = (props)=>{ const SharePage = (props)=>{
const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props; const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props;
const [themeBundle, setThemeBundle] = useState({}); const [state, setState] = useState({
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); themeBundle : {},
currentBrewRendererPageNum : 1,
});
const handleBrewRendererPageChange = useCallback((pageNumber)=>{ const handleBrewRendererPageChange = useCallback((pageNumber)=>{
setCurrentBrewRendererPageNum(pageNumber); setState((prevState)=>({
currentBrewRendererPageNum : pageNumber,
...prevState }));
}, []); }, []);
const handleControlKeys = (e)=>{ const handleControlKeys = (e)=>{
@@ -36,7 +40,11 @@ const SharePage = (props)=>{
useEffect(()=>{ useEffect(()=>{
document.addEventListener('keydown', handleControlKeys); document.addEventListener('keydown', handleControlKeys);
fetchThemeBundle(undefined, setThemeBundle, brew.renderer, brew.theme); fetchThemeBundle(
{ setState },
brew.renderer,
brew.theme
);
return ()=>{ return ()=>{
document.removeEventListener('keydown', handleControlKeys); document.removeEventListener('keydown', handleControlKeys);
@@ -106,9 +114,9 @@ const SharePage = (props)=>{
lang={brew.lang} lang={brew.lang}
renderer={brew.renderer} renderer={brew.renderer}
theme={brew.theme} theme={brew.theme}
themeBundle={themeBundle} themeBundle={state.themeBundle}
onPageChange={handleBrewRendererPageChange} onPageChange={handleBrewRendererPageChange}
currentBrewRendererPageNum={currentBrewRendererPageNum} currentBrewRendererPageNum={state.currentBrewRendererPageNum}
allowPrint={true} allowPrint={true}
/> />
</div> </div>

View File

@@ -39,14 +39,10 @@ const UserPage = (props)=>{
}] : []) }] : [])
]; ];
const clearError = ()=>{
setError(null);
};
const navItems = ( const navItems = (
<Navbar> <Navbar>
<Nav.section> <Nav.section>
{error && (<ErrorNavItem error={error} clearError={clearError}></ErrorNavItem>)} {error && (<ErrorNavItem error={error} parent={null}></ErrorNavItem>)}
<NewBrew /> <NewBrew />
<HelpNavItem /> <HelpNavItem />
<VaultNavitem /> <VaultNavitem />

View File

@@ -1,16 +1,14 @@
.vaultPage { .vaultPage {
height : 100%; height : 100%;
overflow-y : hidden; overflow-y : hidden;
background-color : #2C3E50;
*:not(input) { user-select : none; } *:not(input) { user-select : none; }
.form {
background:white;
}
:where(.content .dataGroup) { :where(.content .dataGroup) {
width : 100%; width : 100%;
height : 100%; height : 100%;
background : white;
&.form .brewLookup { &.form .brewLookup {
position : relative; position : relative;
@@ -173,6 +171,7 @@
max-height : 100%; max-height : 100%;
padding : 70px 50px; padding : 70px 50px;
overflow-y : scroll; overflow-y : scroll;
background-color : #2C3E50;
container-type : inline-size; container-type : inline-size;
h3 { font-size : 25px; } h3 { font-size : 25px; }

View File

@@ -1,74 +0,0 @@
import requestMiddleware from './request-middleware';
jest.mock('superagent');
import request from 'superagent';
describe('request-middleware', ()=>{
let version;
let setFn;
let testFn;
beforeEach(()=>{
jest.resetAllMocks();
version = global.version;
global.version = '999';
setFn = jest.fn();
testFn = jest.fn(()=>{ return { set: setFn }; });
});
afterEach(()=>{
global.version = version;
});
it('should add header to get', ()=>{
// Ensure tests functions have been reset
expect(testFn).not.toHaveBeenCalled();
expect(setFn).not.toHaveBeenCalled();
request.get = testFn;
requestMiddleware.get('path');
expect(testFn).toHaveBeenCalledWith('path');
expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999');
});
it('should add header to put', ()=>{
expect(testFn).not.toHaveBeenCalled();
expect(setFn).not.toHaveBeenCalled();
request.put = testFn;
requestMiddleware.put('path');
expect(testFn).toHaveBeenCalledWith('path');
expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999');
});
it('should add header to post', ()=>{
expect(testFn).not.toHaveBeenCalled();
expect(setFn).not.toHaveBeenCalled();
request.post = testFn;
requestMiddleware.post('path');
expect(testFn).toHaveBeenCalledWith('path');
expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999');
});
it('should add header to delete', ()=>{
expect(testFn).not.toHaveBeenCalled();
expect(setFn).not.toHaveBeenCalled();
request.delete = testFn;
requestMiddleware.delete('path');
expect(testFn).toHaveBeenCalledWith('path');
expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999');
});
});

View File

@@ -1,35 +0,0 @@
const getLocalStorageMap = function(){
const localStorageMap = {
'AUTOSAVE_ON' : 'HB_editor_autoSaveOn',
'HOMEBREWERY-EDITOR-THEME' : 'HB_editor_theme',
'liveScroll' : 'HB_editor_liveScroll',
'naturalcrit-pane-split' : 'HB_editor_splitWidth',
'HOMEBREWERY-LISTPAGE-SORTDIR' : 'HB_listPage_sortDir',
'HOMEBREWERY-LISTPAGE-SORTTYPE' : 'HB_listPage_sortType',
'HOMEBREWERY-LISTPAGE-VISIBILITY-published' : 'HB_listPage_visibility_group_published',
'HOMEBREWERY-LISTPAGE-VISIBILITY-unpublished' : 'HB_listPage_visibility_group_unpublished',
'hbAdminTab' : 'HB_adminPage_currentTab',
'homebrewery-new' : 'HB_newPage_content',
'homebrewery-new-meta' : 'HB_newPage_metadata',
'homebrewery-new-style' : 'HB_newPage_style',
'homebrewery-recently-edited' : 'HB_nav_recentlyEdited',
'homebrewery-recently-viewed' : 'HB_nav_recentlyViewed',
'hb_toolbarState' : 'HB_renderer_toolbarState',
'hb_toolbarVisibility' : 'HB_renderer_toolbarVisibility'
};
if(global?.account?.username){
const username = global.account.username;
localStorageMap[`HOMEBREWERY-DEFAULT-SAVE-LOCATION-${username}`] = `HB_editor_defaultSave_${username}`;
}
return localStorageMap;
};
export default getLocalStorageMap;

View File

@@ -1,22 +0,0 @@
import getLocalStorageMap from './localStorageKeyMap.js';
const updateLocalStorage = function(){
// Return if no window and thus no local storage
if(typeof window === 'undefined') return;
const localStorageKeyMap = getLocalStorageMap();
const storage = window.localStorage;
Object.keys(localStorageKeyMap).forEach((key)=>{
if(storage[key]){
if(!storage[localStorageKeyMap[key]]){
const data = storage.getItem(key);
storage.setItem(localStorageKeyMap[key], data);
};
storage.removeItem(key);
}
});
};
export { updateLocalStorage };

View File

@@ -1,9 +1,10 @@
{ {
"development": true,
"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",
"web_port" : 8000, "web_port" : 8000,
"enable_v3" : true,
"enable_themes" : true,
"local_environments" : ["docker", "local"], "local_environments" : ["docker", "local"],
"publicUrl" : "https://homebrewery.naturalcrit.com", "publicUrl" : "https://homebrewery.naturalcrit.com",
"hb_images" : null, "hb_images" : null,

1365
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -83,19 +83,19 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.28.4", "@babel/core": "^7.27.1",
"@babel/plugin-transform-runtime": "^7.28.3", "@babel/plugin-transform-runtime": "^7.28.0",
"@babel/preset-env": "^7.28.3", "@babel/preset-env": "^7.28.0",
"@babel/preset-react": "^7.27.1", "@babel/preset-react": "^7.27.1",
"@babel/runtime": "^7.28.4", "@babel/runtime": "^7.27.6",
"@dmsnell/diff-match-patch": "^1.1.0", "@dmsnell/diff-match-patch": "^1.1.0",
"@googleapis/drive": "^18.0.0", "@googleapis/drive": "^13.0.1",
"@sanity/diff-match-patch": "^3.2.0", "@sanity/diff-match-patch": "^3.2.0",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"codemirror": "^5.65.6", "codemirror": "^5.65.6",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"core-js": "^3.46.0", "core-js": "^3.44.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"create-react-class": "^15.7.0", "create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.3", "dedent-tabs": "^0.10.3",
@@ -104,14 +104,14 @@
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-static-gzip": "3.0.0", "express-static-gzip": "3.0.0",
"fflate": "^0.8.2", "fflate": "^0.8.2",
"fs-extra": "11.3.2", "fs-extra": "11.3.0",
"hash-wasm": "^4.12.0", "hash-wasm": "^4.12.0",
"idb-keyval": "^6.2.2", "idb-keyval": "^6.2.2",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jwt-simple": "^0.5.6", "jwt-simple": "^0.5.6",
"less": "^3.13.1", "less": "^3.13.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "16.4.1", "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-emoji": "^2.0.1", "marked-emoji": "^2.0.1",
@@ -119,16 +119,16 @@
"marked-gfm-heading-id": "^4.1.2", "marked-gfm-heading-id": "^4.1.2",
"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.3",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1", "moment": "^2.30.1",
"mongoose": "^8.19.1", "mongoose": "^8.16.3",
"nanoid": "5.1.6", "nanoid": "5.1.5",
"nconf": "^0.13.0", "nconf": "^0.13.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-frame-component": "^4.1.3", "react-frame-component": "^4.1.3",
"react-router": "^7.9.4", "react-router": "^7.6.3",
"romans": "^3.1.0", "romans": "^3.1.0",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"superagent": "^10.2.1", "superagent": "^10.2.1",
@@ -136,19 +136,19 @@
"written-number": "^0.11.1" "written-number": "^0.11.1"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/stylelint-plugin": "^4.0.0", "@stylistic/stylelint-plugin": "^3.1.3",
"babel-plugin-transform-import-meta": "^2.3.3", "babel-plugin-transform-import-meta": "^2.3.3",
"eslint": "^9.37.0", "eslint": "^9.31.0",
"eslint-plugin-jest": "^29.0.1", "eslint-plugin-jest": "^29.0.1",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"globals": "^16.4.0", "globals": "^16.3.0",
"jest": "^30.2.0", "jest": "^30.0.5",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",
"stylelint": "^16.25.0", "stylelint": "^16.22.0",
"stylelint-config-recess-order": "^7.3.0", "stylelint-config-recess-order": "^7.1.0",
"stylelint-config-recommended": "^17.0.0", "stylelint-config-recommended": "^16.0.0",
"supertest": "^7.1.4" "supertest": "^7.1.4"
} }
} }

View File

@@ -35,7 +35,6 @@ 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 dbCheck from './middleware/dbCheck.js';
const sanitizeBrew = (brew, accessType)=>{ const sanitizeBrew = (brew, accessType)=>{
@@ -275,7 +274,7 @@ app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{
app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);}); app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);});
//User Page //User Page
app.get('/user/:username', dbCheck, async (req, res, next)=>{ app.get('/user/:username', async (req, res, next)=>{
const ownAccount = req.account && (req.account.username == req.params.username); const ownAccount = req.account && (req.account.username == req.params.username);
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,
@@ -347,7 +346,7 @@ app.get('/user/:username', dbCheck, async (req, res, next)=>{
}); });
//Change author name on brews //Change author name on brews
app.put('/api/user/rename', dbCheck, async (req, res)=>{ app.put('/api/user/rename', async (req, res)=>{
const { username, newUsername } = req.body; const { username, newUsername } = req.body;
const ownAccount = req.account && (req.account.username == newUsername); const ownAccount = req.account && (req.account.username == newUsername);
@@ -433,7 +432,7 @@ app.get('/new', asyncHandler(async(req, res, next)=>{
})); }));
//Share Page //Share Page
app.get('/share/:id', dbCheck, asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
const { brew } = req; const { brew } = req;
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,
title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`, title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`,
@@ -460,7 +459,7 @@ app.get('/share/:id', dbCheck, asyncHandler(getBrew('share')), asyncHandler(asyn
})); }));
//Account Page //Account Page
app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{ app.get('/account', asyncHandler(async (req, res, next)=>{
const data = {}; const data = {};
data.title = 'Account Information Page'; data.title = 'Account Information Page';
@@ -488,8 +487,8 @@ app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{
const query = { authors: req.account.username, googleId: { $exists: false } }; const query = { authors: req.account.username, googleId: { $exists: false } };
const mongoCount = await HomebrewModel.countDocuments(query) const mongoCount = await HomebrewModel.countDocuments(query)
.catch((err)=>{ .catch((err)=>{
mongoCount = 0;
console.log(err); console.log(err);
return 0;
}); });
data.accountDetails = { data.accountDetails = {
@@ -563,6 +562,8 @@ const renderPage = async (req, res)=>{
brews : req.brews, brews : req.brews,
googleBrews : req.googleBrews, googleBrews : req.googleBrews,
account : req.account, account : req.account,
enable_v3 : config.get('enable_v3'),
enable_themes : config.get('enable_themes'),
config : configuration, config : configuration,
ogMeta : req.ogMeta, ogMeta : req.ogMeta,
userThemes : req.userThemes userThemes : req.userThemes

View File

@@ -22,24 +22,12 @@ const handleConnectionError = (error)=>{
} }
}; };
const addListeners = (conn)=>{
conn.connection.on('disconnecting', ()=>{console.log('Mongo disconnecting...');});
conn.connection.on('disconnected', ()=>{console.log('Mongo disconnected!');});
conn.connection.on('connecting', ()=>{console.log('Mongo connecting...');});
conn.connection.on('connected', ()=>{console.log('Mongo connected!');});
return conn;
};
const disconnect = async ()=>{ const disconnect = async ()=>{
return await Mongoose.disconnect(); return await Mongoose.disconnect();
}; };
const connect = async (config)=>{ const connect = async (config)=>{
return await Mongoose.connect(getMongoDBURL(config), { return await Mongoose.connect(getMongoDBURL(config), { retryWrites: false })
retryWrites : false,
autoIndex : (config.get('local_environments').includes(config.get('node_env')))
})
.then(addListeners(Mongoose))
.catch((error)=>handleConnectionError(error)); .catch((error)=>handleConnectionError(error));
}; };
@@ -47,4 +35,3 @@ export default {
connect, connect,
disconnect disconnect
}; };

View File

@@ -8,12 +8,11 @@ import Markdown from '../shared/naturalcrit/markdown.js';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import asyncHandler from 'express-async-handler'; import asyncHandler from 'express-async-handler';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { makePatches, applyPatches, stringifyPatches, parsePatch } from '@sanity/diff-match-patch'; import {makePatches, applyPatches, stringifyPatches, parsePatch} from '@sanity/diff-match-patch';
import { md5 } from 'hash-wasm'; import { md5 } from 'hash-wasm';
import { splitTextStyleAndMetadata, import { splitTextStyleAndMetadata,
brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js'; brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js';
import checkClientVersion from './middleware/check-client-version.js'; import checkClientVersion from './middleware/check-client-version.js';
import dbCheck from './middleware/dbCheck.js';
const router = express.Router(); const router = express.Router();
@@ -378,7 +377,7 @@ const api = {
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error. // Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[0]); const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[0]);
if(patchedResult != brewFromClient.text) if(patchedResult != brewFromClient.text)
throw ('Patches did not apply cleanly, text mismatch detected'); throw("Patches did not apply cleanly, text mismatch detected");
// brew.text = applyPatches(patches, brewFromServer.text)[0]; // brew.text = applyPatches(patches, brewFromServer.text)[0];
} catch (err) { } catch (err) {
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`); //debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
@@ -481,7 +480,6 @@ const api = {
await HomebrewModel.deleteOne({ editId: id }); await HomebrewModel.deleteOne({ editId: id });
return next(); return next();
} }
throw(err);
} }
let brew = req.brew; let brew = req.brew;
@@ -532,8 +530,6 @@ const api = {
} }
}; };
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));
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));

View File

@@ -7,29 +7,29 @@ import zlib from 'zlib';
const HomebrewSchema = mongoose.Schema({ const HomebrewSchema = mongoose.Schema({
shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } }, shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
editId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } }, editId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
googleId : { type: String, index: true }, googleId : { type: String },
title : { type: String, default: '', index: true }, title : { type: String, default: '' },
text : { type: String, default: '' }, text : { type: String, default: '' },
textBin : { type: Buffer }, textBin : { type: Buffer },
pageCount : { type: Number, default: 1, index: true }, pageCount : { type: Number, default: 1 },
description : { type: String, default: '' }, description : { type: String, default: '' },
tags : { type: [String], index: true }, tags : [String],
systems : [String], systems : [String],
lang : { type: String, default: 'en', index: true }, lang : { type: String, default: 'en' },
renderer : { type: String, default: '', index: true }, renderer : { type: String, default: '' },
authors : { type: [String], index: true }, authors : [String],
invitedAuthors : [String], invitedAuthors : [String],
published : { type: Boolean, default: false, index: true }, published : { type: Boolean, default: false },
thumbnail : { type: String, default: '', index: true }, thumbnail : { type: String, default: '' },
createdAt : { type: Date, default: Date.now, index: true }, createdAt : { type: Date, default: Date.now },
updatedAt : { type: Date, default: Date.now, index: true }, updatedAt : { type: Date, default: Date.now },
lastViewed : { type: Date, default: Date.now, index: true }, lastViewed : { type: Date, default: Date.now },
views : { type: Number, default: 0 }, views : { type: Number, default: 0 },
version : { type: Number, default: 1, index: true }, version : { type: Number, default: 1 },
lock : { type: Object, index: true } lock : { type: Object }
}, { versionKey: false }); }, { versionKey: false });
HomebrewSchema.statics.increaseView = async function(query) { HomebrewSchema.statics.increaseView = async function(query) {
@@ -43,8 +43,6 @@ HomebrewSchema.statics.increaseView = async function(query) {
return brew; return brew;
}; };
// STATIC FUNCTIONS
HomebrewSchema.statics.get = async function(query, fields=null){ HomebrewSchema.statics.get = async function(query, fields=null){
const brew = await Homebrew.findOne(query, fields).orFail() const brew = await Homebrew.findOne(query, fields).orFail()
.catch((error)=>{throw 'Can not find brew';}); .catch((error)=>{throw 'Can not find brew';});
@@ -65,15 +63,6 @@ HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, f
return brews; return brews;
}; };
// INDEXES
HomebrewSchema.index({ updatedAt: -1, lastViewed: -1 });
HomebrewSchema.index({ published: 1, title: 'text' });
HomebrewSchema.index({ lock: 1, sparse: true });
HomebrewSchema.path('lock.reviewRequested').index({ sparse: true });
const Homebrew = mongoose.model('Homebrew', HomebrewSchema); const Homebrew = mongoose.model('Homebrew', HomebrewSchema);
export { export {

View File

@@ -1,15 +0,0 @@
import mongoose from 'mongoose';
import config from '../config.js';
export default (req, res, next)=>{
// Bypass DB checks during testing
if(config.get('node_env') == 'test') return next();
if(mongoose.connection.readyState == 1) return next();
throw {
HBErrorCode : '13',
name : 'Database Connection Error',
message : 'Unable to connect to database',
status : mongoose.connection.readyState
};
};

View File

@@ -1,28 +0,0 @@
import mongoose from 'mongoose';
import dbCheck from './dbCheck.js';
import config from '../config.js';
describe('dbCheck middleware', ()=>{
const next = jest.fn();
afterEach(()=>jest.clearAllMocks());
it('should skip check in test mode', ()=>{
config.get = jest.fn(()=>'test');
expect(()=>dbCheck({}, {}, next)).not.toThrow();
expect(next).toHaveBeenCalled();
});
it('should call next if readyState == 1', ()=>{
config.get = jest.fn(()=>'production');
mongoose.connection.readyState = 1;
dbCheck({}, {}, next);
expect(next).toHaveBeenCalled();
});
it('should throw if readyState != 1', ()=>{
config.get = jest.fn(()=>'production');
mongoose.connection.readyState = 99;
expect(()=>dbCheck({}, {}, next)).toThrow(/Unable to connect/);
});
});

View File

@@ -8,7 +8,7 @@ const brewSnippetsToJSON = (menuTitle, userBrewSnippets, themeBundleSnippets=nul
const mpAsSnippets = []; const mpAsSnippets = [];
// Snippets from Themes first. // Snippets from Themes first.
if(themeBundleSnippets) { if(themeBundleSnippets) {
for (const themes of themeBundleSnippets) { for (let themes of themeBundleSnippets) {
if(typeof themes !== 'string') { if(typeof themes !== 'string') {
const userSnippets = []; const userSnippets = [];
const snipSplit = themes.snippets.trim().split(textSplit).slice(1); const snipSplit = themes.snippets.trim().split(textSplit).slice(1);
@@ -77,8 +77,8 @@ const yamlSnippetsToText = (yamlObj)=>{
let snippetsText = ''; let snippetsText = '';
for (const snippet of yamlObj) { for (let snippet of yamlObj) {
for (const subSnippet of snippet.subsnippets) { for (let subSnippet of snippet.subsnippets) {
snippetsText = `${snippetsText}\\snippet ${subSnippet.name}\n${subSnippet.gen || ''}\n`; snippetsText = `${snippetsText}\\snippet ${subSnippet.name}\n${subSnippet.gen || ''}\n`;
} }
} }
@@ -121,7 +121,7 @@ const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{
const res = await request const res = await request
.get(`/api/theme/${renderer}/${theme}`) .get(`/api/theme/${renderer}/${theme}`)
.catch((err)=>{ .catch((err)=>{
setError(err); setError(err)
}); });
if(!res) { if(!res) {
setThemeBundle({}); setThemeBundle({});
@@ -133,14 +133,14 @@ const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{
setError(null); setError(null);
}; };
const debugTextMismatch = (clientTextRaw, serverTextRaw, label)=>{ const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
const clientText = clientTextRaw?.normalize('NFC') || ''; const clientText = clientTextRaw?.normalize('NFC') || '';
const serverText = serverTextRaw?.normalize('NFC') || ''; const serverText = serverTextRaw?.normalize('NFC') || '';
const clientBuffer = Buffer.from(clientText, 'utf8'); const clientBuffer = Buffer.from(clientText, 'utf8');
const serverBuffer = Buffer.from(serverText, 'utf8'); const serverBuffer = Buffer.from(serverText, 'utf8');
if(clientBuffer.equals(serverBuffer)) { if (clientBuffer.equals(serverBuffer)) {
console.log(`${label} text matches byte-for-byte.`); console.log(`${label} text matches byte-for-byte.`);
return; return;
} }
@@ -151,7 +151,7 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label)=>{
// Byte-level diff // Byte-level diff
for (let i = 0; i < Math.min(clientBuffer.length, serverBuffer.length); i++) { for (let i = 0; i < Math.min(clientBuffer.length, serverBuffer.length); i++) {
if(clientBuffer[i] !== serverBuffer[i]) { if (clientBuffer[i] !== serverBuffer[i]) {
console.log(`Byte mismatch at offset ${i}: client=0x${clientBuffer[i].toString(16)} server=0x${serverBuffer[i].toString(16)}`); console.log(`Byte mismatch at offset ${i}: client=0x${clientBuffer[i].toString(16)} server=0x${serverBuffer[i].toString(16)}`);
break; break;
} }
@@ -159,14 +159,14 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label)=>{
// Char-level diff // Char-level diff
for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) { for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) {
if(clientText[i] !== serverText[i]) { if (clientText[i] !== serverText[i]) {
console.log(`Char mismatch at index ${i}:`); console.log(`Char mismatch at index ${i}:`);
console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`); console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`);
console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`); console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`);
break; break;
} }
} }
}; }
export { export {
splitTextStyleAndMetadata, splitTextStyleAndMetadata,

View File

@@ -38,10 +38,14 @@
animation-duration : 0.4s; animation-duration : 0.4s;
} }
.CodeMirror-search-field { .CodeMirror-vscrollbar {
width:25em !important; &::-webkit-scrollbar { width : 20px; }
outline:1px inset #00000055 !important; &::-webkit-scrollbar-thumb {
width : 20px;
background : linear-gradient(90deg, #858585 15px, #808080 15px);
} }
}
//.cm-tab { //.cm-tab {
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right; // background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;

View File

@@ -185,7 +185,7 @@ const mustacheSpans = {
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-+*/()#%=?.&:!@$^;:\[\]_= ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g; const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g;
const match = completeSpan.exec(src); const match = completeSpan.exec(src);
if(match) { if(match) {
//Find closing delimiter //Find closing delimiter
@@ -242,7 +242,7 @@ const mustacheDivs = {
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-+*/()#%=?.&:!@$^;:\[\]_= ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm; const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm;
const match = completeBlock.exec(src); const match = completeBlock.exec(src);
if(match) { if(match) {
//Find closing delimiter //Find closing delimiter
@@ -297,7 +297,7 @@ const mustacheInjectInline = {
level : 'inline', level : 'inline',
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?.&:!@$^;:\[\]_= ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g; const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/g;
const match = inlineRegex.exec(src); const match = inlineRegex.exec(src);
if(match) { if(match) {
const lastToken = tokens[tokens.length - 1]; const lastToken = tokens[tokens.length - 1];
@@ -343,7 +343,7 @@ const mustacheInjectBlock = {
level : 'block', level : 'block',
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?.&:!@$^;:\[\]_= ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/ym; const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/ym;
const match = inlineRegex.exec(src); const match = inlineRegex.exec(src);
if(match) { if(match) {
const lastToken = tokens[tokens.length - 1]; const lastToken = tokens[tokens.length - 1];
@@ -435,7 +435,7 @@ const replaceVar = function(input, hoist=false, allowUnresolved=false) {
try { try {
return mathParser.evaluate(replacedLabel); return mathParser.evaluate(replacedLabel);
} catch { } catch (error) {
return undefined; // Return undefined if invalid math result return undefined; // Return undefined if invalid math result
} }
} }
@@ -680,7 +680,7 @@ const tableTerminators = [
Marked.use(MarkedVariables()); Marked.use(MarkedVariables());
Marked.use(MarkedDefinitionLists()); Marked.use(MarkedDefinitionLists());
Marked.use({ extensions: [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] }); Marked.use({ extensions : [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] });
Marked.use(mustacheInjectBlock); Marked.use(mustacheInjectBlock);
Marked.use(MarkedAlignedParagraphs()); Marked.use(MarkedAlignedParagraphs());
Marked.use(MarkedSubSuperText()); Marked.use(MarkedSubSuperText());

View File

@@ -49,7 +49,7 @@ const cleanUrl = function (sanitize, base, href) {
prot = decodeURIComponent(unescape(href)) prot = decodeURIComponent(unescape(href))
.replace(nonWordAndColonTest, '') .replace(nonWordAndColonTest, '')
.toLowerCase(); .toLowerCase();
} catch { } catch (e) {
return null; return null;
} }
if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) { if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
@@ -58,7 +58,7 @@ const cleanUrl = function (sanitize, base, href) {
} }
try { try {
href = encodeURI(href).replace(/%25/g, '%'); href = encodeURI(href).replace(/%25/g, '%');
} catch { } catch (e) {
return null; return null;
} }
return href; return href;

View File

@@ -418,7 +418,6 @@
color : var(--HB_Color_Footnotes); color : var(--HB_Color_Footnotes);
} }
.footnote { .footnote {
text-transform: uppercase;
position : absolute; position : absolute;
right : 80px; right : 80px;
bottom : 32px; bottom : 32px;

View File

@@ -611,17 +611,3 @@ h6,
} }
.toc.wide li { break-inside : auto; } .toc.wide li { break-inside : auto; }
} }
/**********************************
Firefox endruns
**********************************/
@supports (-moz-user-select: none) { // This section will only apply to Firefox; it's the only browser that supports `-mos-xyz...`
.page {
blockquote, table {
page-break-inside: auto;
break-inside: auto;
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 B

View File

@@ -1,6 +0,0 @@
{
"name" : "UnearthedArcana",
"renderer" : "V3",
"baseTheme" : false,
"baseSnippets" : false
}

View File

@@ -1,38 +0,0 @@
@import (less) './themes/fonts/5e/fonts.less';
@import (less) './themes/assets/assets.less';
:root {
//Colors
--HB_Color_Background : #FFFFFF; // White
--HB_Color_WatercolorStain : #000000; // Black
}
.page {
font-family: Cambria,Georgia,serif;
font-size: 14px;
h1, h2, h3, h4 {
font-variant: small-caps;
font-weight: normal;
}
h1 {
column-span: all;
-webkit-column-span: all;
font-size: 40px;
}
h2 {
font-size: 26px;
}
h3 {
font-size: 20px;
border-bottom: 2px solid black;
}
h4 {
font-size: 18px;
}
h5 {
font-size: 16px;
}
h6 {
font-size: 14px;
}
}

View File

@@ -14,7 +14,6 @@ const diceFont = {
'df_d10_7' : 'df d10-7', 'df_d10_7' : 'df d10-7',
'df_d10_8' : 'df d10-8', 'df_d10_8' : 'df d10-8',
'df_d10_9' : 'df d10-9', 'df_d10_9' : 'df d10-9',
'df_d10_0' : 'df d10-0',
'df_d12' : 'df d12', 'df_d12' : 'df d12',
'df_d12_1' : 'df d12-1', 'df_d12_1' : 'df d12-1',
'df_d12_10' : 'df d12-10', 'df_d12_10' : 'df d12-10',
@@ -91,108 +90,7 @@ const diceFont = {
'df_solid_small_dot_d6_3' : 'df solid-small-dot-d6-3', 'df_solid_small_dot_d6_3' : 'df solid-small-dot-d6-3',
'df_solid_small_dot_d6_4' : 'df solid-small-dot-d6-4', 'df_solid_small_dot_d6_4' : 'df solid-small-dot-d6-4',
'df_solid_small_dot_d6_5' : 'df solid-small-dot-d6-5', 'df_solid_small_dot_d6_5' : 'df solid-small-dot-d6-5',
'df_solid_small_dot_d6_6' : 'df solid-small-dot-d6-6', 'df_solid_small_dot_d6_6' : 'df solid-small-dot-d6-6'
'df_d10_00' : 'df d10-00',
'df_d10_01' : 'df d10-01',
'df_d10_02' : 'df d10-02',
'df_d10_03' : 'df d10-03',
'df_d10_04' : 'df d10-04',
'df_d10_05' : 'df d10-05',
'df_d10_06' : 'df d10-06',
'df_d10_07' : 'df d10-07',
'df_d10_08' : 'df d10-08',
'df_d10_09' : 'df d10-09',
'df_d10_10' : 'df d10-10',
'df_d10_11' : 'df d10-11',
'df_d10_12' : 'df d10-12',
'df_d10_13' : 'df d10-13',
'df_d10_14' : 'df d10-14',
'df_d10_15' : 'df d10-15',
'df_d10_16' : 'df d10-16',
'df_d10_17' : 'df d10-17',
'df_d10_18' : 'df d10-18',
'df_d10_19' : 'df d10-19',
'df_d10_20' : 'df d10-20',
'df_d10_21' : 'df d10-21',
'df_d10_22' : 'df d10-22',
'df_d10_23' : 'df d10-23',
'df_d10_24' : 'df d10-24',
'df_d10_25' : 'df d10-25',
'df_d10_26' : 'df d10-26',
'df_d10_27' : 'df d10-27',
'df_d10_28' : 'df d10-28',
'df_d10_29' : 'df d10-29',
'df_d10_30' : 'df d10-30',
'df_d10_31' : 'df d10-31',
'df_d10_32' : 'df d10-32',
'df_d10_33' : 'df d10-33',
'df_d10_34' : 'df d10-34',
'df_d10_35' : 'df d10-35',
'df_d10_36' : 'df d10-36',
'df_d10_37' : 'df d10-37',
'df_d10_38' : 'df d10-38',
'df_d10_39' : 'df d10-39',
'df_d10_40' : 'df d10-40',
'df_d10_41' : 'df d10-41',
'df_d10_42' : 'df d10-42',
'df_d10_43' : 'df d10-43',
'df_d10_44' : 'df d10-44',
'df_d10_45' : 'df d10-45',
'df_d10_46' : 'df d10-46',
'df_d10_47' : 'df d10-47',
'df_d10_48' : 'df d10-48',
'df_d10_49' : 'df d10-49',
'df_d10_50' : 'df d10-50',
'df_d10_51' : 'df d10-51',
'df_d10_52' : 'df d10-52',
'df_d10_53' : 'df d10-53',
'df_d10_54' : 'df d10-54',
'df_d10_55' : 'df d10-55',
'df_d10_56' : 'df d10-56',
'df_d10_57' : 'df d10-57',
'df_d10_58' : 'df d10-58',
'df_d10_59' : 'df d10-59',
'df_d10_60' : 'df d10-60',
'df_d10_61' : 'df d10-61',
'df_d10_62' : 'df d10-62',
'df_d10_63' : 'df d10-63',
'df_d10_64' : 'df d10-64',
'df_d10_65' : 'df d10-65',
'df_d10_66' : 'df d10-66',
'df_d10_67' : 'df d10-67',
'df_d10_68' : 'df d10-68',
'df_d10_69' : 'df d10-69',
'df_d10_70' : 'df d10-70',
'df_d10_71' : 'df d10-71',
'df_d10_72' : 'df d10-72',
'df_d10_73' : 'df d10-73',
'df_d10_74' : 'df d10-74',
'df_d10_75' : 'df d10-75',
'df_d10_76' : 'df d10-76',
'df_d10_77' : 'df d10-77',
'df_d10_78' : 'df d10-78',
'df_d10_79' : 'df d10-79',
'df_d10_80' : 'df d10-80',
'df_d10_81' : 'df d10-81',
'df_d10_82' : 'df d10-82',
'df_d10_83' : 'df d10-83',
'df_d10_84' : 'df d10-84',
'df_d10_85' : 'df d10-85',
'df_d10_86' : 'df d10-86',
'df_d10_87' : 'df d10-87',
'df_d10_88' : 'df d10-88',
'df_d10_89' : 'df d10-89',
'df_d10_90' : 'df d10-90',
'df_d10_91' : 'df d10-91',
'df_d10_92' : 'df d10-92',
'df_d10_93' : 'df d10-93',
'df_d10_94' : 'df d10-94',
'df_d10_95' : 'df d10-95',
'df_d10_96' : 'df d10-96',
'df_d10_97' : 'df d10-97',
'df_d10_98' : 'df d10-98',
'df_d10_99' : 'df d10-99',
'df_d10_100' : 'df d10-100'
}; };
export default diceFont; export default diceFont;

View File

@@ -6,14 +6,6 @@
src : url('../../../fonts/iconFonts/diceFont.woff2'); src : url('../../../fonts/iconFonts/diceFont.woff2');
} }
@font-face {
font-family : 'DiceFontD100';
src : url('../../../fonts/iconFonts/diceFontD100.woff2') format('woff2');
font-weight : normal;
font-style : normal;
font-display : block;
}
.df { .df {
display : inline; display : inline;
font-family : 'DiceFont'; font-family : 'DiceFont';
@@ -34,6 +26,17 @@
&.F-plus::before { content : '\f192'; } &.F-plus::before { content : '\f192'; }
&.F-zero::before { content : '\f193'; } &.F-zero::before { content : '\f193'; }
&.d10::before { content : '\f194'; } &.d10::before { content : '\f194'; }
&.d10-0::before { content : '\f100'; }
&.d10-1::before { content : '\f101'; }
&.d10-10::before { content : '\f102'; }
&.d10-2::before { content : '\f103'; }
&.d10-3::before { content : '\f104'; }
&.d10-4::before { content : '\f105'; }
&.d10-5::before { content : '\f106'; }
&.d10-6::before { content : '\f107'; }
&.d10-7::before { content : '\f108'; }
&.d10-8::before { content : '\f109'; }
&.d10-9::before { content : '\f10a'; }
&.d12::before { content : '\f195'; } &.d12::before { content : '\f195'; }
&.d12-1::before { content : '\f10b'; } &.d12-1::before { content : '\f10b'; }
&.d12-10::before { content : '\f10c'; } &.d12-10::before { content : '\f10c'; }
@@ -111,568 +114,4 @@
&.solid-small-dot-d6-4::before { content : '\f18c'; } &.solid-small-dot-d6-4::before { content : '\f18c'; }
&.solid-small-dot-d6-5::before { content : '\f18d'; } &.solid-small-dot-d6-5::before { content : '\f18d'; }
&.solid-small-dot-d6-6::before { content : '\f18e'; } &.solid-small-dot-d6-6::before { content : '\f18e'; }
// Replacement d10
&.d10-0::before {
font-family : 'DiceFontD100';
content : '\e900';
}
&.d10-1::before {
font-family : 'DiceFontD100';
content : '\e901';
}
&.d10-2::before {
font-family : 'DiceFontD100';
content : '\e902';
}
&.d10-3::before {
font-family : 'DiceFontD100';
content : '\e903';
}
&.d10-4::before {
font-family : 'DiceFontD100';
content : '\e904';
}
&.d10-5::before {
font-family : 'DiceFontD100';
content : '\e905';
}
&.d10-6::before {
font-family : 'DiceFontD100';
content : '\e906';
}
&.d10-7::before {
font-family : 'DiceFontD100';
content : '\e907';
}
&.d10-8::before {
font-family : 'DiceFontD100';
content : '\e908';
}
&.d10-9::before {
font-family : 'DiceFontD100';
content : '\e909';
}
&.d10-10::before {
font-family : 'DiceFontD100';
content : '\e90a';
}
// d100
&.d10-00::before {
font-family : 'DiceFontD100';
content : '\e90b';
}
&.d10-01::before {
font-family : 'DiceFontD100';
content : '\e90c';
}
&.d10-02::before {
font-family : 'DiceFontD100';
content : '\e90d';
}
&.d10-03::before {
font-family : 'DiceFontD100';
content : '\e90e';
}
&.d10-04::before {
font-family : 'DiceFontD100';
content : '\e90f';
}
&.d10-05::before {
font-family : 'DiceFontD100';
content : '\e910';
}
&.d10-06::before {
font-family : 'DiceFontD100';
content : '\e911';
}
&.d10-07::before {
font-family : 'DiceFontD100';
content : '\e912';
}
&.d10-08::before {
font-family : 'DiceFontD100';
content : '\e913';
}
&.d10-09::before {
font-family : 'DiceFontD100';
content : '\e914';
}
&.d10-10::before {
font-family : 'DiceFontD100';
content : '\e915';
}
&.d10-11::before {
font-family : 'DiceFontD100';
content : '\e916';
}
&.d10-12::before {
font-family : 'DiceFontD100';
content : '\e917';
}
&.d10-13::before {
font-family : 'DiceFontD100';
content : '\e918';
}
&.d10-14::before {
font-family : 'DiceFontD100';
content : '\e919';
}
&.d10-15::before {
font-family : 'DiceFontD100';
content : '\e91a';
}
&.d10-16::before {
font-family : 'DiceFontD100';
content : '\e91b';
}
&.d10-17::before {
font-family : 'DiceFontD100';
content : '\e91c';
}
&.d10-18::before {
font-family : 'DiceFontD100';
content : '\e91d';
}
&.d10-19::before {
font-family : 'DiceFontD100';
content : '\e91e';
}
&.d10-20::before {
font-family : 'DiceFontD100';
content : '\e91f';
}
&.d10-21::before {
font-family : 'DiceFontD100';
content : '\e920';
}
&.d10-22::before {
font-family : 'DiceFontD100';
content : '\e921';
}
&.d10-23::before {
font-family : 'DiceFontD100';
content : '\e922';
}
&.d10-24::before {
font-family : 'DiceFontD100';
content : '\e923';
}
&.d10-25::before {
font-family : 'DiceFontD100';
content : '\e924';
}
&.d10-26::before {
font-family : 'DiceFontD100';
content : '\e925';
}
&.d10-27::before {
font-family : 'DiceFontD100';
content : '\e926';
}
&.d10-28::before {
font-family : 'DiceFontD100';
content : '\e927';
}
&.d10-29::before {
font-family : 'DiceFontD100';
content : '\e928';
}
&.d10-30::before {
font-family : 'DiceFontD100';
content : '\e929';
}
&.d10-31::before {
font-family : 'DiceFontD100';
content : '\e92a';
}
&.d10-32::before {
font-family : 'DiceFontD100';
content : '\e92b';
}
&.d10-33::before {
font-family : 'DiceFontD100';
content : '\e92c';
}
&.d10-34::before {
font-family : 'DiceFontD100';
content : '\e92d';
}
&.d10-35::before {
font-family : 'DiceFontD100';
content : '\e92e';
}
&.d10-36::before {
font-family : 'DiceFontD100';
content : '\e92f';
}
&.d10-37::before {
font-family : 'DiceFontD100';
content : '\e930';
}
&.d10-38::before {
font-family : 'DiceFontD100';
content : '\e931';
}
&.d10-39::before {
font-family : 'DiceFontD100';
content : '\e932';
}
&.d10-40::before {
font-family : 'DiceFontD100';
content : '\e933';
}
&.d10-41::before {
font-family : 'DiceFontD100';
content : '\e934';
}
&.d10-42::before {
font-family : 'DiceFontD100';
content : '\e935';
}
&.d10-43::before {
font-family : 'DiceFontD100';
content : '\e936';
}
&.d10-44::before {
font-family : 'DiceFontD100';
content : '\e937';
}
&.d10-45::before {
font-family : 'DiceFontD100';
content : '\e938';
}
&.d10-46::before {
font-family : 'DiceFontD100';
content : '\e939';
}
&.d10-47::before {
font-family : 'DiceFontD100';
content : '\e93a';
}
&.d10-48::before {
font-family : 'DiceFontD100';
content : '\e93b';
}
&.d10-49::before {
font-family : 'DiceFontD100';
content : '\e93c';
}
&.d10-50::before {
font-family : 'DiceFontD100';
content : '\e93d';
}
&.d10-51::before {
font-family : 'DiceFontD100';
content : '\e93e';
}
&.d10-52::before {
font-family : 'DiceFontD100';
content : '\e93f';
}
&.d10-53::before {
font-family : 'DiceFontD100';
content : '\e940';
}
&.d10-54::before {
font-family : 'DiceFontD100';
content : '\e941';
}
&.d10-55::before {
font-family : 'DiceFontD100';
content : '\e942';
}
&.d10-56::before {
font-family : 'DiceFontD100';
content : '\e943';
}
&.d10-57::before {
font-family : 'DiceFontD100';
content : '\e944';
}
&.d10-58::before {
font-family : 'DiceFontD100';
content : '\e945';
}
&.d10-59::before {
font-family : 'DiceFontD100';
content : '\e946';
}
&.d10-60::before {
font-family : 'DiceFontD100';
content : '\e947';
}
&.d10-61::before {
font-family : 'DiceFontD100';
content : '\e948';
}
&.d10-62::before {
font-family : 'DiceFontD100';
content : '\e949';
}
&.d10-63::before {
font-family : 'DiceFontD100';
content : '\e94a';
}
&.d10-64::before {
font-family : 'DiceFontD100';
content : '\e94b';
}
&.d10-65::before {
font-family : 'DiceFontD100';
content : '\e94c';
}
&.d10-66::before {
font-family : 'DiceFontD100';
content : '\e94d';
}
&.d10-67::before {
font-family : 'DiceFontD100';
content : '\e94e';
}
&.d10-68::before {
font-family : 'DiceFontD100';
content : '\e94f';
}
&.d10-69::before {
font-family : 'DiceFontD100';
content : '\e950';
}
&.d10-70::before {
font-family : 'DiceFontD100';
content : '\e951';
}
&.d10-71::before {
font-family : 'DiceFontD100';
content : '\e952';
}
&.d10-72::before {
font-family : 'DiceFontD100';
content : '\e953';
}
&.d10-73::before {
font-family : 'DiceFontD100';
content : '\e954';
}
&.d10-74::before {
font-family : 'DiceFontD100';
content : '\e955';
}
&.d10-75::before {
font-family : 'DiceFontD100';
content : '\e956';
}
&.d10-76::before {
font-family : 'DiceFontD100';
content : '\e957';
}
&.d10-77::before {
font-family : 'DiceFontD100';
content : '\e958';
}
&.d10-78::before {
font-family : 'DiceFontD100';
content : '\e959';
}
&.d10-79::before {
font-family : 'DiceFontD100';
content : '\e95a';
}
&.d10-80::before {
font-family : 'DiceFontD100';
content : '\e95b';
}
&.d10-81::before {
font-family : 'DiceFontD100';
content : '\e95c';
}
&.d10-82::before {
font-family : 'DiceFontD100';
content : '\e95d';
}
&.d10-83::before {
font-family : 'DiceFontD100';
content : '\e95e';
}
&.d10-84::before {
font-family : 'DiceFontD100';
content : '\e95f';
}
&.d10-85::before {
font-family : 'DiceFontD100';
content : '\e960';
}
&.d10-86::before {
font-family : 'DiceFontD100';
content : '\e961';
}
&.d10-87::before {
font-family : 'DiceFontD100';
content : '\e962';
}
&.d10-88::before {
font-family : 'DiceFontD100';
content : '\e963';
}
&.d10-89::before {
font-family : 'DiceFontD100';
content : '\e964';
}
&.d10-90::before {
font-family : 'DiceFontD100';
content : '\e965';
}
&.d10-91::before {
font-family : 'DiceFontD100';
content : '\e966';
}
&.d10-92::before {
font-family : 'DiceFontD100';
content : '\e967';
}
&.d10-93::before {
font-family : 'DiceFontD100';
content : '\e968';
}
&.d10-94::before {
font-family : 'DiceFontD100';
content : '\e969';
}
&.d10-95::before {
font-family : 'DiceFontD100';
content : '\e96a';
}
&.d10-96::before {
font-family : 'DiceFontD100';
content : '\e96b';
}
&.d10-97::before {
font-family : 'DiceFontD100';
content : '\e96c';
}
&.d10-98::before {
font-family : 'DiceFontD100';
content : '\e96d';
}
&.d10-99::before {
font-family : 'DiceFontD100';
content : '\e96e';
}
&.d10-100::before {
font-family : 'DiceFontD100';
content : '\e96f';
}
} }

View File

@@ -35,13 +35,6 @@
"baseTheme": "Blank", "baseTheme": "Blank",
"baseSnippets": "5ePHB", "baseSnippets": "5ePHB",
"path": "Journal" "path": "Journal"
},
"UnearthedArcana": {
"name": "UnearthedArcana",
"renderer": "V3",
"baseTheme": false,
"baseSnippets": false,
"path": "UnearthedArcana"
} }
} }
} }