0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-23 18:43:02 +00:00

Compare commits

..

93 Commits

Author SHA1 Message Date
Trevor Buckner
63bebe1efd Lint everything
Catching up on a bunch of linting so random changes stop showing up on PRs when the linter is run.
2025-10-06 00:02:24 -04:00
Trevor Buckner
22e26d635a Merge pull request #4460 from naturalcrit/cleanupLocalStorageKeysTest
Clean up localStorageMap code
2025-10-05 23:28:34 -04:00
Trevor Buckner
643e0ac650 small cleanups of localstorage keys code 2025-10-05 23:24:50 -04:00
Trevor Buckner
5395412ac5 Remove tests for getLocalStorageMap()
The function is a simple getter with trivial logic; test is effectively just asserting the size of the map, which coverage adds no meaningful value and adds cruft to the codebase.
2025-10-05 23:24:35 -04:00
Trevor Buckner
dc4610ea1b Merge pull request #4447 from dbolack-ab/issue_3426
Applies G-Ambatte's fix for Firefox browser lock.
2025-10-05 22:31:15 -04:00
Trevor Buckner
1e71e9e18a Use blockquote and table elements, not .classes 2025-10-05 22:19:43 -04:00
Trevor Buckner
4203e90d09 Merge branch 'master' into issue_3426 2025-10-05 22:09:06 -04:00
Trevor Buckner
dc94555c94 Merge pull request #4458 from naturalcrit/new/edit/home_commonSaveButton
Make the renderSaveButton() function common between edit/new/home
2025-10-05 22:03:32 -04:00
Trevor Buckner
41aebf084b Make the renderSaveButton() function common between edit/new/home
Each of the edit/home/new pages renders its save button differently. This makes it a common function with all the same possible render states (does the document have unsaved changes? Is it already saved? Was it auto-saved?).

- Common save button
- Adds the "save" button to /home page which wasn't there before
- Animates the "save" button in /home and /new when the user makes their first change to signal that yes, you do have to actually click the save button if you want to keep this.
- "reminder... you haven't saved for X minutes" still not functional on /new and /home since that involves more moving pieces.
2025-10-05 21:57:19 -04:00
David Bolack
74e17e154f Merge branch 'issue_3426' of github.com:dbolack-ab/homebrewery into issue_3426 2025-10-05 20:12:32 -05:00
Trevor Buckner
a944b23ca0 Merge pull request #4457 from naturalcrit/new/home/edit/_unsavedChanges_common
Make `unsavedChanges` state common
2025-10-05 20:09:14 -04:00
Trevor Buckner
12052853db Merge branch 'master' into new/home/edit/_unsavedChanges_common 2025-10-05 20:07:56 -04:00
Trevor Buckner
c0f67bef5a Merge pull request #4434 from naturalcrit/fix-red-background
Fix dev background
2025-10-05 19:51:01 -04:00
David Bolack
8f715a6615 Isolate change to Firefox 2025-10-05 18:36:14 -05:00
Víctor Losada Hernández
1f51abaf10 this makes more sense 2025-10-05 19:57:49 +02:00
Víctor Losada Hernández
c90a8c53a5 lets test this 2025-10-05 19:56:50 +02:00
Víctor Losada Hernández
ac18f4bd1d Merge branch 'master' of https://github.com/naturalcrit/homebrewery into fix-red-background 2025-10-05 19:43:29 +02:00
Víctor Losada Hernández
7393aef806 set up development config variavle 2025-10-05 19:42:01 +02:00
Trevor Buckner
2c4c4b8f92 Make unsavedChanges state common
/editPage.jsx uses `unsavedChanges` state to detect when autosave should fire, or unsaved changes warning should display.

/homePage.jsx uses a similar check (different variables) to detect when to show the popup "save now"! button

/newPage.jsx doesn't do any of this, but probably should pop up a warning when saving hasn't happened for a long time

This commit just gives all of the pages the same common `unsavedChanges` state, calculated in the same way, and updates any sections that depend on that updated state.

This is precursor work to adding "unsaved changes" warnings to all three pages.
2025-10-04 22:17:24 -04:00
Trevor Buckner
c751d647d9 Merge pull request #4440 from naturalcrit/UnifyNewHomeEdit-Structure&Naming
Clean Up Common features of new/home/edit
2025-10-04 21:52:28 -04:00
Trevor Buckner
6057b35d19 Merge branch 'master' into UnifyNewHomeEdit-Structure&Naming 2025-10-04 21:48:10 -04:00
Trevor Buckner
521d42f32f Merge pull request #4455 from G-Ambatte/enableOldKeyDeletion
Permanently enable old local storage key deletion
2025-10-04 21:47:21 -04:00
Trevor Buckner
e9f8302597 Merge branch 'master' into enableOldKeyDeletion 2025-10-04 21:47:06 -04:00
Trevor Buckner
f429b1755d Merge pull request #4456 from naturalcrit/RemoveHandlePageChangeWrappers
Remove handler function for cursor/renderer page. Use setState directly
2025-10-04 21:46:32 -04:00
Trevor Buckner
20e12ebcb5 Remove handler function for cursor/renderer page. Use setState directly 2025-10-04 21:39:24 -04:00
G.Ambatte
ae51213c8c Permanently enable old local storage key deletion 2025-10-05 12:25:51 +13:00
Trevor Buckner
8f7ae35f08 Merge branch 'master' into issue_3426 2025-10-04 18:32:15 -04:00
Trevor Buckner
44023f390c Merge pull request #4453 from G-Ambatte/fixDockerInstructionsForWindows-#4443
Add Windows-specific instructions to Docker README
2025-10-04 18:02:47 -04:00
Trevor Buckner
48b95712e2 Merge branch 'master' into fixDockerInstructionsForWindows-#4443 2025-10-04 18:01:58 -04:00
Trevor Buckner
16c28e16ce Merge pull request #4452 from G-Ambatte/standardizeLocalStorageKeyNames-#4119
Standardize local storage key names #4119
2025-10-04 17:54:10 -04:00
G.Ambatte
379b518c6b Merge branch 'master' into fixDockerInstructionsForWindows-#4443 2025-10-04 15:40:03 +13:00
G.Ambatte
962dcbdbf6 Update Docker instructions 2025-10-04 15:36:14 +13:00
G.Ambatte
400fa250ee Move key deletion out of key update check 2025-10-04 15:19:31 +13:00
G.Ambatte
e82921f81a Add key to activate deletion for testing 2025-10-04 14:54:57 +13:00
G.Ambatte
18367526bd Merge branch 'master' into standardizeLocalStorageKeyNames-#4119 2025-10-04 13:07:51 +13:00
David Bolack
f0bb06e706 Merge branch 'master' into issue_3426 2025-10-03 18:52:56 -05:00
Víctor Losada Hernández
aff9a85769 end of file character shit 2025-10-03 21:38:43 +02:00
Víctor Losada Hernández
e0379a0baa last cleanup 2025-10-03 21:38:10 +02:00
Víctor Losada Hernández
e8a0681015 Merge branch 'master' of https://github.com/naturalcrit/homebrewery into fix-red-background 2025-10-03 21:37:06 +02:00
Víctor Losada Hernández
3ed61ebe2c Merge branch 'fix-red-background' of https://github.com/naturalcrit/homebrewery into fix-red-background 2025-10-03 21:32:55 +02:00
Víctor Losada Hernández
c2e51b0baa removing isclient check to see what's what 2025-10-03 21:32:52 +02:00
Trevor Buckner
bc258f5239 Merge branch 'master' into UnifyNewHomeEdit-Structure&Naming 2025-10-02 22:02:53 -04:00
Trevor Buckner
e64fc83ea6 Merge pull request #4436 from G-Ambatte/addMongoIndexes
Add indexes to Mongo schema
2025-10-02 20:48:32 -04:00
G.Ambatte
ee6d2ac35d Merge branch 'master' into addMongoIndexes 2025-10-03 13:42:25 +13:00
G.Ambatte
f22f7196ca Update README.md 2025-10-03 13:41:53 +13:00
Trevor Buckner
ba23763294 Merge branch 'master' into UnifyNewHomeEdit-Structure&Naming 2025-10-02 19:45:42 -04:00
Trevor Buckner
7f832a55db Merge pull request #4439 from naturalcrit/CommonHandleBrewChange
Combine handle<Text/Style/Snippet/Meta>Change() into one common function
2025-10-02 19:39:35 -04:00
Trevor Buckner
51b91567f6 Merge branch 'master' into fix-red-background 2025-10-02 18:39:31 -04:00
Víctor Losada Hernández
eefda9fe45 simplifying per suggestion 2025-10-02 12:40:12 +02:00
G.Ambatte
24db8f85ac Merge branch 'standardizeLocalStorageKeyNames-#4119' of https://github.com/G-Ambatte/homebrewery into standardizeLocalStorageKeyNames-#4119 2025-10-02 18:30:07 +13:00
G.Ambatte
82a8db129e Merge branch 'naturalcrit:master' into addMongoIndexes 2025-10-02 10:54:21 +13:00
Víctor Losada Hernández
e793db7b37 separating the words to make it less ugly 2025-10-01 22:55:32 +02:00
Víctor Losada Hernández
ff5450ad8c Merge branch 'master' of https://github.com/naturalcrit/homebrewery into fix-red-background 2025-09-29 22:28:12 +02:00
Trevor Buckner
07495b0dea Make Print/Vault/New common nav buttons to all pages 2025-09-29 12:48:25 -04:00
G.Ambatte
718dba3e4a Merge branch 'master' into standardizeLocalStorageKeyNames-#4119 2025-09-29 23:14:14 +13:00
Trevor Buckner
c6ed67db08 Merge branch 'master' into UnifyNewHomeEdit-Structure&Naming 2025-09-26 22:56:15 -04:00
David Bolack
c50c279ef3 Merge branch 'issue_3426' of github.com:dbolack-ab/homebrewery into issue_3426 2025-09-22 20:36:45 -05:00
David Bolack
cc246fb31a Merge branch 'master' into issue_3426 2025-09-22 20:36:09 -05:00
Trevor Buckner
788324fe31 Merge handText/Style/Snip/MetaChange into handleBrewChange 2025-09-11 22:03:25 -04:00
Trevor Buckner
da8772daa7 Use setXXXPageNum instead of handleXXXPageNum
No need for separate wrapper functions when we can just pass the setState functions directly.
2025-09-11 16:14:45 -04:00
G.Ambatte
87a36bb02d Add tests for localStorageKeyMap.js 2025-09-11 21:57:00 +12:00
G.Ambatte
1459f6a320 Tweak local storage update logic 2025-09-11 21:25:13 +12:00
G.Ambatte
a11fa72261 Change JSON file to JS getter function 2025-09-11 21:02:56 +12:00
G.Ambatte
2663d86627 Don't update storage values if key already exists 2025-09-10 20:31:50 +12:00
G.Ambatte
8d4ea7cfd8 Update listPage storage keys 2025-09-10 20:20:42 +12:00
G.Ambatte
b6818e963b Remove unused dismiss keys 2025-09-10 20:12:50 +12:00
G.Ambatte
dc1bc471aa Update SplitPane keys 2025-09-10 20:10:18 +12:00
G.Ambatte
5504c1b96b Update Autosave key 2025-09-10 20:00:53 +12:00
G.Ambatte
fd370c777d Update Editor theme key 2025-09-10 19:57:16 +12:00
G.Ambatte
58277585e1 Update Recent Items keys 2025-09-10 19:55:42 +12:00
G.Ambatte
885c0105f3 Update adminPage key 2025-09-10 19:36:39 +12:00
G.Ambatte
52486495c8 Update adminPage storage keys 2025-09-10 19:35:14 +12:00
G.Ambatte
328e071268 Update BrewRenderer toolbar keys 2025-09-10 19:22:54 +12:00
G.Ambatte
088ca9971c Update accountPage keys 2025-09-10 19:16:29 +12:00
G.Ambatte
c99f59d42b Update newPage.jsx keys 2025-09-10 19:08:47 +12:00
G.Ambatte
cb3eb77c61 Merge branch 'standardizeLocalStorageKeyNames-#4119' of https://github.com/G-Ambatte/homebrewery into standardizeLocalStorageKeyNames-#4119 2025-09-10 18:54:00 +12:00
G.Ambatte
7163b1a287 Add function to add dynamic keys to local storage map 2025-09-10 18:53:53 +12:00
G.Ambatte
08d228831d Add missing keys to JSON, tweak layout 2025-09-10 18:53:23 +12:00
G.Ambatte
ad8bb34c93 Merge branch 'master' into standardizeLocalStorageKeyNames-#4119 2025-09-10 16:58:16 +12:00
G.Ambatte
1eb5b6d3a4 Copy exisitng key data to new keys 2025-09-09 22:04:40 +12:00
G.Ambatte
6c3a5f193d Merge branch 'master' into addMongoIndexes 2025-08-25 17:24:27 +12:00
Víctor Losada Hernández
4c5eef46a0 Merge branch 'master' into issue_3426 2025-08-21 16:33:19 +02:00
David Bolack
a1ab27b57f Applies G-Ambatte's fix 2025-07-30 19:47:59 -05:00
G.Ambatte
4d014bf379 Merge branch 'master' into addMongoIndexes 2025-07-20 14:06:53 +12:00
G.Ambatte
4856c803ed Use local_environments from config 2025-07-20 11:48:22 +12:00
G.Ambatte
d9cd270f3b Add autobuild to Mongoose connection (local/docker ONLY) 2025-07-20 11:43:14 +12:00
G.Ambatte
878ea1449d Add indexes to HomebrewSchema 2025-07-20 11:42:24 +12:00
Víctor Losada Hernández
d6a5a1f03c no idea what changed but now it works 2025-07-18 00:39:36 +02:00
Víctor Losada Hernández
f04d6cdd1f fix to current 2025-07-17 23:32:18 +02:00
Víctor Losada Hernández
4fd61ce92c Merge branch 'master' of https://github.com/naturalcrit/homebrewery into fix-red-background 2025-07-17 23:30:01 +02:00
Víctor Losada Hernández
88b70d340e final bit 2025-05-27 11:27:04 +02:00
Víctor Losada Hernández
ed05d8c754 move all to homebrew.jsx 2025-05-27 11:25:01 +02:00
Víctor Losada Hernández
077aaeb815 log 2025-05-27 10:54:07 +02:00
33 changed files with 469 additions and 260 deletions

View File

@@ -49,7 +49,7 @@ Make an changes you need to `config/docker.json` then build the image. If it doe
"web_port" : 8000,
"enable_v3" : true,
"mongodb_uri": "mongodb://172.17.0.2/homebrewery",
"enable_themes" : true,
"enable_themes" : true
}
```
@@ -90,6 +90,13 @@ 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
```
**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
When Homebrewery code updates, your docker container will not automatically follow the changes. To do so you will need to rebuild your homebrewery image.
@@ -117,3 +124,9 @@ 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
```
**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,8 +75,9 @@ it using the two commands:
1. `npm install`
1. `npm start`
You should now be able to go to [http://localhost:8000](http://localhost:8000)
in your browser and use The Homebrewery offline.
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.
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:
- [Course](https://learn.mongodb.com/courses/m103-basic-cluster-administration) on cluster administration, useful for beginners
@@ -145,3 +146,4 @@ 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
[gitter-url]: https://gitter.im/naturalcrit/Lobby

View File

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

View File

@@ -2,7 +2,8 @@ require('./splitPane.less');
const React = require('react');
const { useState, useEffect } = React;
const storageKey = 'naturalcrit-pane-split';
const PANE_WIDTH_KEY = 'HB_editor_splitWidth';
const LIVE_SCROLL_KEY = 'HB_editor_liveScroll';
const SplitPane = (props)=>{
const {
@@ -18,9 +19,9 @@ const SplitPane = (props)=>{
const [liveScroll, setLiveScroll] = useState(false);
useEffect(()=>{
const savedPos = window.localStorage.getItem(storageKey);
const savedPos = window.localStorage.getItem(PANE_WIDTH_KEY);
setDividerPos(savedPos ? limitPosition(savedPos, 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)) : window.innerWidth / 2);
setLiveScroll(window.localStorage.getItem('liveScroll') === 'true');
setLiveScroll(window.localStorage.getItem(LIVE_SCROLL_KEY) === 'true');
window.addEventListener('resize', handleResize);
return ()=>window.removeEventListener('resize', handleResize);
@@ -29,13 +30,13 @@ const SplitPane = (props)=>{
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
const handleResize = ()=>setDividerPos(limitPosition(window.localStorage.getItem(storageKey), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)));
const handleResize = ()=>setDividerPos(limitPosition(window.localStorage.getItem(PANE_WIDTH_KEY), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)));
const handleUp =(e)=>{
e.preventDefault();
if(isDragging) {
onDragFinish(dividerPos);
window.localStorage.setItem(storageKey, dividerPos);
window.localStorage.setItem(PANE_WIDTH_KEY, dividerPos);
}
setIsDragging(false);
};
@@ -52,7 +53,7 @@ const SplitPane = (props)=>{
};
const liveScrollToggle = ()=>{
window.localStorage.setItem('liveScroll', String(!liveScroll));
window.localStorage.setItem(LIVE_SCROLL_KEY, String(!liveScroll));
setLiveScroll(!liveScroll);
};

View File

@@ -24,6 +24,8 @@ const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m;
const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m;
const PAGE_HEIGHT = 1056;
const TOOLBAR_STATE_KEY = 'HB_renderer_toolbarState';
const INITIAL_CONTENT = dedent`
<!DOCTYPE html><html><head>
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
@@ -122,7 +124,7 @@ const BrewRenderer = (props)=>{
//useEffect to store or gather toolbar state from storage
useEffect(()=>{
const toolbarState = JSON.parse(window.localStorage.getItem('hb_toolbarState'));
const toolbarState = JSON.parse(window.localStorage.getItem(TOOLBAR_STATE_KEY));
toolbarState && setDisplayOptions(toolbarState);
}, []);
@@ -284,7 +286,7 @@ const BrewRenderer = (props)=>{
const handleDisplayOptionsChange = (newDisplayOptions)=>{
setDisplayOptions(newDisplayOptions);
localStorage.setItem('hb_toolbarState', JSON.stringify(newDisplayOptions));
localStorage.setItem(TOOLBAR_STATE_KEY, JSON.stringify(newDisplayOptions));
};
const pagesStyle = {
@@ -293,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]);
@@ -327,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 */}

View File

@@ -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;

View File

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

View File

@@ -10,7 +10,7 @@ const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
const SnippetBar = require('./snippetbar/snippetbar.jsx');
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
const EDITOR_THEME_KEY = 'HB_editor_theme';
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
@@ -140,7 +140,7 @@ const Editor = createClass({
handleViewChange : function(newView){
this.props.setMoveArrows(newView === 'text');
this.setState({
view : newView
}, ()=>{

View File

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

View File

@@ -4,6 +4,8 @@ import './homebrew.less';
import React from 'react';
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 EditPage from './pages/editPage/editPage.jsx';
import UserPage from './pages/userPage/userPage.jsx';
@@ -17,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} />;
};
@@ -48,9 +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} />} />

View File

@@ -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;

View File

@@ -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'>

View File

@@ -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);

View File

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

View 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

View File

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

View File

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

View File

@@ -1,48 +1,53 @@
/* 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;
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 BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
const SNIPKEY = 'homebrewery-new-snippets';
const METAKEY = 'homebrewery-new-meta';
const AUTOSAVE_KEY = 'HB_editor_autoSaveOn';
const BREWKEY = 'HB_newPage_content';
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 = {
@@ -74,10 +79,8 @@ 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_ON') ?? true);
const autoSavePref = JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true);
setAutoSaveEnabled(autoSavePref);
setWarnUnsavedChanges(!autoSavePref);
setHTMLErrors(Markdown.validate(currentBrew.text));
@@ -120,28 +123,16 @@ const EditPage = (props)=>{
editorRef.current?.update();
};
const handleEditorViewPageChange = (pageNumber)=>{
setCurrentEditorViewPageNum(pageNumber);
};
const handleEditorCursorPageChange = (pageNumber)=>{
setCurrentEditorCursorPageNum(pageNumber);
};
const handleBrewRendererPageChange = (pageNumber)=>{
setCurrentBrewRendererPageNum(pageNumber);
};
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);
@@ -318,20 +309,24 @@ 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 = ()=>{
clearTimeout(warnUnsavedTimeout.current);
clearTimeout(saveTimeout.current);
localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled));
localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(!autoSaveEnabled));
setAutoSaveEnabled(!autoSaveEnabled);
setWarnUnsavedChanges(autoSaveEnabled);
};
@@ -361,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>
@@ -391,8 +386,8 @@ const EditPage = (props)=>{
userThemes={props.userThemes}
themeBundle={themeBundle}
updateBrew={updateBrew}
onCursorPageChange={handleEditorCursorPageChange}
onViewPageChange={handleEditorViewPageChange}
onCursorPageChange={setCurrentEditorCursorPageNum}
onViewPageChange={setCurrentEditorViewPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
@@ -405,7 +400,7 @@ const EditPage = (props)=>{
themeBundle={themeBundle}
errors={HTMLErrors}
lang={currentBrew.lang}
onPageChange={handleBrewRendererPageChange}
onPageChange={setCurrentBrewRendererPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}

View File

@@ -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,32 +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 handleEditorViewPageChange = (pageNumber)=>{
setCurrentEditorViewPageNum(pageNumber);
};
const handleEditorCursorPageChange = (pageNumber)=>{
setCurrentEditorCursorPageNum(pageNumber);
};
const handleBrewRendererPageChange = (pageNumber)=>{
setCurrentBrewRendererPageNum(pageNumber);
};
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);
@@ -104,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);
@@ -112,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 />
@@ -138,8 +192,8 @@ const HomePage =(props)=>{
renderer={currentBrew.renderer}
showEditButtons={false}
themeBundle={themeBundle}
onCursorPageChange={handleEditorCursorPageChange}
onViewPageChange={handleEditorViewPageChange}
onCursorPageChange={setCurrentEditorCursorPageNum}
onViewPageChange={setCurrentEditorViewPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
@@ -148,7 +202,7 @@ const HomePage =(props)=>{
text={currentBrew.text}
style={currentBrew.style}
renderer={currentBrew.renderer}
onPageChange={handleBrewRendererPageChange}
onPageChange={setCurrentBrewRendererPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
@@ -156,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>
@@ -164,7 +218,7 @@ const HomePage =(props)=>{
Create your own <i className='fas fa-magic' />
</a>
</div>
)
);
};
module.exports = HomePage;

View File

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

View File

@@ -1,34 +1,44 @@
/*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';
const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
const SNIPKEY = 'homebrewery-new-snippets';
const METAKEY = 'homebrewery-new-meta';
const SAVEKEYPREFIX = 'HOMEBREWERY-DEFAULT-SAVE-LOCATION-';
// Page specific imports
import { Meta } from 'vitreum/headtags';
const NewPage = (props) => {
const BREWKEY = 'HB_newPage_content';
const STYLEKEY = 'HB_newPage_style';
const METAKEY = 'HB_newPage_metadata';
const SNIPKEY = 'HB_newPage_snippets';
const SAVEKEYPREFIX = 'HB_editor_defaultSave_';
const useLocalStorage = true;
const neverSaved = true;
const NewPage = (props)=>{
props = {
brew: DEFAULT_BREW,
brew : DEFAULT_BREW,
...props
};
@@ -41,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);
};
}, []);
@@ -74,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);
@@ -84,44 +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 handleEditorViewPageChange = (pageNumber)=>{
setCurrentEditorViewPageNum(pageNumber);
};
const handleEditorCursorPageChange = (pageNumber)=>{
setCurrentEditorCursorPageNum(pageNumber);
};
const handleBrewRendererPageChange = (pageNumber)=>{
setCurrentBrewRendererPageNum(pageNumber);
};
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);
@@ -135,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;
@@ -147,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;
@@ -164,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 = ()=>{
@@ -180,7 +209,7 @@ const NewPage = (props) => {
setIsSaving(false);
};
const renderNavbar = () => (
const renderNavbar = ()=>(
<Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
@@ -190,8 +219,10 @@ const NewPage = (props) => {
{error
? <ErrorNavItem error={error} clearError={clearError} />
: renderSaveButton()}
<NewBrewItem />
<PrintNavItem />
<HelpNavItem />
<VaultNavItem />
<RecentNavItem />
<AccountNavItem />
</Nav.section>
@@ -199,7 +230,7 @@ const NewPage = (props) => {
);
return (
<div className='newPage sitePage'>
<div className='newPage sitePage'>
{renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={handleSplitMove}>
@@ -210,8 +241,8 @@ const NewPage = (props) => {
renderer={currentBrew.renderer}
userThemes={props.userThemes}
themeBundle={themeBundle}
onCursorPageChange={handleEditorCursorPageChange}
onViewPageChange={handleEditorViewPageChange}
onCursorPageChange={setCurrentEditorCursorPageNum}
onViewPageChange={setCurrentEditorViewPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
@@ -224,7 +255,7 @@ const NewPage = (props) => {
themeBundle={themeBundle}
errors={HTMLErrors}
lang={currentBrew.lang}
onPageChange={handleBrewRendererPageChange}
onPageChange={setCurrentBrewRendererPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}

View File

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

View File

@@ -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; }

View File

@@ -0,0 +1,35 @@
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

@@ -0,0 +1,22 @@
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,4 +1,5 @@
{
"development": true,
"host" : "homebrewery.local.naturalcrit.com:8000",
"naturalcrit_url" : "local.naturalcrit.com:8010",
"secret" : "secret",

View File

@@ -27,7 +27,10 @@ const disconnect = async ()=>{
};
const connect = async (config)=>{
return await Mongoose.connect(getMongoDBURL(config), { retryWrites: false })
return await Mongoose.connect(getMongoDBURL(config), {
retryWrites : false,
autoIndex : (config.get('local_environments').includes(config.get('node_env')))
})
.catch((error)=>handleConnectionError(error));
};

View File

@@ -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

View File

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

View File

@@ -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,

View File

@@ -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());

View File

@@ -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;

View File

@@ -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;
}
}
}