mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-25 11:53:02 +00:00
Compare commits
52 Commits
RemoveHand
...
LintEveryt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63bebe1efd | ||
|
|
22e26d635a | ||
|
|
643e0ac650 | ||
|
|
5395412ac5 | ||
|
|
dc4610ea1b | ||
|
|
1e71e9e18a | ||
|
|
4203e90d09 | ||
|
|
dc94555c94 | ||
|
|
41aebf084b | ||
|
|
74e17e154f | ||
|
|
a944b23ca0 | ||
|
|
12052853db | ||
|
|
c0f67bef5a | ||
|
|
8f715a6615 | ||
|
|
1f51abaf10 | ||
|
|
c90a8c53a5 | ||
|
|
ac18f4bd1d | ||
|
|
7393aef806 | ||
|
|
2c4c4b8f92 | ||
|
|
c751d647d9 | ||
|
|
6057b35d19 | ||
|
|
521d42f32f | ||
|
|
e9f8302597 | ||
|
|
f429b1755d | ||
|
|
ae51213c8c | ||
|
|
8f7ae35f08 | ||
|
|
f0bb06e706 | ||
|
|
aff9a85769 | ||
|
|
e0379a0baa | ||
|
|
e8a0681015 | ||
|
|
3ed61ebe2c | ||
|
|
c2e51b0baa | ||
|
|
bc258f5239 | ||
|
|
ba23763294 | ||
|
|
51b91567f6 | ||
|
|
eefda9fe45 | ||
|
|
e793db7b37 | ||
|
|
ff5450ad8c | ||
|
|
07495b0dea | ||
|
|
c6ed67db08 | ||
|
|
c50c279ef3 | ||
|
|
cc246fb31a | ||
|
|
788324fe31 | ||
|
|
da8772daa7 | ||
|
|
4c5eef46a0 | ||
|
|
a1ab27b57f | ||
|
|
d6a5a1f03c | ||
|
|
f04d6cdd1f | ||
|
|
4fd61ce92c | ||
|
|
88b70d340e | ||
|
|
ed05d8c754 | ||
|
|
077aaeb815 |
@@ -295,12 +295,6 @@ const BrewRenderer = (props)=>{
|
||||
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]);
|
||||
renderedPages = useMemo(()=>renderPages(), [props.text, displayOptions]);
|
||||
|
||||
@@ -329,10 +323,9 @@ const BrewRenderer = (props)=>{
|
||||
contentDidMount={frameDidMount}
|
||||
onClick={()=>{emitClick();}}
|
||||
>
|
||||
<div className={`brewRenderer ${global.config.deployment && 'deployment'}`}
|
||||
<div className='brewRenderer'
|
||||
onKeyDown={handleControlKeys}
|
||||
tabIndex={-1}
|
||||
style={ styleObject }
|
||||
>
|
||||
|
||||
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
overflow-y : scroll;
|
||||
will-change : transform;
|
||||
&:has(.facing, .flow) { padding : 60px 30px; }
|
||||
&.deployment { background-color : darkred; }
|
||||
:where(.pages) {
|
||||
&.facing {
|
||||
display : grid;
|
||||
|
||||
@@ -18,7 +18,7 @@ module.exports = {
|
||||
try {
|
||||
Boolean(new URL(value));
|
||||
return null;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return 'Must be a valid URL';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ const WithRoute = ({ el: Element, ...rest })=>{
|
||||
const params = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const queryParams = Object.fromEntries(searchParams?.entries() || []);
|
||||
|
||||
return <Element {...rest} {...params} query={queryParams} />;
|
||||
};
|
||||
|
||||
@@ -50,11 +49,20 @@ const Homebrew = (props)=>{
|
||||
global.enable_themes = enable_themes;
|
||||
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 (
|
||||
<Router location={url}>
|
||||
<div className='homebrew'>
|
||||
<div className={`homebrew${(config.deployment || config.local) ? ' deployment' : ''}`} style={backgroundObject()}>
|
||||
<Routes>
|
||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} />
|
||||
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} />
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
@import 'naturalcrit/styles/core.less';
|
||||
.homebrew {
|
||||
height : 100%;
|
||||
background-color:@steel;
|
||||
&.deployment { background-color : darkred; }
|
||||
|
||||
.sitePage {
|
||||
display : flex;
|
||||
flex-direction : column;
|
||||
height : 100%;
|
||||
overflow-y : hidden;
|
||||
background-color : @steel;
|
||||
.content {
|
||||
position : relative;
|
||||
flex : auto;
|
||||
|
||||
@@ -2,9 +2,9 @@ require('./error-navitem.less');
|
||||
const React = require('react');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
const ErrorNavItem = ({error = '', clearError})=>{
|
||||
const ErrorNavItem = ({ error = '', clearError })=>{
|
||||
const response = error.response;
|
||||
const errorCode = error.code
|
||||
const errorCode = error.code;
|
||||
const status = response?.status;
|
||||
const HBErrorCode = response?.body?.HBErrorCode;
|
||||
const message = response?.body?.message;
|
||||
@@ -15,7 +15,7 @@ const ErrorNavItem = ({error = '', clearError})=>{
|
||||
errMsg += `\`\`\`\n${error.stack}\n`;
|
||||
errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``;
|
||||
console.log(errMsg);
|
||||
} catch (e){}
|
||||
} catch {}
|
||||
|
||||
if(status === 409) {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
|
||||
@@ -34,8 +34,8 @@ const NewBrew = ()=>{
|
||||
}
|
||||
|
||||
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.`);
|
||||
|
||||
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);
|
||||
|
||||
@@ -2,22 +2,22 @@ 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 getShareId = (brew)=>(
|
||||
brew.googleId && !brew.stubbed
|
||||
? brew.googleId + brew.shareId
|
||||
: brew.shareId
|
||||
);
|
||||
|
||||
const getRedditLink = (brew)=>{
|
||||
const text = dedent`
|
||||
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)}`;
|
||||
};
|
||||
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(brew.title.toWellFormed())}&text=${encodeURIComponent(text)}`;
|
||||
};
|
||||
|
||||
export default ({brew}) => (
|
||||
export default ({ brew })=>(
|
||||
<Nav.dropdown>
|
||||
<Nav.item color='teal' icon='fas fa-share-alt'>
|
||||
share
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
/* eslint-disable max-lines */
|
||||
import './editPage.less';
|
||||
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
// Common imports
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import Markdown from 'naturalcrit/markdown.js';
|
||||
import _ from 'lodash';
|
||||
|
||||
import _ from 'lodash';;
|
||||
import { makePatches, stringifyPatches } from '@sanity/diff-match-patch';
|
||||
import { md5 } from 'hash-wasm';
|
||||
import { gzipSync, strToU8 } from 'fflate';
|
||||
import { Meta } from 'vitreum/headtags';
|
||||
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 ShareNavItem from '../../navbar/share.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';
|
||||
|
||||
import SplitPane from 'client/components/splitPane/splitPane.jsx';
|
||||
import Editor from '../../editor/editor.jsx';
|
||||
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
|
||||
// Page specific imports
|
||||
import { Meta } from 'vitreum/headtags';
|
||||
import { md5 } from 'hash-wasm';
|
||||
import { gzipSync, strToU8 } from 'fflate';
|
||||
import { makePatches, stringifyPatches } from '@sanity/diff-match-patch';
|
||||
|
||||
import ShareNavItem from '../../navbar/share.navitem.jsx';
|
||||
import LockNotification from './lockNotification/lockNotification.jsx';
|
||||
|
||||
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
|
||||
import { printCurrentBrew, fetchThemeBundle } from '../../../../shared/helpers.js';
|
||||
|
||||
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
|
||||
|
||||
import googleDriveIcon from '../../googleDrive.svg';
|
||||
|
||||
const SAVE_TIMEOUT = 10000;
|
||||
@@ -46,6 +46,8 @@ const STYLEKEY = 'HB_newPage_style';
|
||||
const SNIPKEY = 'HB_newPage_snippets';
|
||||
const METAKEY = 'HB_newPage_meta';
|
||||
|
||||
const useLocalStorage = false;
|
||||
const neverSaved = false;
|
||||
|
||||
const EditPage = (props)=>{
|
||||
props = {
|
||||
@@ -77,8 +79,6 @@ const EditPage = (props)=>{
|
||||
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
|
||||
|
||||
const useLocalStorage = false;
|
||||
|
||||
useEffect(()=>{
|
||||
const autoSavePref = JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true);
|
||||
setAutoSaveEnabled(autoSavePref);
|
||||
@@ -123,16 +123,16 @@ const EditPage = (props)=>{
|
||||
editorRef.current?.update();
|
||||
};
|
||||
|
||||
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata'
|
||||
if (subfield == 'renderer' || subfield == 'theme')
|
||||
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(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
|
||||
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
|
||||
|
||||
if(useLocalStorage) {
|
||||
if(field == 'text') localStorage.setItem(BREWKEY, value);
|
||||
@@ -309,14 +309,18 @@ const EditPage = (props)=>{
|
||||
|
||||
// #3 - Unsaved changes exist, click to save, show SAVE NOW
|
||||
if(unsavedChanges)
|
||||
return <Nav.item className='save' onClick={()=>trySave(true)} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
|
||||
return <Nav.item className='save' onClick={()=>trySave(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>;
|
||||
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>;
|
||||
return <Nav.item className='save saved'>saved</Nav.item>;
|
||||
};
|
||||
|
||||
const toggleAutoSave = ()=>{
|
||||
@@ -352,11 +356,11 @@ const EditPage = (props)=>{
|
||||
{renderSaveButton()}
|
||||
{renderAutoSaveButton()}
|
||||
</Nav.dropdown>}
|
||||
<NewBrewItem/>
|
||||
<HelpNavItem/>
|
||||
<ShareNavItem brew={currentBrew} />
|
||||
<NewBrewItem />
|
||||
<PrintNavItem />
|
||||
<HelpNavItem />
|
||||
<VaultNavItem />
|
||||
<ShareNavItem brew={currentBrew} />
|
||||
<RecentNavItem brew={currentBrew} storageKey='edit' />
|
||||
<AccountNavItem/>
|
||||
</Nav.section>
|
||||
|
||||
@@ -1,55 +1,79 @@
|
||||
/* eslint-disable max-lines */
|
||||
import './homePage.less';
|
||||
|
||||
import React from 'react';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import Markdown from 'naturalcrit/markdown.js';
|
||||
import { Meta } from 'vitreum/headtags';
|
||||
// Common imports
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import Markdown from 'naturalcrit/markdown.js';
|
||||
import _ from 'lodash';
|
||||
|
||||
import Nav from 'naturalcrit/nav/nav.jsx';
|
||||
import Navbar from '../../navbar/navbar.jsx';
|
||||
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
|
||||
import HelpNavItem from '../../navbar/help.navitem.jsx';
|
||||
import VaultNavItem from '../../navbar/vault.navitem.jsx';
|
||||
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
|
||||
import AccountNavItem from '../../navbar/account.navitem.jsx';
|
||||
import ErrorNavItem from '../../navbar/error-navitem.jsx';
|
||||
import { fetchThemeBundle } from '../../../../shared/helpers.js';
|
||||
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 SplitPane from 'client/components/splitPane/splitPane.jsx';
|
||||
import Editor from '../../editor/editor.jsx';
|
||||
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
|
||||
|
||||
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
|
||||
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';
|
||||
|
||||
const BREWKEY = 'homebrewery-new';
|
||||
const STYLEKEY = 'homebrewery-new-style';
|
||||
const SNIPKEY = 'homebrewery-new-snippets';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
|
||||
const useLocalStorage = false;
|
||||
const neverSaved = true;
|
||||
|
||||
const HomePage =(props)=>{
|
||||
props = {
|
||||
brew : DEFAULT_BREW,
|
||||
ver : '0.0.0',
|
||||
...props
|
||||
};
|
||||
...props
|
||||
};
|
||||
|
||||
const [currentBrew , setCurrentBrew] = useState(props.brew);
|
||||
const [welcomeText , setWelcomeText] = useState(props.brew.text);
|
||||
const [error , setError] = useState(undefined);
|
||||
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text));
|
||||
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 [isSaving , setIsSaving] = useState(false);
|
||||
const [autoSaveEnabled , setAutoSaveEnable] = useState(false);
|
||||
|
||||
const editorRef = useRef(null);
|
||||
|
||||
const useLocalStorage = 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 = ()=>{
|
||||
@@ -65,20 +89,27 @@ const HomePage =(props)=>{
|
||||
});
|
||||
};
|
||||
|
||||
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')
|
||||
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(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
|
||||
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
|
||||
|
||||
if(useLocalStorage) {
|
||||
if(field == 'text') localStorage.setItem(BREWKEY, value);
|
||||
@@ -92,6 +123,41 @@ const HomePage =(props)=>{
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -100,11 +166,11 @@ const HomePage =(props)=>{
|
||||
const renderNavbar = ()=>{
|
||||
return <Navbar ver={props.ver}>
|
||||
<Nav.section>
|
||||
{error ?
|
||||
<ErrorNavItem error={error} clearError={clearError}></ErrorNavItem> :
|
||||
null
|
||||
}
|
||||
{error
|
||||
? <ErrorNavItem error={error} clearError={clearError} />
|
||||
: renderSaveButton()}
|
||||
<NewBrewItem />
|
||||
<PrintNavItem />
|
||||
<HelpNavItem />
|
||||
<VaultNavItem />
|
||||
<RecentNavItem />
|
||||
@@ -144,7 +210,7 @@ const HomePage =(props)=>{
|
||||
/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
<div className={`floatingSaveButton${welcomeText !== currentBrew.text ? ' show' : ''}`} onClick={save}>
|
||||
<div className={`floatingSaveButton${unsavedChanges ? ' show' : ''}`} onClick={save}>
|
||||
Save current <i className='fas fa-save' />
|
||||
</div>
|
||||
|
||||
@@ -152,7 +218,7 @@ const HomePage =(props)=>{
|
||||
Create your own <i className='fas fa-magic' />
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = HomePage;
|
||||
|
||||
@@ -34,7 +34,13 @@
|
||||
}
|
||||
|
||||
.navItem.save {
|
||||
.fadeInRight();
|
||||
.transition(opacity);
|
||||
background-color : @orange;
|
||||
&:hover { background-color : @green; }
|
||||
&.neverSaved {
|
||||
.fadeOutRight();
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
/* eslint-disable max-lines */
|
||||
import './newPage.less';
|
||||
|
||||
// Common imports
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import Markdown from 'naturalcrit/markdown.js';
|
||||
import _ from 'lodash';
|
||||
|
||||
import Nav from 'naturalcrit/nav/nav.jsx';
|
||||
import Navbar from '../../navbar/navbar.jsx';
|
||||
import AccountNavItem from '../../navbar/account.navitem.jsx';
|
||||
import ErrorNavItem from '../../navbar/error-navitem.jsx';
|
||||
import HelpNavItem from '../../navbar/help.navitem.jsx';
|
||||
import PrintNavItem from '../../navbar/print.navitem.jsx';
|
||||
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
|
||||
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 { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
|
||||
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
|
||||
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';
|
||||
|
||||
const BREWKEY = 'HB_newPage_content';
|
||||
const STYLEKEY = 'HB_newPage_style';
|
||||
@@ -27,10 +33,12 @@ const METAKEY = 'HB_newPage_metadata';
|
||||
const SNIPKEY = 'HB_newPage_snippets';
|
||||
const SAVEKEYPREFIX = 'HB_editor_defaultSave_';
|
||||
|
||||
const useLocalStorage = true;
|
||||
const neverSaved = true;
|
||||
|
||||
const NewPage = (props) => {
|
||||
const NewPage = (props)=>{
|
||||
props = {
|
||||
brew: DEFAULT_BREW,
|
||||
brew : DEFAULT_BREW,
|
||||
...props
|
||||
};
|
||||
|
||||
@@ -43,17 +51,29 @@ const NewPage = (props) => {
|
||||
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 editorRef = useRef(null);
|
||||
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
|
||||
|
||||
const useLocalStorage = true;
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleControlKeys);
|
||||
useEffect(()=>{
|
||||
loadBrew();
|
||||
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
||||
|
||||
return () => {
|
||||
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);
|
||||
};
|
||||
}, []);
|
||||
@@ -76,6 +96,7 @@ const NewPage = (props) => {
|
||||
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
|
||||
|
||||
setCurrentBrew(brew);
|
||||
lastSavedBrew.current = brew;
|
||||
setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && saveGoogle);
|
||||
|
||||
localStorage.setItem(BREWKEY, brew.text);
|
||||
@@ -86,32 +107,27 @@ const NewPage = (props) => {
|
||||
window.history.replaceState({}, window.location.title, '/new/');
|
||||
};
|
||||
|
||||
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) printCurrentBrew();
|
||||
if (e.keyCode === S_KEY || e.keyCode === P_KEY) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
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')
|
||||
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(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
|
||||
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
|
||||
|
||||
if(useLocalStorage) {
|
||||
if(field == 'text') localStorage.setItem(BREWKEY, value);
|
||||
@@ -125,10 +141,10 @@ const NewPage = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
const save = async ()=>{
|
||||
setIsSaving(true);
|
||||
|
||||
let updatedBrew = { ...currentBrew };
|
||||
const updatedBrew = { ...currentBrew };
|
||||
splitTextStyleAndMetadata(updatedBrew);
|
||||
|
||||
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm;
|
||||
@@ -137,13 +153,13 @@ const NewPage = (props) => {
|
||||
const res = await request
|
||||
.post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`)
|
||||
.send(updatedBrew)
|
||||
.catch((err) => {
|
||||
.catch((err)=>{
|
||||
setIsSaving(false);
|
||||
setError(err);
|
||||
});
|
||||
|
||||
setIsSaving(false)
|
||||
if (!res) return;
|
||||
setIsSaving(false);
|
||||
if(!res) return;
|
||||
|
||||
const savedBrew = res.body;
|
||||
|
||||
@@ -154,15 +170,38 @@ const NewPage = (props) => {
|
||||
};
|
||||
|
||||
const renderSaveButton = ()=>{
|
||||
if(isSaving){
|
||||
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
|
||||
save...
|
||||
</Nav.item>;
|
||||
} else {
|
||||
return <Nav.item icon='fas fa-save' className='save' onClick={save}>
|
||||
save
|
||||
</Nav.item>;
|
||||
}
|
||||
// #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 = ()=>{
|
||||
@@ -170,7 +209,7 @@ const NewPage = (props) => {
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const renderNavbar = () => (
|
||||
const renderNavbar = ()=>(
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
|
||||
@@ -180,8 +219,10 @@ const NewPage = (props) => {
|
||||
{error
|
||||
? <ErrorNavItem error={error} clearError={clearError} />
|
||||
: renderSaveButton()}
|
||||
<NewBrewItem />
|
||||
<PrintNavItem />
|
||||
<HelpNavItem />
|
||||
<VaultNavItem />
|
||||
<RecentNavItem />
|
||||
<AccountNavItem />
|
||||
</Nav.section>
|
||||
@@ -189,7 +230,7 @@ const NewPage = (props) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='newPage sitePage'>
|
||||
<div className='newPage sitePage'>
|
||||
{renderNavbar()}
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={handleSplitMove}>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
.newPage {
|
||||
.navItem.save {
|
||||
.fadeInRight();
|
||||
.transition(opacity);
|
||||
background-color : @orange;
|
||||
&:hover { background-color : @green; }
|
||||
&.neverSaved {
|
||||
.fadeOutRight();
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
.vaultPage {
|
||||
height : 100%;
|
||||
overflow-y : hidden;
|
||||
background-color : #2C3E50;
|
||||
|
||||
*:not(input) { user-select : none; }
|
||||
|
||||
.form {
|
||||
background:white;
|
||||
}
|
||||
|
||||
:where(.content .dataGroup) {
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
background : white;
|
||||
|
||||
&.form .brewLookup {
|
||||
position : relative;
|
||||
@@ -171,7 +173,6 @@
|
||||
max-height : 100%;
|
||||
padding : 70px 50px;
|
||||
overflow-y : scroll;
|
||||
background-color : #2C3E50;
|
||||
container-type : inline-size;
|
||||
|
||||
h3 { font-size : 25px; }
|
||||
|
||||
@@ -26,7 +26,7 @@ const getLocalStorageMap = function(){
|
||||
|
||||
if(global?.account?.username){
|
||||
const username = global.account.username;
|
||||
localStorageMap[`HOMEBREWERY-DEFAULT-SAVE-LOCATION-${username}`] = `HB_editor_defaultSave_${username}`;
|
||||
localStorageMap[`HOMEBREWERY-DEFAULT-SAVE-LOCATION-${username}`] = `HB_editor_defaultSave_${username}`;
|
||||
}
|
||||
|
||||
return localStorageMap;
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import getLocalStorageMap from './localStorageKeyMap.js';
|
||||
|
||||
describe('getLocalStorageMap', ()=>{
|
||||
it('no username', ()=>{
|
||||
const account = global.account;
|
||||
|
||||
delete global.account;
|
||||
|
||||
const map = getLocalStorageMap();
|
||||
|
||||
global.account = account;
|
||||
|
||||
expect(map).toBeInstanceOf(Object);
|
||||
expect(Object.entries(map)).toHaveLength(16);
|
||||
});
|
||||
|
||||
it('no username', ()=>{
|
||||
const account = global.account;
|
||||
|
||||
global.account = { username: 'test' };
|
||||
|
||||
const map = getLocalStorageMap();
|
||||
|
||||
global.account = account;
|
||||
|
||||
expect(map).toBeInstanceOf(Object);
|
||||
expect(Object.entries(map)).toHaveLength(17);
|
||||
expect(map).toHaveProperty('HOMEBREWERY-DEFAULT-SAVE-LOCATION-test', 'HB_editor_defaultSave_test');
|
||||
});
|
||||
});
|
||||
@@ -4,21 +4,16 @@ const updateLocalStorage = function(){
|
||||
// Return if no window and thus no local storage
|
||||
if(typeof window === 'undefined') return;
|
||||
|
||||
// Return if the local storage key map has no content
|
||||
const localStorageKeyMap = getLocalStorageMap();
|
||||
if(Object.keys(localStorageKeyMap).length == 0) return;
|
||||
|
||||
const storage = window.localStorage;
|
||||
|
||||
const deleteKeys = storage?.getItem('HB_deleteKeys') != null;
|
||||
|
||||
Object.keys(localStorageKeyMap).forEach((key)=>{
|
||||
if(storage[key]){
|
||||
if(!storage[localStorageKeyMap[key]]){
|
||||
const data = storage.getItem(key);
|
||||
storage.setItem(localStorageKeyMap[key], data);
|
||||
};
|
||||
if(deleteKeys) storage.removeItem(key);
|
||||
storage.removeItem(key);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"development": true,
|
||||
"host" : "homebrewery.local.naturalcrit.com:8000",
|
||||
"naturalcrit_url" : "local.naturalcrit.com:8010",
|
||||
"secret" : "secret",
|
||||
|
||||
@@ -8,9 +8,9 @@ import Markdown from '../shared/naturalcrit/markdown.js';
|
||||
import yaml from 'js-yaml';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
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 { splitTextStyleAndMetadata,
|
||||
import { splitTextStyleAndMetadata,
|
||||
brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js';
|
||||
import checkClientVersion from './middleware/check-client-version.js';
|
||||
|
||||
@@ -377,14 +377,14 @@ const api = {
|
||||
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
|
||||
const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[0]);
|
||||
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];
|
||||
} catch (err) {
|
||||
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
||||
console.error('Failed to apply patches:', {
|
||||
//patches : brewFromClient.patches,
|
||||
brewId : brewFromClient.editId || 'unknown',
|
||||
error : err
|
||||
brewId : brewFromClient.editId || 'unknown',
|
||||
error : err
|
||||
});
|
||||
// While running in parallel, don't throw the error upstream.
|
||||
// throw err; // rethrow to preserve the 500 behavior
|
||||
|
||||
@@ -8,7 +8,7 @@ const brewSnippetsToJSON = (menuTitle, userBrewSnippets, themeBundleSnippets=nul
|
||||
const mpAsSnippets = [];
|
||||
// Snippets from Themes first.
|
||||
if(themeBundleSnippets) {
|
||||
for (let themes of themeBundleSnippets) {
|
||||
for (const themes of themeBundleSnippets) {
|
||||
if(typeof themes !== 'string') {
|
||||
const userSnippets = [];
|
||||
const snipSplit = themes.snippets.trim().split(textSplit).slice(1);
|
||||
@@ -76,9 +76,9 @@ const yamlSnippetsToText = (yamlObj)=>{
|
||||
if(typeof yamlObj == 'string') return yamlObj;
|
||||
|
||||
let snippetsText = '';
|
||||
|
||||
for (let snippet of yamlObj) {
|
||||
for (let subSnippet of snippet.subsnippets) {
|
||||
|
||||
for (const snippet of yamlObj) {
|
||||
for (const subSnippet of snippet.subsnippets) {
|
||||
snippetsText = `${snippetsText}\\snippet ${subSnippet.name}\n${subSnippet.gen || ''}\n`;
|
||||
}
|
||||
}
|
||||
@@ -121,7 +121,7 @@ const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{
|
||||
const res = await request
|
||||
.get(`/api/theme/${renderer}/${theme}`)
|
||||
.catch((err)=>{
|
||||
setError(err)
|
||||
setError(err);
|
||||
});
|
||||
if(!res) {
|
||||
setThemeBundle({});
|
||||
@@ -133,14 +133,14 @@ const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
|
||||
const debugTextMismatch = (clientTextRaw, serverTextRaw, label)=>{
|
||||
const clientText = clientTextRaw?.normalize('NFC') || '';
|
||||
const serverText = serverTextRaw?.normalize('NFC') || '';
|
||||
|
||||
const clientBuffer = Buffer.from(clientText, 'utf8');
|
||||
const serverBuffer = Buffer.from(serverText, 'utf8');
|
||||
|
||||
if (clientBuffer.equals(serverBuffer)) {
|
||||
if(clientBuffer.equals(serverBuffer)) {
|
||||
console.log(`✅ ${label} text matches byte-for-byte.`);
|
||||
return;
|
||||
}
|
||||
@@ -151,7 +151,7 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
|
||||
|
||||
// Byte-level diff
|
||||
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)}`);
|
||||
break;
|
||||
}
|
||||
@@ -159,14 +159,14 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
|
||||
|
||||
// Char-level diff
|
||||
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(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`);
|
||||
console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
splitTextStyleAndMetadata,
|
||||
|
||||
@@ -435,7 +435,7 @@ const replaceVar = function(input, hoist=false, allowUnresolved=false) {
|
||||
|
||||
try {
|
||||
return mathParser.evaluate(replacedLabel);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return undefined; // Return undefined if invalid math result
|
||||
}
|
||||
}
|
||||
@@ -680,7 +680,7 @@ const tableTerminators = [
|
||||
|
||||
Marked.use(MarkedVariables());
|
||||
Marked.use(MarkedDefinitionLists());
|
||||
Marked.use({ extensions : [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
||||
Marked.use({ extensions: [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
||||
Marked.use(mustacheInjectBlock);
|
||||
Marked.use(MarkedAlignedParagraphs());
|
||||
Marked.use(MarkedSubSuperText());
|
||||
|
||||
@@ -49,7 +49,7 @@ const cleanUrl = function (sanitize, base, href) {
|
||||
prot = decodeURIComponent(unescape(href))
|
||||
.replace(nonWordAndColonTest, '')
|
||||
.toLowerCase();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
|
||||
@@ -58,7 +58,7 @@ const cleanUrl = function (sanitize, base, href) {
|
||||
}
|
||||
try {
|
||||
href = encodeURI(href).replace(/%25/g, '%');
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return href;
|
||||
|
||||
@@ -611,3 +611,17 @@ h6,
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user