0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-25 11:53:02 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Trevor Buckner
698d60e0f2 Adds routes to crawl google users for unsubbed brews 2025-07-28 13:28:48 -04:00
151 changed files with 7086 additions and 9676 deletions

View File

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

View File

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

View File

@@ -83,96 +83,11 @@ pre {
.page .exampleTable td,th { .page .exampleTable td,th {
border:1px dashed #00000030; border:1px dashed #00000030;
} }
.page .df {
font-size: 2em;
vertical-align: middle;
}
``` ```
## changelog ## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery). For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
### Friday 1/11/2026 - v3.20.1
{{taskList
##### calculuschild
* [x] Add D100 "ball" dice icons `:d100:` :df_d100_05:
##### G-Ambatte
* [x] Fix transparent edge on back cover image
Fixes issue [#4551](https://github.com/naturalcrit/homebrewery/issues/4551)
* [x] Fix "Out of sync" error when document contains extended unicode characters
Fixes issue [#4583](https://github.com/naturalcrit/homebrewery/issues/4583)
##### 5e-Cleric
* [x] Fix page count error on Vault
* [x] Fix cover page footnote set to all-caps
Fixes issue [#4559](https://github.com/naturalcrit/homebrewery/issues/4559)
}}
### Friday 11/14/2025 - v3.20.0
{{taskList
##### calculuschild
* [x] Cleanup and removal of redundant code (home/new/edit pages)
* [x] Compress brew payloads and when saving to speed up saving and reduce bandwidth usage
##### Gazook89
* [x] Cleanup (reorganizing files and folders)
##### G-Ambatte
* [x] Fix default save location failing on new documents
Fixes issue [#4437](https://github.com/naturalcrit/homebrewery/issues/3175)
* [x] Fix usernames with special symbols unable to open userpage
Fixes issue [#807](https://github.com/naturalcrit/homebrewery/issues/807)
* [x] Cleanup (tests, documentation, localstorage key names)
Fixes issues [#4119](https://github.com/naturalcrit/homebrewery/issues/4119), [#4443](https://github.com/naturalcrit/homebrewery/issues/4443), [#4454](https://github.com/naturalcrit/homebrewery/issues/4454)
* [x] Added indexes to Mongo schema for speed improvements on local installs
##### 5e-Cleric
* [x] Better handling for errors during brew deletion
* [x] Improve file import error messaging
* [x] Tweaked editor visuals (scrollbar, dev environment, cursor type)
Fixes issue [#2689](https://github.com/naturalcrit/homebrewery/issues/2689)
* [x] Add support for math symbols in curly blocks for CSS `calc()`
Fixes issue [#3175](https://github.com/naturalcrit/homebrewery/issues/3175)
##### abquintic
* [x] Added new {{openSans **:fas_copyright: LICENSE**}} snippets (DriveThruRPG, AELF, GNU, WotC, Mongoose, etc.) with logos
* [x] Added d100 dice icons via `:icon_name:` syntax
* [x] Fix Firefox crash from very long blockquotes
Fixes issue [#3426](https://github.com/naturalcrit/homebrewery/issues/3426)
* [x] Allow more symbol characters in CSS vars inside curly blocks
Fixes issue [#4201](https://github.com/naturalcrit/homebrewery/issues/4201)
* [x] Remove duplicate columnbreak at end of page
Fixes issue [#4401](https://github.com/naturalcrit/homebrewery/issues/4401)
* [x] Fix editor losing scroll position after page resize
Fixes issue [#2963](https://github.com/naturalcrit/homebrewery/issues/2963)
##### Emmanuel Ferdman (first contribution!)
* [x] Fixed edge case crash on admin page
}}
\page
### Wednesday 7/09/2025 - v3.19.3 ### Wednesday 7/09/2025 - v3.19.3
{{taskList {{taskList

View File

@@ -1,23 +1,21 @@
import './admin.less'; import './admin.less';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import BrewUtils from './brewUtils/brewUtils.jsx'; const BrewUtils = require('./brewUtils/brewUtils.jsx');
import NotificationUtils from './notificationUtils/notificationUtils.jsx'; const NotificationUtils = require('./notificationUtils/notificationUtils.jsx');
import AuthorUtils from './authorUtils/authorUtils.jsx'; import AuthorUtils from './authorUtils/authorUtils.jsx';
import LockTools from './lockTools/lockTools.jsx'; import LockTools from './lockTools/lockTools.jsx';
const tabGroups = ['brew', 'notifications', 'authors', 'locks']; const tabGroups = ['brew', 'notifications', 'authors', 'locks'];
const ADMIN_TAB = 'HB_adminPage_currentTab';
const Admin = ()=>{ const Admin = ()=>{
const [currentTab, setCurrentTab] = useState(''); const [currentTab, setCurrentTab] = useState('');
useEffect(()=>{ useEffect(()=>{
setCurrentTab(localStorage.getItem(ADMIN_TAB) || 'brew'); setCurrentTab(localStorage.getItem('hbAdminTab') || 'brew');
}, []); }, []);
useEffect(()=>{ useEffect(()=>{
localStorage.setItem(ADMIN_TAB, currentTab); localStorage.setItem('hbAdminTab', currentTab);
}, [currentTab]); }, [currentTab]);
return ( return (

View File

@@ -84,4 +84,4 @@ const authorLookup = ()=>{
); );
}; };
export default authorLookup; module.exports = authorLookup;

View File

@@ -10,4 +10,4 @@ const authorUtils = ()=>{
); );
}; };
export default authorUtils; module.exports = authorUtils;

View File

@@ -1,8 +1,9 @@
import React from 'react'; const React = require('react');
import createReactClass from 'create-react-class'; const createClass = require('create-react-class');
import request from 'superagent';
const BrewCleanup = createReactClass({ const request = require('superagent');
const BrewCleanup = createClass({
displayName : 'BrewCleanup', displayName : 'BrewCleanup',
getDefaultProps(){ getDefaultProps(){
return {}; return {};
@@ -68,4 +69,4 @@ const BrewCleanup = createReactClass({
} }
}); });
export default BrewCleanup; module.exports = BrewCleanup;

View File

@@ -1,8 +1,8 @@
import React from 'react'; const React = require('react');
import createReactClass from 'create-react-class'; const createClass = require('create-react-class');
import request from 'superagent'; const request = require('superagent');
const BrewCompress = createReactClass({ const BrewCompress = createClass({
displayName : 'BrewCompress', displayName : 'BrewCompress',
getDefaultProps(){ getDefaultProps(){
return {}; return {};
@@ -85,4 +85,4 @@ const BrewCompress = createReactClass({
} }
}); });
export default BrewCompress; module.exports = BrewCompress;

View File

@@ -1,11 +1,12 @@
import React from 'react'; const React = require('react');
import createReactClass from 'create-react-class'; const createClass = require('create-react-class');
import request from 'superagent'; const cx = require('classnames');
import cx from 'classnames';
import Moment from 'moment'; const request = require('superagent');
const Moment = require('moment');
const BrewLookup = createReactClass({
const BrewLookup = createClass({
getDefaultProps() { getDefaultProps() {
return {}; return {};
}, },
@@ -109,4 +110,4 @@ const BrewLookup = createReactClass({
} }
}); });
export default BrewLookup; module.exports = BrewLookup;

View File

@@ -1,14 +1,15 @@
import React from 'react'; const React = require('react');
import './brewUtils.less'; const createClass = require('create-react-class');
require('./brewUtils.less');
import BrewCleanup from './brewCleanup/brewCleanup.jsx'; const BrewCleanup = require('./brewCleanup/brewCleanup.jsx');
import BrewLookup from './brewLookup/brewLookup.jsx'; const BrewLookup = require('./brewLookup/brewLookup.jsx');
import BrewCompress from './brewCompress/brewCompress.jsx'; const BrewCompress = require ('./brewCompress/brewCompress.jsx');
import Stats from './stats/stats.jsx'; const Stats = require('./stats/stats.jsx');
const BrewUtils = ()=>{ const BrewUtils = createClass({
return ( render : function(){
<> return <>
<Stats /> <Stats />
<hr /> <hr />
<BrewLookup /> <BrewLookup />
@@ -16,7 +17,8 @@ const BrewUtils = ()=>{
<BrewCleanup /> <BrewCleanup />
<hr /> <hr />
<BrewCompress /> <BrewCompress />
</> </>;
); }
}; });
export default BrewUtils;
module.exports = BrewUtils;

View File

@@ -1,8 +1,9 @@
import React from 'react'; const React = require('react');
import createReactClass from 'create-react-class'; const createClass = require('create-react-class');
import request from 'superagent';
const Stats = createReactClass({ const request = require('superagent');
const Stats = createClass({
displayName : 'Stats', displayName : 'Stats',
getDefaultProps(){ getDefaultProps(){
return {}; return {};
@@ -42,4 +43,4 @@ const Stats = createReactClass({
} }
}); });
export default Stats; module.exports = Stats;

View File

@@ -1,11 +1,11 @@
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
import './lockTools.less'; require('./lockTools.less');
import React from 'react'; const React = require('react');
import createReactClass from 'create-react-class'; const createClass = require('create-react-class');
import request from '../../homebrew/utils/request-middleware.js'; import request from '../../homebrew/utils/request-middleware.js';
const LockTools = createReactClass({ const LockTools = createClass({
displayName : 'LockTools', displayName : 'LockTools',
getInitialState : function() { getInitialState : function() {
return { return {
@@ -55,7 +55,7 @@ const LockTools = createReactClass({
} }
}); });
const LockBrew = createReactClass({ const LockBrew = createClass({
displayName : 'LockBrew', displayName : 'LockBrew',
getInitialState : function() { getInitialState : function() {
// Default values // Default values
@@ -183,7 +183,7 @@ const LockBrew = createReactClass({
} }
}); });
const LockTable = createReactClass({ const LockTable = createClass({
displayName : 'LockTable', displayName : 'LockTable',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -273,7 +273,7 @@ const LockTable = createReactClass({
} }
}); });
const LockLookup = createReactClass({ const LockLookup = createClass({
displayName : 'LockLookup', displayName : 'LockLookup',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -339,4 +339,4 @@ const LockLookup = createReactClass({
} }
}); });
export default LockTools; module.exports = LockTools;

View File

@@ -1,6 +1,7 @@
import './notificationAdd.less'; require('./notificationAdd.less');
import React, { useState, useRef } from 'react'; const React = require('react');
import request from 'superagent'; const { useState, useRef } = require('react');
const request = require('superagent');
const NotificationAdd = ()=>{ const NotificationAdd = ()=>{
const [notificationResult, setNotificationResult] = useState(null); const [notificationResult, setNotificationResult] = useState(null);
@@ -105,4 +106,4 @@ const NotificationAdd = ()=>{
); );
}; };
export default NotificationAdd; module.exports = NotificationAdd;

View File

@@ -1,7 +1,9 @@
import './notificationLookup.less'; require('./notificationLookup.less');
import React, { useState } from 'react';
import request from 'superagent'; const React = require('react');
import Moment from 'moment'; const { useState } = require('react');
const request = require('superagent');
const Moment = require('moment');
const NotificationDetail = ({ notification, onDelete })=>( const NotificationDetail = ({ notification, onDelete })=>(
<> <>
@@ -100,4 +102,4 @@ const NotificationLookup = ()=>{
); );
}; };
export default NotificationLookup; module.exports = NotificationLookup;

View File

@@ -1,6 +1,7 @@
import React from 'react'; const React = require('react');
import NotificationLookup from './notificationLookup/notificationLookup.jsx';
import NotificationAdd from './notificationAdd/notificationAdd.jsx'; const NotificationLookup = require('./notificationLookup/notificationLookup.jsx');
const NotificationAdd = require('./notificationAdd/notificationAdd.jsx');
const NotificationUtils = ()=>{ const NotificationUtils = ()=>{
return ( return (
@@ -11,4 +12,4 @@ const NotificationUtils = ()=>{
); );
}; };
export default NotificationUtils; module.exports = NotificationUtils;

View File

@@ -1,9 +1,9 @@
import React from 'react'; const React = require('react');
import createReactClass from 'create-react-class'; const createClass = require('create-react-class');
import _ from 'lodash'; const _ = require('lodash');
import './combobox.less'; require('./combobox.less');
const Combobox = createReactClass({ const Combobox = createClass({
displayName : 'Combobox', displayName : 'Combobox',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -126,4 +126,4 @@ const Combobox = createReactClass({
} }
}); });
export default Combobox; module.exports = Combobox;

View File

@@ -1,19 +1,20 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
import './brewRenderer.less'; require('./brewRenderer.less');
import React, { useState, useRef, useMemo, useEffect } from 'react'; const React = require('react');
import _ from 'lodash'; const { useState, useRef, useMemo, useEffect } = React;
const _ = require('lodash');
import MarkdownLegacy from '../../../shared/markdownLegacy.js'; const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
import Markdown from '../../../shared/markdown.js'; import Markdown from 'naturalcrit/markdown.js';
import ErrorBar from './errorBar/errorBar.jsx'; const ErrorBar = require('./errorBar/errorBar.jsx');
import ToolBar from './toolBar/toolBar.jsx'; const ToolBar = require('./toolBar/toolBar.jsx');
//TODO: move to the brew renderer //TODO: move to the brew renderer
import RenderWarnings from '../../components/renderWarnings/renderWarnings.jsx'; const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
import NotificationPopup from './notificationPopup/notificationPopup.jsx'; const NotificationPopup = require('./notificationPopup/notificationPopup.jsx');
import Frame from 'react-frame-component'; const Frame = require('react-frame-component').default;
import dedent from 'dedent'; const dedent = require('dedent-tabs').default;
import { printCurrentBrew } from '../../../shared/helpers.js'; const { printCurrentBrew } = require('../../../shared/helpers.js');
import HeaderNav from './headerNav/headerNav.jsx'; import HeaderNav from './headerNav/headerNav.jsx';
import { safeHTML } from './safeHTML.js'; import { safeHTML } from './safeHTML.js';
@@ -23,8 +24,6 @@ const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m;
const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m; const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m;
const PAGE_HEIGHT = 1056; const PAGE_HEIGHT = 1056;
const TOOLBAR_STATE_KEY = 'HB_renderer_toolbarState';
const INITIAL_CONTENT = dedent` const INITIAL_CONTENT = dedent`
<!DOCTYPE html><html><head> <!DOCTYPE html><html><head>
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" /> <link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
@@ -41,7 +40,7 @@ const BrewPage = (props)=>{
...props ...props
}; };
const pageRef = useRef(null); const pageRef = useRef(null);
const cleanText = safeHTML(props.contents); const cleanText = safeHTML(`${props.contents}\n<div class="columnSplit"></div>\n`);
useEffect(()=>{ useEffect(()=>{
if(!pageRef.current) return; if(!pageRef.current) return;
@@ -123,7 +122,7 @@ const BrewRenderer = (props)=>{
//useEffect to store or gather toolbar state from storage //useEffect to store or gather toolbar state from storage
useEffect(()=>{ useEffect(()=>{
const toolbarState = JSON.parse(window.localStorage.getItem(TOOLBAR_STATE_KEY)); const toolbarState = JSON.parse(window.localStorage.getItem('hb_toolbarState'));
toolbarState && setDisplayOptions(toolbarState); toolbarState && setDisplayOptions(toolbarState);
}, []); }, []);
@@ -285,7 +284,7 @@ const BrewRenderer = (props)=>{
const handleDisplayOptionsChange = (newDisplayOptions)=>{ const handleDisplayOptionsChange = (newDisplayOptions)=>{
setDisplayOptions(newDisplayOptions); setDisplayOptions(newDisplayOptions);
localStorage.setItem(TOOLBAR_STATE_KEY, JSON.stringify(newDisplayOptions)); localStorage.setItem('hb_toolbarState', JSON.stringify(newDisplayOptions));
}; };
const pagesStyle = { const pagesStyle = {
@@ -294,6 +293,12 @@ const BrewRenderer = (props)=>{
rowGap : `${displayOptions.rowGap}px` rowGap : `${displayOptions.rowGap}px`
}; };
const styleObject = {};
if(global.config.deployment) {
styleObject.backgroundImage = `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='40px' width='200px'><text x='0' y='15' fill='%23fff7' font-size='20'>${global.config.deployment}</text></svg>")`;
}
const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]); const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]);
renderedPages = useMemo(()=>renderPages(), [props.text, displayOptions]); renderedPages = useMemo(()=>renderPages(), [props.text, displayOptions]);
@@ -322,9 +327,10 @@ const BrewRenderer = (props)=>{
contentDidMount={frameDidMount} contentDidMount={frameDidMount}
onClick={()=>{emitClick();}} onClick={()=>{emitClick();}}
> >
<div className='brewRenderer' <div className={`brewRenderer ${global.config.deployment && 'deployment'}`}
onKeyDown={handleControlKeys} onKeyDown={handleControlKeys}
tabIndex={-1} tabIndex={-1}
style={ styleObject }
> >
{/* Apply CSS from Style tab and render pages from Markdown tab */} {/* Apply CSS from Style tab and render pages from Markdown tab */}
@@ -344,4 +350,4 @@ const BrewRenderer = (props)=>{
); );
}; };
export default BrewRenderer; module.exports = BrewRenderer;

View File

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

View File

@@ -1,5 +1,5 @@
import './errorBar.less'; require('./errorBar.less');
import React from 'react'; const React = require('react');
import Dialog from '../../../components/dialog.jsx'; import Dialog from '../../../components/dialog.jsx';
@@ -50,4 +50,4 @@ const ErrorBar = (props)=>{
); );
}; };
export default ErrorBar; module.exports = ErrorBar;

View File

@@ -1,7 +1,7 @@
import './headerNav.less'; require('./headerNav.less');
import React from 'react'; import * as React from 'react';
import _ from 'lodash'; import * as _ from 'lodash';
const MAX_TEXT_LENGTH = 40; const MAX_TEXT_LENGTH = 40;

View File

@@ -1,7 +1,7 @@
import './notificationPopup.less'; require('./notificationPopup.less');
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import Markdown from 'markdown.js'; import Markdown from 'naturalcrit/markdown.js';
import Dialog from '../../../components/dialog.jsx'; import Dialog from '../../../components/dialog.jsx';
@@ -62,4 +62,4 @@ const NotificationPopup = ()=>{
</Dialog>; </Dialog>;
}; };
export default NotificationPopup; module.exports = NotificationPopup;

View File

@@ -1,15 +1,14 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import './toolBar.less'; require('./toolBar.less');
import React, { useState, useEffect } from 'react'; const React = require('react');
import _ from 'lodash'; const { useState, useEffect } = React;
const _ = require('lodash');
import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anchored.jsx'; import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anchored.jsx';
const MAX_ZOOM = 300; const MAX_ZOOM = 300;
const MIN_ZOOM = 10; const MIN_ZOOM = 10;
const TOOLBAR_VISIBILITY = 'HB_renderer_toolbarVisibility';
const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPages, headerState, setHeaderState })=>{ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPages, headerState, setHeaderState })=>{
const [pageNum, setPageNum] = useState(1); const [pageNum, setPageNum] = useState(1);
@@ -22,7 +21,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
}, [visiblePages]); }, [visiblePages]);
useEffect(()=>{ useEffect(()=>{
const Visibility = localStorage.getItem(TOOLBAR_VISIBILITY); const Visibility = localStorage.getItem('hb_toolbarVisibility');
if (Visibility) setToolsVisible(Visibility === 'true'); if (Visibility) setToolsVisible(Visibility === 'true');
}, []); }, []);
@@ -101,7 +100,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
<div className='toggleButton'> <div className='toggleButton'>
<button title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{ <button title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{
setToolsVisible(!toolsVisible); setToolsVisible(!toolsVisible);
localStorage.setItem(TOOLBAR_VISIBILITY, !toolsVisible); localStorage.setItem('hb_toolbarVisibility', !toolsVisible);
}}><i className='fas fa-glasses' /></button> }}><i className='fas fa-glasses' /></button>
<button title={`${headerState ? 'Hide' : 'Show'} Header Navigation`} onClick={()=>{setHeaderState(!headerState);}}><i className='fas fa-rectangle-list' /></button> <button title={`${headerState ? 'Hide' : 'Show'} Header Navigation`} onClick={()=>{setHeaderState(!headerState);}}><i className='fas fa-rectangle-list' /></button>
</div> </div>
@@ -258,4 +257,4 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
); );
}; };
export default ToolBar; module.exports = ToolBar;

View File

@@ -1,16 +1,16 @@
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
import './editor.less'; require('./editor.less');
import React from 'react'; const React = require('react');
import createReactClass from 'create-react-class'; const createClass = require('create-react-class');
import _ from 'lodash'; const _ = require('lodash');
import dedent from 'dedent'; const dedent = require('dedent-tabs').default;
import Markdown from '../../../shared/markdown.js'; import Markdown from '../../../shared/naturalcrit/markdown.js';
import CodeEditor from '../../components/codeEditor/codeEditor.jsx'; const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
import SnippetBar from './snippetbar/snippetbar.jsx'; const SnippetBar = require('./snippetbar/snippetbar.jsx');
import MetadataEditor from './metadataEditor/metadataEditor.jsx'; const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
const EDITOR_THEME_KEY = 'HB_editor_theme'; const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/; const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
@@ -31,7 +31,7 @@ const DEFAULT_SNIPPET_TEXT = dedent`
`; `;
let isJumping = false; let isJumping = false;
const Editor = createReactClass({ const Editor = createClass({
displayName : 'Editor', displayName : 'Editor',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -40,7 +40,10 @@ const Editor = createReactClass({
style : '' style : ''
}, },
onBrewChange : ()=>{}, onTextChange : ()=>{},
onStyleChange : ()=>{},
onMetaChange : ()=>{},
onSnipChange : ()=>{},
reportError : ()=>{}, reportError : ()=>{},
onCursorPageChange : ()=>{}, onCursorPageChange : ()=>{},
@@ -58,7 +61,7 @@ const Editor = createReactClass({
return { return {
editorTheme : this.props.editorTheme, editorTheme : this.props.editorTheme,
view : 'text', //'text', 'style', 'meta', 'snippet' view : 'text', //'text', 'style', 'meta', 'snippet'
snippetBarHeight : 26, snippetbarHeight : 25
}; };
}, },
@@ -76,8 +79,8 @@ const Editor = createReactClass({
document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys); document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys);
document.addEventListener('keydown', this.handleControlKeys); document.addEventListener('keydown', this.handleControlKeys);
this.codeEditor.current.codeMirror?.on('cursorActivity', (cm)=>{this.updateCurrentCursorPage(cm.getCursor());}); this.codeEditor.current.codeMirror.on('cursorActivity', (cm)=>{this.updateCurrentCursorPage(cm.getCursor());});
this.codeEditor.current.codeMirror?.on('scroll', _.throttle(()=>{this.updateCurrentViewPage(this.codeEditor.current.getTopVisibleLine());}, 200)); this.codeEditor.current.codeMirror.on('scroll', _.throttle(()=>{this.updateCurrentViewPage(this.codeEditor.current.getTopVisibleLine());}, 200));
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY); const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
if(editorTheme) { if(editorTheme) {
@@ -85,15 +88,7 @@ const Editor = createReactClass({
editorTheme : editorTheme editorTheme : editorTheme
}); });
} }
const snippetBar = document.querySelector('.editor > .snippetBar'); this.setState({ snippetbarHeight: document.querySelector('.editor > .snippetBar').offsetHeight });
if (!snippetBar) return;
this.resizeObserver = new ResizeObserver(entries => {
const height = document.querySelector('.editor > .snippetBar').offsetHeight;
this.setState({ snippetBarHeight: height });
});
this.resizeObserver.observe(snippetBar);
}, },
componentDidUpdate : function(prevProps, prevState, snapshot) { componentDidUpdate : function(prevProps, prevState, snapshot) {
@@ -116,10 +111,6 @@ const Editor = createReactClass({
} }
}, },
componentWillUnmount() {
if (this.resizeObserver) this.resizeObserver.disconnect();
},
handleControlKeys : function(e){ handleControlKeys : function(e){
if(!(e.ctrlKey && e.metaKey && e.shiftKey)) return; if(!(e.ctrlKey && e.metaKey && e.shiftKey)) return;
const LEFTARROW_KEY = 37; const LEFTARROW_KEY = 37;
@@ -156,21 +147,21 @@ const Editor = createReactClass({
this.setState({ this.setState({
view : newView view : newView
}, ()=>{ }, ()=>{
this.codeEditor.current?.codeMirror?.focus(); this.codeEditor.current?.codeMirror.focus();
}); });
}, },
highlightCustomMarkdown : function(){ highlightCustomMarkdown : function(){
if(!this.codeEditor.current?.codeMirror) return; if(!this.codeEditor.current) return;
if((this.state.view === 'text') ||(this.state.view === 'snippet')) { if((this.state.view === 'text') ||(this.state.view === 'snippet')) {
const codeMirror = this.codeEditor.current.codeMirror; const codeMirror = this.codeEditor.current.codeMirror;
codeMirror?.operation(()=>{ // Batch CodeMirror styling codeMirror.operation(()=>{ // Batch CodeMirror styling
const foldLines = []; const foldLines = [];
//reset custom text styles //reset custom text styles
const customHighlights = codeMirror?.getAllMarks().filter((mark)=>{ const customHighlights = codeMirror.getAllMarks().filter((mark)=>{
// Record details of folded sections // Record details of folded sections
if(mark.__isFold) { if(mark.__isFold) {
const fold = mark.find(); const fold = mark.find();
@@ -191,10 +182,10 @@ const Editor = createReactClass({
const textOrSnip = this.state.view === 'text'; const textOrSnip = this.state.view === 'text';
//reset custom line styles //reset custom line styles
codeMirror?.removeLineClass(lineNumber, 'background', 'pageLine'); codeMirror.removeLineClass(lineNumber, 'background', 'pageLine');
codeMirror?.removeLineClass(lineNumber, 'background', 'snippetLine'); codeMirror.removeLineClass(lineNumber, 'background', 'snippetLine');
codeMirror?.removeLineClass(lineNumber, 'text'); codeMirror.removeLineClass(lineNumber, 'text');
codeMirror?.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash'); codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash');
// Don't process lines inside folded text // Don't process lines inside folded text
// If the current lineNumber is inside any folded marks, skip line styling // If the current lineNumber is inside any folded marks, skip line styling
@@ -210,19 +201,19 @@ const Editor = createReactClass({
else if(this.state.view !== 'text') userSnippetCount += 1; else if(this.state.view !== 'text') userSnippetCount += 1;
// add back the original class 'background' but also add the new class '.pageline' // add back the original class 'background' but also add the new class '.pageline'
codeMirror?.addLineClass(lineNumber, 'background', tabHighlight); codeMirror.addLineClass(lineNumber, 'background', tabHighlight);
const pageCountElement = Object.assign(document.createElement('span'), { const pageCountElement = Object.assign(document.createElement('span'), {
className : 'editor-page-count', className : 'editor-page-count',
textContent : textOrSnip ? editorPageCount : userSnippetCount textContent : textOrSnip ? editorPageCount : userSnippetCount
}); });
codeMirror?.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement); codeMirror.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement);
}; };
// New CodeMirror styling for V3 renderer // New Codemirror styling for V3 renderer
if(this.props.renderer === 'V3') { if(this.props.renderer === 'V3') {
if(line.match(/^\\column(?:break)?$/)){ if(line.match(/^\\column(?:break)?$/)){
codeMirror?.addLineClass(lineNumber, 'text', 'columnSplit'); codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
} }
// definition lists // definition lists
@@ -231,14 +222,14 @@ const Editor = createReactClass({
const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error. const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error.
let match; let match;
while ((match = regex.exec(line)) != null){ while ((match = regex.exec(line)) != null){
codeMirror?.markText({ line: lineNumber, ch: match.indices[0][0] }, { line: lineNumber, ch: match.indices[0][1] }, { className: 'dl-highlight' }); codeMirror.markText({ line: lineNumber, ch: match.indices[0][0] }, { line: lineNumber, ch: match.indices[0][1] }, { className: 'dl-highlight' });
codeMirror?.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' }); codeMirror.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' });
codeMirror?.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' }); codeMirror.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' });
const ddIndex = match.indices[2][0]; const ddIndex = match.indices[2][0];
const colons = /::/g; const colons = /::/g;
const colonMatches = colons.exec(match[2]); const colonMatches = colons.exec(match[2]);
if(colonMatches !== null){ if(colonMatches !== null){
codeMirror?.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight' }); codeMirror.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight' });
} }
} }
} }
@@ -255,7 +246,7 @@ const Editor = createReactClass({
const match = subRegex.exec(line) || superRegex.exec(line); const match = subRegex.exec(line) || superRegex.exec(line);
if(match) { if(match) {
isSuper = !subRegex.lastIndex; isSuper = !subRegex.lastIndex;
codeMirror?.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' }); codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' });
} }
startIndex = line.indexOf('^', Math.max(startIndex + 1, subRegex.lastIndex, superRegex.lastIndex)); startIndex = line.indexOf('^', Math.max(startIndex + 1, subRegex.lastIndex, superRegex.lastIndex));
} }
@@ -266,7 +257,7 @@ const Editor = createReactClass({
const regex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gm; const regex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gm;
let match; let match;
while ((match = regex.exec(line)) != null) { while ((match = regex.exec(line)) != null) {
codeMirror?.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' }); codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' });
} }
} }
// Highlight inline spans {{content}} // Highlight inline spans {{content}}
@@ -284,7 +275,7 @@ const Editor = createReactClass({
blockCount = 0; blockCount = 0;
continue; continue;
} }
codeMirror?.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'inline-block' }); codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'inline-block' });
} }
} else if(line.trimLeft().startsWith('{{') || line.trimLeft().startsWith('}}')){ } else if(line.trimLeft().startsWith('{{') || line.trimLeft().startsWith('}}')){
// Highlight block divs {{\n Content \n}} // Highlight block divs {{\n Content \n}}
@@ -293,7 +284,7 @@ const Editor = createReactClass({
const match = line.match(/^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *$|^ *}}$/); const match = line.match(/^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *$|^ *}}$/);
if(match) if(match)
endCh = match.index+match[0].length; endCh = match.index+match[0].length;
codeMirror?.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' }); codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
} }
// Emojis // Emojis
@@ -314,11 +305,11 @@ const Editor = createReactClass({
const endPos = { line: lineNumber, ch: match.index + match[0].length }; const endPos = { line: lineNumber, ch: match.index + match[0].length };
// Iterate over conflicting marks and clear them // Iterate over conflicting marks and clear them
const marks = codeMirror?.findMarks(startPos, endPos); const marks = codeMirror.findMarks(startPos, endPos);
marks.forEach(function(marker) { marks.forEach(function(marker) {
if(!marker.__isFold) marker.clear(); if(!marker.__isFold) marker.clear();
}); });
codeMirror?.markText(startPos, endPos, { className: 'emoji' }); codeMirror.markText(startPos, endPos, { className: 'emoji' });
} }
startIndex = line.indexOf(':', Math.max(startIndex + 1, emojiRegex.lastIndex)); startIndex = line.indexOf(':', Math.max(startIndex + 1, emojiRegex.lastIndex));
} }
@@ -338,8 +329,8 @@ const Editor = createReactClass({
const currentPos = brewRenderer.scrollTop; const currentPos = brewRenderer.scrollTop;
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top; const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
const checkIfScrollComplete = ()=>{
let scrollingTimeout; let scrollingTimeout;
const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
scrollingTimeout = setTimeout(()=>{ scrollingTimeout = setTimeout(()=>{
isJumping = false; isJumping = false;
@@ -378,51 +369,54 @@ const Editor = createReactClass({
const textString = this.props.brew.text.split(textSplit).slice(0, targetPage-1).join(textSplit); const textString = this.props.brew.text.split(textSplit).slice(0, targetPage-1).join(textSplit);
const targetLine = textString.match('\n') ? textString.split('\n').length - 1 : -1; const targetLine = textString.match('\n') ? textString.split('\n').length - 1 : -1;
let currentY = this.codeEditor.current.codeMirror?.getScrollInfo().top; let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
let targetY = this.codeEditor.current.codeMirror?.heightAtLine(targetLine, 'local', true); let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
const checkIfScrollComplete = ()=>{
let scrollingTimeout; let scrollingTimeout;
const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
scrollingTimeout = setTimeout(()=>{ scrollingTimeout = setTimeout(()=>{
isJumping = false; isJumping = false;
this.codeEditor.current.codeMirror?.off('scroll', checkIfScrollComplete); this.codeEditor.current.codeMirror.off('scroll', checkIfScrollComplete);
}, 150); // If 150 ms pass without a scroll event, assume scrolling is done }, 150); // If 150 ms pass without a scroll event, assume scrolling is done
}; };
isJumping = true; isJumping = true;
checkIfScrollComplete(); checkIfScrollComplete();
if (this.codeEditor.current?.codeMirror) { this.codeEditor.current.codeMirror.on('scroll', checkIfScrollComplete);
this.codeEditor.current.codeMirror?.on('scroll', checkIfScrollComplete);
}
if(smooth) { if(smooth) {
//Scroll 1/10 of the way every 10ms until 1px off. //Scroll 1/10 of the way every 10ms until 1px off.
const incrementalScroll = setInterval(()=>{ const incrementalScroll = setInterval(()=>{
currentY += (targetY - currentY) / 10; currentY += (targetY - currentY) / 10;
this.codeEditor.current.codeMirror?.scrollTo(null, currentY); this.codeEditor.current.codeMirror.scrollTo(null, currentY);
// Update target: target height is not accurate until within +-10 lines of the visible window // Update target: target height is not accurate until within +-10 lines of the visible window
if(Math.abs(targetY - currentY > 100)) if(Math.abs(targetY - currentY > 100))
targetY = this.codeEditor.current.codeMirror?.heightAtLine(targetLine, 'local', true); targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
// End when close enough // End when close enough
if(Math.abs(targetY - currentY) < 1) { if(Math.abs(targetY - currentY) < 1) {
this.codeEditor.current.codeMirror?.scrollTo(null, targetY); // Scroll any remaining difference this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 }); this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
this.codeEditor.current.codeMirror?.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash'); this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
clearInterval(incrementalScroll); clearInterval(incrementalScroll);
} }
}, 10); }, 10);
} else { } else {
this.codeEditor.current.codeMirror?.scrollTo(null, targetY); // Scroll any remaining difference this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 }); this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
this.codeEditor.current.codeMirror?.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash'); this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
} }
}, },
//Called when there are changes to the editor's dimensions //Called when there are changes to the editor's dimensions
update : function(){}, update : function(){
this.codeEditor.current?.updateSize();
const snipHeight = document.querySelector('.editor > .snippetBar').offsetHeight;
if(snipHeight !== this.state.snippetbarHeight)
this.setState({ snippetbarHeight: snipHeight });
},
updateEditorTheme : function(newTheme){ updateEditorTheme : function(newTheme){
window.localStorage.setItem(EDITOR_THEME_KEY, newTheme); window.localStorage.setItem(EDITOR_THEME_KEY, newTheme);
@@ -444,10 +438,10 @@ const Editor = createReactClass({
language='gfm' language='gfm'
view={this.state.view} view={this.state.view}
value={this.props.brew.text} value={this.props.brew.text}
onChange={this.props.onBrewChange('text')} onChange={this.props.onTextChange}
editorTheme={this.state.editorTheme} editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }} /> style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
</>; </>;
} }
if(this.isStyle()){ if(this.isStyle()){
@@ -457,11 +451,11 @@ const Editor = createReactClass({
language='css' language='css'
view={this.state.view} view={this.state.view}
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT} value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
onChange={this.props.onBrewChange('style')} onChange={this.props.onStyleChange}
enableFolding={true} enableFolding={true}
editorTheme={this.state.editorTheme} editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }} /> style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
</>; </>;
} }
if(this.isMeta()){ if(this.isMeta()){
@@ -473,11 +467,12 @@ const Editor = createReactClass({
<MetadataEditor <MetadataEditor
metadata={this.props.brew} metadata={this.props.brew}
themeBundle={this.props.themeBundle} themeBundle={this.props.themeBundle}
onChange={this.props.onBrewChange('metadata')} onChange={this.props.onMetaChange}
reportError={this.props.reportError} reportError={this.props.reportError}
userThemes={this.props.userThemes}/> userThemes={this.props.userThemes}/>
</>; </>;
} }
if(this.isSnip()){ if(this.isSnip()){
if(!this.props.brew.snippets) { this.props.brew.snippets = DEFAULT_SNIPPET_TEXT; } if(!this.props.brew.snippets) { this.props.brew.snippets = DEFAULT_SNIPPET_TEXT; }
return <> return <>
@@ -486,11 +481,11 @@ const Editor = createReactClass({
language='gfm' language='gfm'
view={this.state.view} view={this.state.view}
value={this.props.brew.snippets} value={this.props.brew.snippets}
onChange={this.props.onBrewChange('snippets')} onChange={this.props.onSnipChange}
enableFolding={true} enableFolding={true}
editorTheme={this.state.editorTheme} editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} rerenderParent={this.rerenderParent}
style={{ height: `calc(100% -${this.state.snippetBarHeight}px)` }} /> style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
</>; </>;
} }
}, },
@@ -544,4 +539,4 @@ const Editor = createReactClass({
} }
}); });
export default Editor; module.exports = Editor;

View File

@@ -1,18 +1,19 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import './metadataEditor.less'; require('./metadataEditor.less');
import React from 'react'; const React = require('react');
import createReactClass from 'create-react-class'; const createClass = require('create-react-class');
import _ from 'lodash'; const _ = require('lodash');
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import Combobox from '../../../components/combobox.jsx'; const Combobox = require('client/components/combobox.jsx');
import TagInput from '../tagInput/tagInput.jsx'; const TagInput = require('../tagInput/tagInput.jsx');
import Themes from 'themes/themes.json';
import validations from './validations.js'; const Themes = require('themes/themes.json');
const validations = require('./validations.js');
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder']; const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
import homebreweryThumbnail from '../../thumbnail.png'; const homebreweryThumbnail = require('../../thumbnail.png');
const callIfExists = (val, fn, ...args)=>{ const callIfExists = (val, fn, ...args)=>{
if(val[fn]) { if(val[fn]) {
@@ -20,7 +21,7 @@ const callIfExists = (val, fn, ...args)=>{
} }
}; };
const MetadataEditor = createReactClass({ const MetadataEditor = createClass({
displayName : 'MetadataEditor', displayName : 'MetadataEditor',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -206,6 +207,8 @@ const MetadataEditor = createReactClass({
}, },
renderThemeDropdown : function(){ renderThemeDropdown : function(){
if(!global.enable_themes) return;
const mergedThemes = _.merge(Themes, this.props.userThemes); const mergedThemes = _.merge(Themes, this.props.userThemes);
const listThemes = (renderer)=>{ const listThemes = (renderer)=>{
@@ -304,6 +307,8 @@ const MetadataEditor = createReactClass({
}, },
renderRenderOptions : function(){ renderRenderOptions : function(){
if(!global.enable_v3) return;
return <div className='field systems'> return <div className='field systems'>
<label>Renderer</label> <label>Renderer</label>
<div className='value'> <div className='value'>
@@ -410,4 +415,4 @@ const MetadataEditor = createReactClass({
} }
}); });
export default MetadataEditor; module.exports = MetadataEditor;

View File

@@ -1,4 +1,4 @@
export default { module.exports = {
title : [ title : [
(value)=>{ (value)=>{
return value?.length > 100 ? 'Max title length of 100 characters' : null; return value?.length > 100 ? 'Max title length of 100 characters' : null;
@@ -18,7 +18,7 @@ export default {
try { try {
Boolean(new URL(value)); Boolean(new URL(value));
return null; return null;
} catch { } catch (e) {
return 'Must be a valid URL'; return 'Must be a valid URL';
} }
} }

View File

@@ -1,36 +1,29 @@
/*eslint max-lines: ["warn", {"max": 350, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 350, "skipBlankLines": true, "skipComments": true}]*/
import './snippetbar.less'; require('./snippetbar.less');
import React from 'react'; const React = require('react');
import createReactClass from 'create-react-class'; const createClass = require('create-react-class');
const _ = require('lodash');
import _ from 'lodash'; const cx = require('classnames');
import cx from 'classnames';
import { loadHistory } from '../../utils/versionHistory.js'; import { loadHistory } from '../../utils/versionHistory.js';
import { brewSnippetsToJSON } from '../../../../shared/helpers.js'; import { brewSnippetsToJSON } from '../../../../shared/helpers.js';
import Legacy5ePHB from 'themes/Legacy/5ePHB/snippets.js'; //Import all themes
import V3_5ePHB from 'themes/V3/5ePHB/snippets.js'; const ThemeSnippets = {};
import V3_5eDMG from 'themes/V3/5eDMG/snippets.js'; ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js');
import V3_Journal from 'themes/V3/Journal/snippets.js'; ThemeSnippets['V3_5ePHB'] = require('themes/V3/5ePHB/snippets.js');
import V3_Blank from 'themes/V3/Blank/snippets.js'; ThemeSnippets['V3_5eDMG'] = require('themes/V3/5eDMG/snippets.js');
ThemeSnippets['V3_Journal'] = require('themes/V3/Journal/snippets.js');
ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/snippets.js');
const ThemeSnippets = { const EditorThemes = require('build/homebrew/codeMirror/editorThemes.json');
Legacy_5ePHB : Legacy5ePHB,
V3_5ePHB : V3_5ePHB,
V3_5eDMG : V3_5eDMG,
V3_Journal : V3_Journal,
V3_Blank : V3_Blank,
};
import EditorThemes from 'build/homebrew/codeMirror/editorThemes.json';
const execute = function(val, props){ const execute = function(val, props){
if(_.isFunction(val)) return val(props); if(_.isFunction(val)) return val(props);
return val; return val;
}; };
const Snippetbar = createReactClass({ const Snippetbar = createClass({
displayName : 'SnippetBar', displayName : 'SnippetBar',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -288,9 +281,9 @@ const Snippetbar = createReactClass({
} }
}); });
export default Snippetbar; module.exports = Snippetbar;
const SnippetGroup = createReactClass({ const SnippetGroup = createClass({
displayName : 'SnippetGroup', displayName : 'SnippetGroup',
getDefaultProps : function() { getDefaultProps : function() {
return { return {

View File

@@ -14,7 +14,7 @@
.snippets { .snippets {
display : flex; display : flex;
justify-content : flex-start; justify-content : flex-start;
min-width : 499.35px; //must be controlled every time an item is added, must be hardcoded for the wrapping as it is applied min-width : 432.18px; //must be controlled every time an item is added, must be hardcoded for the wrapping as it is applied
} }
.editors { .editors {
@@ -237,7 +237,7 @@
} }
} }
@container editor (width < 750px) { @container editor (width < 683px) {
.snippetBar { .snippetBar {
.editors { .editors {
flex : 1; flex : 1;

View File

@@ -1,6 +1,7 @@
import './tagInput.less'; require('./tagInput.less');
import React, { useState, useEffect } from 'react'; const React = require('react');
import _ from 'lodash'; const { useState, useEffect } = React;
const _ = require('lodash');
const TagInput = ({ unique = true, values = [], ...props })=>{ const TagInput = ({ unique = true, values = [], ...props })=>{
const [tempInputText, setTempInputText] = useState(''); const [tempInputText, setTempInputText] = useState('');
@@ -101,4 +102,4 @@ const TagInput = ({ unique = true, values = [], ...props })=>{
); );
}; };
export default TagInput; module.exports = TagInput;

View File

@@ -1,11 +1,9 @@
/* eslint-disable camelcase */
import 'core-js/es/string/to-well-formed.js'; //Polyfill for older browsers import 'core-js/es/string/to-well-formed.js'; //Polyfill for older browsers
import './homebrew.less'; import './homebrew.less';
import React from 'react'; import React from 'react';
import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router'; import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router';
import { updateLocalStorage } from './utils/updateLocalStorage/updateLocalStorageKeys.js';
import HomePage from './pages/homePage/homePage.jsx'; import HomePage from './pages/homePage/homePage.jsx';
import EditPage from './pages/editPage/editPage.jsx'; import EditPage from './pages/editPage/editPage.jsx';
import UserPage from './pages/userPage/userPage.jsx'; import UserPage from './pages/userPage/userPage.jsx';
@@ -19,6 +17,7 @@ const WithRoute = ({ el: Element, ...rest })=>{
const params = useParams(); const params = useParams();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const queryParams = Object.fromEntries(searchParams?.entries() || []); const queryParams = Object.fromEntries(searchParams?.entries() || []);
return <Element {...rest} {...params} query={queryParams} />; return <Element {...rest} {...params} query={queryParams} />;
}; };
@@ -27,6 +26,8 @@ const Homebrew = (props)=>{
url = '', url = '',
version = '0.0.0', version = '0.0.0',
account = null, account = null,
enable_v3 = false,
enable_themes,
config, config,
brew = { brew = {
title : '', title : '',
@@ -43,22 +44,13 @@ const Homebrew = (props)=>{
global.account = account; global.account = account;
global.version = version; global.version = version;
global.enable_v3 = enable_v3;
global.enable_themes = enable_themes;
global.config = config; global.config = config;
const backgroundObject = ()=>{
if(global.config.deployment || (config.local && config.development)){
const bgText = global.config.deployment || 'Local';
return {
backgroundImage : `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='100px' width='200px'><text x='0' y='15' fill='%23fff7' font-size='20'>${bgText}</text></svg>")`
};
}
return null;
};
updateLocalStorage();
return ( return (
<Router location={url}> <Router location={url}>
<div className={`homebrew${(config.deployment || config.local) ? ' deployment' : ''}`} style={backgroundObject()}> <div className='homebrew'>
<Routes> <Routes>
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} /> <Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} />
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} /> <Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} />

View File

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

View File

@@ -1,9 +1,9 @@
import React from 'react'; const React = require('react');
import createReactClass from 'create-react-class'; const createClass = require('create-react-class');
import request from 'superagent'; const Nav = require('naturalcrit/nav/nav.jsx');
import Nav from './nav.jsx'; const request = require('superagent');
const Account = createReactClass({ const Account = createClass({
displayName : 'AccountNavItem', displayName : 'AccountNavItem',
getInitialState : function() { getInitialState : function() {
return { return {
@@ -70,7 +70,7 @@ const Account = createReactClass({
{global.account.username} {global.account.username}
</Nav.item> </Nav.item>
<Nav.item <Nav.item
href={`/user/${encodeURIComponent(global.account.username)}`} href={`/user/${encodeURI(global.account.username)}`}
color='yellow' color='yellow'
icon='fas fa-beer' icon='fas fa-beer'
> >
@@ -111,4 +111,4 @@ const Account = createReactClass({
} }
}); });
export default Account; module.exports = Account;

View File

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

View File

@@ -1,9 +1,9 @@
import React from 'react'; const React = require('react');
import dedent from 'dedent'; const dedent = require('dedent-tabs').default;
import Nav from './nav.jsx'; const Nav = require('naturalcrit/nav/nav.jsx');
export default function(props){ module.exports = function(props){
return <Nav.dropdown> return <Nav.dropdown>
<Nav.item color='grey' icon='fas fa-question-circle'> <Nav.item color='grey' icon='fas fa-question-circle'>
need help? need help?

View File

@@ -1,11 +1,11 @@
import React from 'react'; const React = require('react');
import createReactClass from 'create-react-class'; const createClass = require('create-react-class');
import Moment from 'moment'; const Moment = require('moment');
import Nav from './nav.jsx'; const Nav = require('naturalcrit/nav/nav.jsx');
const MetadataNav = createReactClass({ const MetadataNav = createClass({
displayName : 'MetadataNav', displayName : 'MetadataNav',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -32,7 +32,7 @@ const MetadataNav = createReactClass({
return <> return <>
{this.props.brew.authors.map((author, idx, arr)=>{ {this.props.brew.authors.map((author, idx, arr)=>{
const spacer = arr.length - 1 == idx ? <></> : <span>, </span>; const spacer = arr.length - 1 == idx ? <></> : <span>, </span>;
return <span key={idx}><a className='userPageLink' href={`/user/${encodeURIComponent(author)}`}>{author}</a>{spacer}</span>; return <span key={idx}><a className='userPageLink' href={`/user/${author}`}>{author}</a>{spacer}</span>;
})} })}
</>; </>;
}, },
@@ -86,4 +86,4 @@ const MetadataNav = createReactClass({
}); });
export default MetadataNav; module.exports = MetadataNav;

View File

@@ -1,11 +1,11 @@
import './navbar.less'; require('./navbar.less');
import React from 'react'; const React = require('react');
import createReactClass from 'create-react-class'; const createClass = require('create-react-class');
import Nav from './nav.jsx'; const Nav = require('naturalcrit/nav/nav.jsx');
import PatreonNavItem from './patreon.navitem.jsx'; const PatreonNavItem = require('./patreon.navitem.jsx');
const Navbar = createReactClass({ const Navbar = createClass({
displayName : 'Navbar', displayName : 'Navbar',
getInitialState : function() { getInitialState : function() {
return { return {
@@ -49,4 +49,4 @@ const Navbar = createReactClass({
} }
}); });
export default Navbar; module.exports = Navbar;

View File

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

View File

@@ -1,69 +1,37 @@
import React from 'react'; const React = require('react');
import _ from 'lodash'; const _ = require('lodash');
import Nav from './nav.jsx'; const Nav = require('naturalcrit/nav/nav.jsx');
import { splitTextStyleAndMetadata } from '../../../shared/helpers.js'; const { splitTextStyleAndMetadata } = require('../../../shared/helpers.js'); // Importing the function from helpers.js
const BREWKEY = 'HB_newPage_content'; const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'HB_newPage_style'; const STYLEKEY = 'homebrewery-new-style';
const METAKEY = 'HB_newPage_meta'; const METAKEY = 'homebrewery-new-meta';
const NewBrew = ()=>{ const NewBrew = ()=>{
const handleFileChange = (e)=>{ const handleFileChange = (e)=>{
const file = e.target.files[0]; const file = e.target.files[0];
if(!file) return; if(file) {
if(!confirmLocalStorageChange()) return;
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e)=>{ reader.onload = (e)=>{
const fileContent = e.target.result; const fileContent = e.target.result;
const newBrew = { text: fileContent, style: '' }; const newBrew = {
text : fileContent,
style : ''
};
if(fileContent.startsWith('```metadata')) { if(fileContent.startsWith('```metadata')) {
splitTextStyleAndMetadata(newBrew); splitTextStyleAndMetadata(newBrew); // Modify newBrew directly
localStorage.setItem(BREWKEY, newBrew.text); localStorage.setItem(BREWKEY, newBrew.text);
localStorage.setItem(STYLEKEY, newBrew.style); localStorage.setItem(STYLEKEY, newBrew.style);
localStorage.setItem(METAKEY, JSON.stringify( localStorage.setItem(METAKEY, JSON.stringify(_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])));
_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])
));
window.location.href = '/new'; window.location.href = '/new';
return; } else {
alert('This file is invalid, please, enter a valid file');
} }
const type = file.name.split('.').pop().toLowerCase();
alert(`This file is invalid: ${!type ? 'Missing file extension' :`.${type} files are not supported`}. Only .txt files exported from the Homebrewery are allowed.`);
console.log(file);
}; };
reader.readAsText(file); reader.readAsText(file);
}
}; };
const confirmLocalStorageChange = ()=>{
const currentText = localStorage.getItem(BREWKEY);
const currentStyle = localStorage.getItem(STYLEKEY);
const currentMeta = localStorage.getItem(METAKEY);
// TRUE if no data in any local storage key
// TRUE if data in any local storage key AND approval given
// FALSE if data in any local storage key AND approval declined
return (!(currentText || currentStyle || currentMeta) || confirm(
`You have made changes in the new brew space. If you continue, that information will be PERMANENTLY LOST.\nAre you sure you wish to continue?`
));
};
const clearLocalStorage = ()=>{
if(!confirmLocalStorageChange()) return;
localStorage.removeItem(BREWKEY);
localStorage.removeItem(STYLEKEY);
localStorage.removeItem(METAKEY);
window.location.href = '/new';
return;
};
return ( return (
<Nav.dropdown> <Nav.dropdown>
<Nav.item <Nav.item
@@ -73,24 +41,17 @@ const NewBrew = ()=>{
new new
</Nav.item> </Nav.item>
<Nav.item <Nav.item
className='new' className='fromBlank'
href='/new' href='/new'
newTab={true} newTab={true}
color='purple' color='purple'
icon='fa-solid fa-file'> icon='fa-solid fa-file'>
resume draft
</Nav.item>
<Nav.item
className='fromBlank'
newTab={true}
color='yellow'
icon='fa-solid fa-file-circle-plus'
onClick={()=>{ clearLocalStorage(); }}>
from blank from blank
</Nav.item> </Nav.item>
<Nav.item <Nav.item
className='fromFile' className='fromFile'
color='green' color='purple'
icon='fa-solid fa-upload' icon='fa-solid fa-upload'
onClick={()=>{ document.getElementById('uploadTxt').click(); }}> onClick={()=>{ document.getElementById('uploadTxt').click(); }}>
<input id='uploadTxt' className='newFromLocal' type='file' onChange={handleFileChange} style={{ display: 'none' }} /> <input id='uploadTxt' className='newFromLocal' type='file' onChange={handleFileChange} style={{ display: 'none' }} />
@@ -100,4 +61,4 @@ const NewBrew = ()=>{
); );
}; };
export default NewBrew; module.exports = NewBrew;

View File

@@ -1,7 +1,7 @@
import React from 'react'; const React = require('react');
import Nav from './nav.jsx'; const Nav = require('naturalcrit/nav/nav.jsx');
export default function(props){ module.exports = function(props){
return <Nav.item return <Nav.item
className='patreon' className='patreon'
newTab={true} newTab={true}

View File

@@ -1,8 +1,8 @@
import React from 'react'; const React = require('react');
import Nav from './nav.jsx'; const Nav = require('naturalcrit/nav/nav.jsx');
import { printCurrentBrew } from '../../../shared/helpers.js'; const { printCurrentBrew } = require('../../../shared/helpers.js');
export default function(){ module.exports = function(){
return <Nav.item onClick={printCurrentBrew} color='purple' icon='far fa-file-pdf'> return <Nav.item onClick={printCurrentBrew} color='purple' icon='far fa-file-pdf'>
get PDF get PDF
</Nav.item>; </Nav.item>;

View File

@@ -1,15 +1,15 @@
import React from 'react'; const React = require('react');
import createReactClass from 'create-react-class'; const createClass = require('create-react-class');
import _ from 'lodash'; const _ = require('lodash');
import Moment from 'moment'; const Moment = require('moment');
import Nav from './nav.jsx'; const Nav = require('naturalcrit/nav/nav.jsx');
const EDIT_KEY = 'HB_nav_recentlyEdited'; const EDIT_KEY = 'homebrewery-recently-edited';
const VIEW_KEY = 'HB_nav_recentlyViewed'; const VIEW_KEY = 'homebrewery-recently-viewed';
const RecentItems = createReactClass({ const RecentItems = createClass({
DisplayName : 'RecentItems', DisplayName : 'RecentItems',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -175,7 +175,7 @@ const RecentItems = createReactClass({
}); });
export default { module.exports = {
edited : (props)=>{ edited : (props)=>{
return <RecentItems return <RecentItems

View File

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

View File

@@ -1,8 +1,8 @@
import React from 'react'; const React = require('react');
import Nav from './nav.jsx'; const Nav = require('naturalcrit/nav/nav.jsx');
export default function (props) { module.exports = function (props) {
return ( return (
<Nav.item <Nav.item
color='purple' color='purple'

View File

@@ -1,7 +1,7 @@
import React from 'react'; const React = require('react');
import moment from 'moment'; const moment = require('moment');
import UIPage from '../basePages/uiPage/uiPage.jsx'; const UIPage = require('../basePages/uiPage/uiPage.jsx');
import NaturalCritIcon from '../../../components/svg/naturalcrit-d20.svg.jsx'; const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
let SAVEKEY = ''; let SAVEKEY = '';
@@ -13,7 +13,7 @@ const AccountPage = (props)=>{
// initialize save location from local storage based on user id // initialize save location from local storage based on user id
React.useEffect(()=>{ React.useEffect(()=>{
if(!saveLocation && accountDetails.username) { if(!saveLocation && accountDetails.username) {
SAVEKEY = `HB_editor_defaultSave_${accountDetails.username}`; SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${accountDetails.username}`;
// if no SAVEKEY in local storage, default save location to Google Drive if user has Google account. // if no SAVEKEY in local storage, default save location to Google Drive if user has Google account.
let saveLocation = window.localStorage.getItem(SAVEKEY); let saveLocation = window.localStorage.getItem(SAVEKEY);
saveLocation = saveLocation ?? (accountDetails.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY'); saveLocation = saveLocation ?? (accountDetails.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');
@@ -79,4 +79,4 @@ const AccountPage = (props)=>{
</UIPage>); </UIPage>);
}; };
export default AccountPage; module.exports = AccountPage;

View File

@@ -1,11 +1,12 @@
import './brewItem.less'; require('./brewItem.less');
import React, { useCallback } from 'react'; const React = require('react');
import moment from 'moment'; const { useCallback } = React;
const moment = require('moment');
import request from '../../../../utils/request-middleware.js'; import request from '../../../../utils/request-middleware.js';
import googleDriveIcon from '../../../../googleDrive.svg'; const googleDriveIcon = require('../../../../googleDrive.svg');
import homebreweryIcon from '../../../../thumbnail.svg'; const homebreweryIcon = require('../../../../thumbnail.svg');
import dedent from 'dedent'; const dedent = require('dedent-tabs').default;
const BrewItem = ({ const BrewItem = ({
brew = { brew = {
@@ -142,7 +143,7 @@ const BrewItem = ({
<span title="Username contained an email address; hidden to protect user's privacy"> <span title="Username contained an email address; hidden to protect user's privacy">
{author} {author}
</span> </span>
) : (<a href={`/user/${encodeURIComponent(author)}`}>{author}</a>)} ) : (<a href={`/user/${author}`}>{author}</a>)}
{index < brew.authors.length - 1 && ', '} {index < brew.authors.length - 1 && ', '}
</React.Fragment> </React.Fragment>
))} ))}
@@ -175,4 +176,4 @@ const BrewItem = ({
); );
}; };
export default BrewItem; module.exports = BrewItem;

View File

@@ -1,20 +1,18 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
import './listPage.less'; require('./listPage.less');
import React from 'react'; const React = require('react');
import createReactClass from 'create-react-class'; const createClass = require('create-react-class');
import _ from 'lodash'; const _ = require('lodash');
import moment from 'moment'; const moment = require('moment');
import BrewItem from './brewItem/brewItem.jsx'; const BrewItem = require('./brewItem/brewItem.jsx');
const USERPAGE_SORT_DIR = 'HB_listPage_sortDir'; const USERPAGE_KEY_PREFIX = 'HOMEBREWERY-LISTPAGE';
const USERPAGE_SORT_TYPE = 'HB_listPage_sortType';
const USERPAGE_GROUP_VISIBILITY_PREFIX = 'HB_listPage_visibility_group';
const DEFAULT_SORT_TYPE = 'alpha'; const DEFAULT_SORT_TYPE = 'alpha';
const DEFAULT_SORT_DIR = 'asc'; const DEFAULT_SORT_DIR = 'asc';
const ListPage = createReactClass({ const ListPage = createClass({
displayName : 'ListPage', displayName : 'ListPage',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -52,12 +50,12 @@ const ListPage = createReactClass({
// LOAD FROM LOCAL STORAGE // LOAD FROM LOCAL STORAGE
if(typeof window !== 'undefined') { if(typeof window !== 'undefined') {
const newSortType = (this.state.sortType ?? (localStorage.getItem(USERPAGE_SORT_TYPE) || DEFAULT_SORT_TYPE)); const newSortType = (this.state.sortType ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`) || DEFAULT_SORT_TYPE));
const newSortDir = (this.state.sortDir ?? (localStorage.getItem(USERPAGE_SORT_DIR) || DEFAULT_SORT_DIR)); const newSortDir = (this.state.sortDir ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`) || DEFAULT_SORT_DIR));
this.updateUrl(this.state.filterString, newSortType, newSortDir); this.updateUrl(this.state.filterString, newSortType, newSortDir);
const brewCollection = this.props.brewCollection.map((brewGroup)=>{ const brewCollection = this.props.brewCollection.map((brewGroup)=>{
brewGroup.visible = (localStorage.getItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroup.class}`) ?? 'true')=='true'; brewGroup.visible = (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`) ?? 'true')=='true';
return brewGroup; return brewGroup;
}); });
@@ -75,10 +73,10 @@ const ListPage = createReactClass({
saveToLocalStorage : function() { saveToLocalStorage : function() {
this.state.brewCollection.map((brewGroup)=>{ this.state.brewCollection.map((brewGroup)=>{
localStorage.setItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroup.class}`, `${brewGroup.visible}`); localStorage.setItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`, `${brewGroup.visible}`);
}); });
localStorage.setItem(USERPAGE_SORT_TYPE, this.state.sortType); localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`, this.state.sortType);
localStorage.setItem(USERPAGE_SORT_DIR, this.state.sortDir); localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`, this.state.sortDir);
}, },
renderBrews : function(brews){ renderBrews : function(brews){
@@ -279,4 +277,4 @@ const ListPage = createReactClass({
} }
}); });
export default ListPage; module.exports = ListPage;

View File

@@ -1,17 +1,16 @@
import './uiPage.less'; require('./uiPage.less');
import React from 'react'; const React = require('react');
import createReactClass from 'create-react-class'; const createClass = require('create-react-class');
import Nav from '../../../navbar/nav.jsx'; const Nav = require('naturalcrit/nav/nav.jsx');
import Navbar from '../../../navbar/navbar.jsx'; const Navbar = require('../../../navbar/navbar.jsx');
import NewBrewItem from '../../../navbar/newbrew.navitem.jsx'; const NewBrewItem = require('../../../navbar/newbrew.navitem.jsx');
import HelpNavItem from '../../../navbar/help.navitem.jsx'; const HelpNavItem = require('../../../navbar/help.navitem.jsx');
import RecentNavItems from '../../../navbar/recent.navitem.jsx'; const RecentNavItem = require('../../../navbar/recent.navitem.jsx').both;
const { both: RecentNavItem } = RecentNavItems; const Account = require('../../../navbar/account.navitem.jsx');
import Account from '../../../navbar/account.navitem.jsx';
const UIPage = createReactClass({ const UIPage = createClass({
displayName : 'UIPage', displayName : 'UIPage',
render : function(){ render : function(){
@@ -36,4 +35,4 @@ const UIPage = createReactClass({
} }
}); });
export default UIPage; module.exports = UIPage;

View File

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

View File

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

View File

@@ -40,4 +40,4 @@ function LockNotification(props) {
</Dialog>; </Dialog>;
}; };
export default LockNotification; module.exports = LockNotification;

View File

@@ -1,8 +1,8 @@
import './errorPage.less'; require('./errorPage.less');
import React from 'react'; const React = require('react');
import UIPage from '../basePages/uiPage/uiPage.jsx'; const UIPage = require('../basePages/uiPage/uiPage.jsx');
import Markdown from '../../../../shared/markdown.js'; import Markdown from '../../../../shared/naturalcrit/markdown.js';
import ErrorIndex from './errors/errorIndex.js'; const ErrorIndex = require('./errors/errorIndex.js');
const ErrorPage = ({ brew })=>{ const ErrorPage = ({ brew })=>{
// Retrieving the error text based on the brew's error code from ErrorIndex // Retrieving the error text based on the brew's error code from ErrorIndex
@@ -22,4 +22,4 @@ const ErrorPage = ({ brew })=>{
); );
}; };
export default ErrorPage; module.exports = ErrorPage;

View File

@@ -1,4 +1,4 @@
import dedent from 'dedent'; const dedent = require('dedent-tabs').default;
const loginUrl = 'https://www.naturalcrit.com/login'; const loginUrl = 'https://www.naturalcrit.com/login';
@@ -96,7 +96,7 @@ const errorIndex = (props)=>{
**Brew Title:** ${escape(props.brew.brewTitle) || 'Unable to show title'} **Brew Title:** ${escape(props.brew.brewTitle) || 'Unable to show title'}
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${encodeURIComponent(author)})`;}).join(', ') || 'Unable to list authors'} **Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
[Click here to be redirected to the brew's share page.](/share/${props.brew.shareId})`, [Click here to be redirected to the brew's share page.](/share/${props.brew.shareId})`,
@@ -111,7 +111,7 @@ const errorIndex = (props)=>{
**Brew Title:** ${escape(props.brew.brewTitle) || 'Unable to show title'} **Brew Title:** ${escape(props.brew.brewTitle) || 'Unable to show title'}
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${encodeURIComponent(author)})`;}).join(', ') || 'Unable to list authors'} **Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
[Click here to be redirected to the brew's share page.](/share/${props.brew.shareId})`, [Click here to be redirected to the brew's share page.](/share/${props.brew.shareId})`,
@@ -196,12 +196,6 @@ const errorIndex = (props)=>{
**Brew ID:** ${props.brew.brewId}`, **Brew ID:** ${props.brew.brewId}`,
// Database Connection Lost
'13' : dedent`
## Database connection has been lost.
The server could not communicate with the database.`,
//account page when account is not defined //account page when account is not defined
'50' : dedent` '50' : dedent`
## You are not signed in ## You are not signed in
@@ -222,7 +216,7 @@ const errorIndex = (props)=>{
**Brew Title:** ${escape(props.brew.brewTitle)} **Brew Title:** ${escape(props.brew.brewTitle)}
**Brew Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${encodeURIComponent(author)})`;}).join(', ') || 'Unable to list authors'}`, **Brew Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}`,
// ####### Admin page error ####### // ####### Admin page error #######
'52' : dedent` '52' : dedent`
@@ -268,4 +262,4 @@ const errorIndex = (props)=>{
}; };
}; };
export default errorIndex; module.exports = errorIndex;

View File

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

View File

@@ -35,14 +35,6 @@
.navItem.save { .navItem.save {
background-color : @orange; background-color : @orange;
transition:all 0.2s;
&:hover { background-color : @green; } &:hover { background-color : @green; }
&.neverSaved {
translate:-100%;
opacity: 0;
background-color :#333;
cursor:auto;
}
} }
} }

View File

@@ -1,280 +1,275 @@
/* eslint-disable max-lines */ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
import './newPage.less'; require('./newPage.less');
const React = require('react');
// Common imports const createClass = require('create-react-class');
import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import Markdown from '../../../../shared/markdown.js';
import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; import Markdown from 'naturalcrit/markdown.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
import SplitPane from '../../../components/splitPane/splitPane.jsx'; const Nav = require('naturalcrit/nav/nav.jsx');
import Editor from '../../editor/editor.jsx'; const PrintNavItem = require('../../navbar/print.navitem.jsx');
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; const Navbar = require('../../navbar/navbar.jsx');
const AccountNavItem = require('../../navbar/account.navitem.jsx');
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const HelpNavItem = require('../../navbar/help.navitem.jsx');
import Nav from '../../navbar/nav.jsx'; const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
import Navbar from '../../navbar/navbar.jsx'; const Editor = require('../../editor/editor.jsx');
import NewBrewItem from '../../navbar/newbrew.navitem.jsx'; const BrewRenderer = require('../../brewRenderer/brewRenderer.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 RecentNavItems from '../../navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
// Page specific imports const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
import { Meta } from 'vitreum/headtags'; const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
const BREWKEY = 'HB_newPage_content'; const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'HB_newPage_style'; const STYLEKEY = 'homebrewery-new-style';
const METAKEY = 'HB_newPage_metadata'; const METAKEY = 'homebrewery-new-meta';
const SNIPKEY = 'HB_newPage_snippets'; let SAVEKEY;
const SAVEKEYPREFIX = 'HB_editor_defaultSave_';
const useLocalStorage = true;
const neverSaved = true;
const NewPage = (props)=>{ const NewPage = createClass({
props = { displayName : 'NewPage',
brew : DEFAULT_BREW, getDefaultProps : function() {
...props return {
brew : DEFAULT_BREW
}; };
},
const [currentBrew , setCurrentBrew ] = useState(props.brew); getInitialState : function() {
const [isSaving , setIsSaving ] = useState(false); const brew = this.props.brew;
const [saveGoogle , setSaveGoogle ] = useState(global.account?.googleId ? true : false);
const [error , setError ] = useState(null);
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle ] = useState({});
const [unsavedChanges , setUnsavedChanges ] = useState(false);
const [autoSaveEnabled , setAutoSaveEnabled ] = useState(false);
const editorRef = useRef(null); return {
const lastSavedBrew = useRef(_.cloneDeep(props.brew)); brew : brew,
// const saveTimeout = useRef(null); isSaving : false,
// const warnUnsavedTimeout = useRef(null); saveGoogle : (global.account && global.account.googleId ? true : false),
const trySaveRef = useRef(trySave); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew error : null,
const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges htmlErrors : Markdown.validate(brew.text),
currentEditorViewPageNum : 1,
useEffect(()=>{ currentEditorCursorPageNum : 1,
loadBrew(); currentBrewRendererPageNum : 1,
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); themeBundle : {}
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); editor : React.createRef(null),
return ()=>{ componentDidMount : function() {
document.removeEventListener('keydown', handleControlKeys); document.addEventListener('keydown', this.handleControlKeys);
};
}, []);
const loadBrew = ()=>{ const brew = this.state.brew;
const brew = { ...currentBrew };
if(!brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser if(!this.props.brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser
const brewStorage = localStorage.getItem(BREWKEY); const brewStorage = localStorage.getItem(BREWKEY);
const styleStorage = localStorage.getItem(STYLEKEY); const styleStorage = localStorage.getItem(STYLEKEY);
const metaStorage = JSON.parse(localStorage.getItem(METAKEY)); const metaStorage = JSON.parse(localStorage.getItem(METAKEY));
brew.text = brewStorage ?? brew.text; brew.text = brewStorage ?? brew.text;
brew.style = styleStorage ?? brew.style; brew.style = styleStorage ?? brew.style;
// brew.title = metaStorage?.title || this.state.brew.title;
// brew.description = metaStorage?.description || this.state.brew.description;
brew.renderer = metaStorage?.renderer ?? brew.renderer; brew.renderer = metaStorage?.renderer ?? brew.renderer;
brew.theme = metaStorage?.theme ?? brew.theme; brew.theme = metaStorage?.theme ?? brew.theme;
brew.lang = metaStorage?.lang ?? brew.lang; brew.lang = metaStorage?.lang ?? brew.lang;
} }
const SAVEKEY = `${SAVEKEYPREFIX}${global.account?.username}`; SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY'; const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
setCurrentBrew(brew); this.setState({
lastSavedBrew.current = brew; brew : brew,
setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && saveGoogle); saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
});
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
localStorage.setItem(BREWKEY, brew.text); localStorage.setItem(BREWKEY, brew.text);
if(brew.style) if(brew.style)
localStorage.setItem(STYLEKEY, brew.style); localStorage.setItem(STYLEKEY, brew.style);
localStorage.setItem(METAKEY, JSON.stringify({ renderer: brew.renderer, theme: brew.theme, lang: brew.lang })); localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
if(window.location.pathname !== '/new') if(window.location.pathname != '/new') {
window.history.replaceState({}, window.location.title, '/new/'); window.history.replaceState({}, window.location.title, '/new/');
};
useEffect(()=>{
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
setUnsavedChanges(hasChange);
if(autoSaveEnabled) trySave(false, hasChange);
}, [currentBrew]);
useEffect(()=>{
trySaveRef.current = trySave;
unsavedChangesRef.current = unsavedChanges;
});
const handleSplitMove = ()=>{
editorRef.current.update();
};
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
if(subfield == 'renderer' || subfield == 'theme')
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
//If there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
setHTMLErrors(Markdown.validate(value));
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value);
if(field == 'style') localStorage.setItem(STYLEKEY, value);
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
renderer : value.renderer,
theme : value.theme,
lang : value.lang
}));
} }
}; },
componentWillUnmount : function() {
document.removeEventListener('keydown', this.handleControlKeys);
},
const trySave = async ()=>{ handleControlKeys : function(e){
setIsSaving(true); if(!(e.ctrlKey || e.metaKey)) return;
const S_KEY = 83;
const P_KEY = 80;
if(e.keyCode == S_KEY) this.save();
if(e.keyCode == P_KEY) printCurrentBrew();
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
e.stopPropagation();
e.preventDefault();
}
},
const updatedBrew = { ...currentBrew }; handleSplitMove : function(){
splitTextStyleAndMetadata(updatedBrew); this.editor.current.update();
},
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm; handleEditorViewPageChange : function(pageNumber){
updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1; this.setState({ currentEditorViewPageNum: pageNumber });
},
const res = await request handleEditorCursorPageChange : function(pageNumber){
.post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`) this.setState({ currentEditorCursorPageNum: pageNumber });
.send(updatedBrew) },
.catch((err)=>{
setIsSaving(false); handleBrewRendererPageChange : function(pageNumber){
setError(err); this.setState({ currentBrewRendererPageNum: pageNumber });
},
handleTextChange : function(text){
//If there are errors, run the validator on every change to give quick feedback
let htmlErrors = this.state.htmlErrors;
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState((prevState)=>({
brew : { ...prevState.brew, text: text },
htmlErrors : htmlErrors,
}));
localStorage.setItem(BREWKEY, text);
},
handleStyleChange : function(style){
this.setState((prevState)=>({
brew : { ...prevState.brew, style: style },
}));
localStorage.setItem(STYLEKEY, style);
},
handleSnipChange : function(snippet){
//If there are errors, run the validator on every change to give quick feedback
let htmlErrors = this.state.htmlErrors;
if(htmlErrors.length) htmlErrors = Markdown.validate(snippet);
this.setState((prevState)=>({
brew : { ...prevState.brew, snippets: snippet },
htmlErrors : htmlErrors,
}), ()=>{if(this.state.autoSave) this.trySave();});
},
handleMetaChange : function(metadata, field=undefined){
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
fetchThemeBundle(this, metadata.renderer, metadata.theme);
this.setState((prevState)=>({
brew : { ...prevState.brew, ...metadata },
}), ()=>{
localStorage.setItem(METAKEY, JSON.stringify({
// 'title' : this.state.brew.title,
// 'description' : this.state.brew.description,
'renderer' : this.state.brew.renderer,
'theme' : this.state.brew.theme,
'lang' : this.state.brew.lang
}));
});
;
},
save : async function(){
this.setState({
isSaving : true
}); });
setIsSaving(false); let brew = this.state.brew;
// Split out CSS to Style if CSS codefence exists
if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) {
const index = brew.text.indexOf('```\n\n');
brew.style = `${brew.style ? `${brew.style}\n` : ''}${brew.text.slice(7, index - 1)}`;
brew.text = brew.text.slice(index + 5);
}
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
const res = await request
.post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
.send(brew)
.catch((err)=>{
this.setState({ isSaving: false, error: err });
});
if(!res) return; if(!res) return;
const savedBrew = res.body; brew = res.body;
localStorage.removeItem(BREWKEY); localStorage.removeItem(BREWKEY);
localStorage.removeItem(STYLEKEY); localStorage.removeItem(STYLEKEY);
localStorage.removeItem(METAKEY); localStorage.removeItem(METAKEY);
window.location = `/edit/${savedBrew.editId}`; window.location = `/edit/${brew.editId}`;
}; },
const renderSaveButton = ()=>{ renderSaveButton : function(){
// #1 - Currently saving, show SAVING if(this.state.isSaving){
if(isSaving) return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>; save...
</Nav.item>;
} else {
return <Nav.item icon='fas fa-save' className='save' onClick={this.save}>
save
</Nav.item>;
}
},
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING renderNavbar : function(){
// if(unsavedChanges && warnUnsavedChanges) { return <Navbar>
// 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={trySave} color='blue' icon='fas fa-save'>save now</Nav.item>;
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
if(autoSaveEnabled)
return <Nav.item className='save saved'>auto-saved</Nav.item>;
// #5 - No unsaved changes, and has never been saved, hide the button
if(neverSaved)
return <Nav.item className='save neverSaved'>save now</Nav.item>;
// DEFAULT - No unsaved changes, show SAVED
return <Nav.item className='save saved'>saved</Nav.item>;
};
const clearError = ()=>{
setError(null);
setIsSaving(false);
};
const renderNavbar = ()=>(
<Navbar>
<Nav.section> <Nav.section>
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item> <Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
</Nav.section> </Nav.section>
<Nav.section> <Nav.section>
{error {this.state.error ?
? <ErrorNavItem error={error} clearError={clearError} /> <ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
: renderSaveButton()} this.renderSaveButton()
<NewBrewItem /> }
<PrintNavItem /> <PrintNavItem />
<HelpNavItem /> <HelpNavItem />
<VaultNavItem />
<RecentNavItem /> <RecentNavItem />
<AccountNavItem /> <AccountNavItem />
</Nav.section> </Nav.section>
</Navbar> </Navbar>;
); },
return ( render : function(){
<div className='newPage sitePage'> return <div className='newPage sitePage'>
{renderNavbar()} {this.renderNavbar()}
<div className='content'> <div className='content'>
<SplitPane onDragFinish={handleSplitMove}> <SplitPane onDragFinish={this.handleSplitMove}>
<Editor <Editor
ref={editorRef} ref={this.editor}
brew={currentBrew} brew={this.state.brew}
onBrewChange={handleBrewChange} onTextChange={this.handleTextChange}
renderer={currentBrew.renderer} onStyleChange={this.handleStyleChange}
userThemes={props.userThemes} onMetaChange={this.handleMetaChange}
themeBundle={themeBundle} onSnipChange={this.handleSnipChange}
onCursorPageChange={setCurrentEditorCursorPageNum} renderer={this.state.brew.renderer}
onViewPageChange={setCurrentEditorViewPageNum} userThemes={this.props.userThemes}
currentEditorViewPageNum={currentEditorViewPageNum} themeBundle={this.state.themeBundle}
currentEditorCursorPageNum={currentEditorCursorPageNum} onCursorPageChange={this.handleEditorCursorPageChange}
currentBrewRendererPageNum={currentBrewRendererPageNum} onViewPageChange={this.handleEditorViewPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
/> />
<BrewRenderer <BrewRenderer
text={currentBrew.text} text={this.state.brew.text}
style={currentBrew.style} style={this.state.brew.style}
renderer={currentBrew.renderer} renderer={this.state.brew.renderer}
theme={currentBrew.theme} theme={this.state.brew.theme}
themeBundle={themeBundle} themeBundle={this.state.themeBundle}
errors={HTMLErrors} errors={this.state.htmlErrors}
lang={currentBrew.lang} lang={this.state.brew.lang}
onPageChange={setCurrentBrewRendererPageNum} onPageChange={this.handleBrewRendererPageChange}
currentEditorViewPageNum={currentEditorViewPageNum} currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum} currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum} currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
allowPrint={true} allowPrint={true}
/> />
</SplitPane> </SplitPane>
</div> </div>
</div> </div>;
); }
}; });
export default NewPage; module.exports = NewPage;

View File

@@ -1,15 +1,6 @@
.newPage { .newPage {
.navItem.save { .navItem.save {
background-color : @orange; background-color : @orange;
transition:all 0.2s;
&:hover { background-color : @green; } &:hover { background-color : @green; }
&.neverSaved {
translate:-100%;
opacity: 0;
background-color :#333;
cursor:auto;
} }
} }
}

View File

@@ -1,27 +1,31 @@
import './sharePage.less'; require('./sharePage.less');
import React, { useState, useEffect, useCallback } from 'react'; const React = require('react');
import { Meta } from 'vitreum/headtags'; const { useState, useEffect, useCallback } = React;
const { Meta } = require('vitreum/headtags');
import Nav from '../../navbar/nav.jsx'; const Nav = require('naturalcrit/nav/nav.jsx');
import Navbar from '../../navbar/navbar.jsx'; const Navbar = require('../../navbar/navbar.jsx');
import MetadataNav from '../../navbar/metadata.navitem.jsx'; const MetadataNav = require('../../navbar/metadata.navitem.jsx');
import PrintNavItem from '../../navbar/print.navitem.jsx'; const PrintNavItem = require('../../navbar/print.navitem.jsx');
import RecentNavItems from '../../navbar/recent.navitem.jsx'; const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const { both: RecentNavItem } = RecentNavItems; const Account = require('../../navbar/account.navitem.jsx');
import Account from '../../navbar/account.navitem.jsx'; const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
import { printCurrentBrew, fetchThemeBundle } from '../../../../shared/helpers.js'; const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
const SharePage = (props)=>{ const SharePage = (props)=>{
const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props; const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props;
const [themeBundle, setThemeBundle] = useState({}); const [state, setState] = useState({
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); themeBundle : {},
currentBrewRendererPageNum : 1,
});
const handleBrewRendererPageChange = useCallback((pageNumber)=>{ const handleBrewRendererPageChange = useCallback((pageNumber)=>{
setCurrentBrewRendererPageNum(pageNumber); setState((prevState)=>({
currentBrewRendererPageNum : pageNumber,
...prevState }));
}, []); }, []);
const handleControlKeys = (e)=>{ const handleControlKeys = (e)=>{
@@ -36,7 +40,11 @@ const SharePage = (props)=>{
useEffect(()=>{ useEffect(()=>{
document.addEventListener('keydown', handleControlKeys); document.addEventListener('keydown', handleControlKeys);
fetchThemeBundle(undefined, setThemeBundle, brew.renderer, brew.theme); fetchThemeBundle(
{ setState },
brew.renderer,
brew.theme
);
return ()=>{ return ()=>{
document.removeEventListener('keydown', handleControlKeys); document.removeEventListener('keydown', handleControlKeys);
@@ -106,9 +114,9 @@ const SharePage = (props)=>{
lang={brew.lang} lang={brew.lang}
renderer={brew.renderer} renderer={brew.renderer}
theme={brew.theme} theme={brew.theme}
themeBundle={themeBundle} themeBundle={state.themeBundle}
onPageChange={handleBrewRendererPageChange} onPageChange={handleBrewRendererPageChange}
currentBrewRendererPageNum={currentBrewRendererPageNum} currentBrewRendererPageNum={state.currentBrewRendererPageNum}
allowPrint={true} allowPrint={true}
/> />
</div> </div>
@@ -116,4 +124,4 @@ const SharePage = (props)=>{
); );
}; };
export default SharePage; module.exports = SharePage;

View File

@@ -1,17 +1,17 @@
import React, { useState } from 'react'; const React = require('react');
import _ from 'lodash'; const { useState } = React;
const _ = require('lodash');
import ListPage from '../basePages/listPage/listPage.jsx'; const ListPage = require('../basePages/listPage/listPage.jsx');
import Nav from '../../navbar/nav.jsx'; const Nav = require('naturalcrit/nav/nav.jsx');
import Navbar from '../../navbar/navbar.jsx'; const Navbar = require('../../navbar/navbar.jsx');
import RecentNavItems from '../../navbar/recent.navitem.jsx'; const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const { both: RecentNavItem } = RecentNavItems; const Account = require('../../navbar/account.navitem.jsx');
import Account from '../../navbar/account.navitem.jsx'; const NewBrew = require('../../navbar/newbrew.navitem.jsx');
import NewBrew from '../../navbar/newbrew.navitem.jsx'; const HelpNavItem = require('../../navbar/help.navitem.jsx');
import HelpNavItem from '../../navbar/help.navitem.jsx'; const ErrorNavItem = require('../../navbar/error-navitem.jsx');
import ErrorNavItem from '../../navbar/error-navitem.jsx'; const VaultNavitem = require('../../navbar/vault.navitem.jsx');
import VaultNavitem from '../../navbar/vault.navitem.jsx';
const UserPage = (props)=>{ const UserPage = (props)=>{
props = { props = {
@@ -39,14 +39,10 @@ const UserPage = (props)=>{
}] : []) }] : [])
]; ];
const clearError = ()=>{
setError(null);
};
const navItems = ( const navItems = (
<Navbar> <Navbar>
<Nav.section> <Nav.section>
{error && (<ErrorNavItem error={error} clearError={clearError}></ErrorNavItem>)} {error && (<ErrorNavItem error={error} parent={null}></ErrorNavItem>)}
<NewBrew /> <NewBrew />
<HelpNavItem /> <HelpNavItem />
<VaultNavitem /> <VaultNavitem />
@@ -61,4 +57,4 @@ const UserPage = (props)=>{
); );
}; };
export default UserPage; module.exports = UserPage;

View File

@@ -1,18 +1,19 @@
/*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/
/*eslint max-params:["warn", { max: 10 }], */ /*eslint max-params:["warn", { max: 10 }], */
import './vaultPage.less'; require('./vaultPage.less');
import React, { useState, useEffect, useRef } from 'react';
import Nav from '../../navbar/nav.jsx'; const React = require('react');
import Navbar from '../../navbar/navbar.jsx'; const { useState, useEffect, useRef } = React;
import RecentNavItems from '../../navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems; const Nav = require('naturalcrit/nav/nav.jsx');
import Account from '../../navbar/account.navitem.jsx'; const Navbar = require('../../navbar/navbar.jsx');
import NewBrew from '../../navbar/newbrew.navitem.jsx'; const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
import HelpNavItem from '../../navbar/help.navitem.jsx'; const Account = require('../../navbar/account.navitem.jsx');
import BrewItem from '../basePages/listPage/brewItem/brewItem.jsx'; const NewBrew = require('../../navbar/newbrew.navitem.jsx');
import SplitPane from '../../../components/splitPane/splitPane.jsx'; const HelpNavItem = require('../../navbar/help.navitem.jsx');
import ErrorIndex from '../errorPage/errors/errorIndex.js'; const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
const SplitPane = require('../../../../shared/naturalcrit/splitPane/splitPane.jsx');
const ErrorIndex = require('../errorPage/errors/errorIndex.js');
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
@@ -100,7 +101,7 @@ const VaultPage = (props)=>{
const title = titleRef.current.value || ''; const title = titleRef.current.value || '';
const author = authorRef.current.value || ''; const author = authorRef.current.value || '';
const count = countRef.current.value || 20; const count = countRef.current.value || 10;
const v3 = v3Ref.current.checked != false; const v3 = v3Ref.current.checked != false;
const legacy = legacyRef.current.checked != false; const legacy = legacyRef.current.checked != false;
const sortOption = sort || 'title'; const sortOption = sort || 'title';
@@ -287,8 +288,7 @@ const VaultPage = (props)=>{
const renderPaginationControls = ()=>{ const renderPaginationControls = ()=>{
if(!totalBrews || totalBrews < 10) return null; if(!totalBrews || totalBrews < 10) return null;
const countInt = parseInt(brewCollection.length || 20);
const countInt = parseInt(countRef.current.value || 20);
const totalPages = Math.ceil(totalBrews / countInt); const totalPages = Math.ceil(totalBrews / countInt);
let startPage, endPage; let startPage, endPage;
@@ -429,4 +429,4 @@ const VaultPage = (props)=>{
); );
}; };
export default VaultPage; module.exports = VaultPage;

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
{ {
"development": true,
"host" : "homebrewery.local.naturalcrit.com:8000", "host" : "homebrewery.local.naturalcrit.com:8000",
"naturalcrit_url" : "local.naturalcrit.com:8010", "naturalcrit_url" : "local.naturalcrit.com:8010",
"secret" : "secret", "secret" : "secret",
"web_port" : 8000, "web_port" : 8000,
"enable_v3" : true,
"enable_themes" : true,
"local_environments" : ["docker", "local"], "local_environments" : ["docker", "local"],
"publicUrl" : "https://homebrewery.naturalcrit.com", "publicUrl" : "https://homebrewery.naturalcrit.com",
"hb_images" : null, "hb_images" : null,

View File

@@ -32,7 +32,7 @@ export default [{
"max-depth" : ["warn", { max: 4 }], "max-depth" : ["warn", { max: 4 }],
"max-params" : ["warn", { max: 5 }], "max-params" : ["warn", { max: 5 }],
"no-restricted-syntax" : ["warn", "ClassDeclaration", "SwitchStatement"], "no-restricted-syntax" : ["warn", "ClassDeclaration", "SwitchStatement"],
"no-unused-vars" : ["warn", { vars: "all", args: "none", varsIgnorePattern: "config|_|cx|createReactClass" }], "no-unused-vars" : ["warn", { vars: "all", args: "none", varsIgnorePattern: "config|_|cx|createClass" }],
"react/jsx-uses-vars" : "warn", "react/jsx-uses-vars" : "warn",
/** Fixable **/ /** Fixable **/

5991
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "homebrewery", "name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown", "description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.20.1", "version": "3.19.3",
"type": "module", "type": "module",
"engines": { "engines": {
"npm": "^10.8.x", "npm": "^10.8.x",
@@ -44,9 +44,7 @@
"phb": "node --experimental-require-module scripts/phb.js", "phb": "node --experimental-require-module scripts/phb.js",
"prod": "set NODE_ENV=production && npm run build", "prod": "set NODE_ENV=production && npm run build",
"postinstall": "npm run build", "postinstall": "npm run build",
"start": "node --experimental-require-module server.js", "start": "node --experimental-require-module server.js"
"docker:build": "docker build -t ${DOCKERID}/homebrewery:$npm_package_version .",
"docker:publish": "docker login && docker push ${DOCKERID}/homebrewery:$npm_package_version"
}, },
"author": "stolksdorf", "author": "stolksdorf",
"license": "MIT", "license": "MIT",
@@ -61,11 +59,8 @@
"server" "server"
], ],
"transformIgnorePatterns": [ "transformIgnorePatterns": [
"node_modules/(?!(nanoid|@exodus/bytes|parse5)/)" "node_modules/(?!nanoid/).*"
], ],
"transform": {
"^.+\\.js$": "babel-jest"
},
"coveragePathIgnorePatterns": [ "coveragePathIgnorePatterns": [
"build/*" "build/*"
], ],
@@ -88,53 +83,52 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.28.4", "@babel/core": "^7.27.1",
"@babel/plugin-transform-runtime": "^7.28.3", "@babel/plugin-transform-runtime": "^7.28.0",
"@babel/preset-env": "^7.28.3", "@babel/preset-env": "^7.28.0",
"@babel/preset-react": "^7.28.5", "@babel/preset-react": "^7.27.1",
"@babel/runtime": "^7.28.4", "@babel/runtime": "^7.27.6",
"@dmsnell/diff-match-patch": "^1.1.0", "@dmsnell/diff-match-patch": "^1.1.0",
"@googleapis/drive": "^19.2.0", "@googleapis/drive": "^13.0.1",
"@sanity/diff-match-patch": "^3.2.0", "@sanity/diff-match-patch": "^3.2.0",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"codemirror": "^5.65.6", "codemirror": "^5.65.6",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"core-js": "^3.47.0", "core-js": "^3.44.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"create-react-class": "^15.7.0", "create-react-class": "^15.7.0",
"dedent": "^1.7.1", "dedent-tabs": "^0.10.3",
"expr-eval": "^2.0.2", "expr-eval": "^2.0.2",
"express": "^5.1.0", "express": "^5.1.0",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-static-gzip": "3.0.0", "express-static-gzip": "3.0.0",
"fflate": "^0.8.2", "fflate": "^0.8.2",
"fs-extra": "11.3.2", "fs-extra": "11.3.0",
"hash-wasm": "^4.12.0", "hash-wasm": "^4.12.0",
"idb-keyval": "^6.2.2", "idb-keyval": "^6.2.2",
"js-yaml": "^4.1.1", "js-yaml": "^4.1.0",
"jwt-simple": "^0.5.6", "jwt-simple": "^0.5.6",
"less": "^3.13.1", "less": "^3.13.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "15.0.12", "marked": "15.0.12",
"marked-alignment-paragraphs": "^1.0.0", "marked-alignment-paragraphs": "^1.0.0",
"marked-definition-lists": "^1.0.1", "marked-definition-lists": "^1.0.1",
"marked-emoji": "^2.0.2", "marked-emoji": "^2.0.1",
"marked-extended-tables": "^2.0.1", "marked-extended-tables": "^2.0.1",
"marked-gfm-heading-id": "^4.1.3", "marked-gfm-heading-id": "^4.1.2",
"marked-nonbreaking-spaces": "^1.0.1", "marked-nonbreaking-spaces": "^1.0.1",
"marked-smartypants-lite": "^1.0.3", "marked-smartypants-lite": "^1.0.3",
"marked-subsuper-text": "^1.0.4", "marked-subsuper-text": "^1.0.3",
"marked-variables": "^1.0.4",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1", "moment": "^2.30.1",
"mongoose": "^8.20.0", "mongoose": "^8.16.3",
"nanoid": "5.1.6", "nanoid": "5.1.5",
"nconf": "^0.13.0", "nconf": "^0.13.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-frame-component": "^4.1.3", "react-frame-component": "^4.1.3",
"react-router": "^7.9.6", "react-router": "^7.6.3",
"romans": "^3.1.0", "romans": "^3.1.0",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"superagent": "^10.2.1", "superagent": "^10.2.1",
@@ -142,21 +136,19 @@
"written-number": "^0.11.1" "written-number": "^0.11.1"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/stylelint-plugin": "^4.0.0", "@stylistic/stylelint-plugin": "^3.1.3",
"babel-jest": "^30.2.0",
"babel-plugin-transform-import-meta": "^2.3.3", "babel-plugin-transform-import-meta": "^2.3.3",
"eslint": "^9.39.1", "eslint": "^9.31.0",
"eslint-plugin-jest": "^29.1.0", "eslint-plugin-jest": "^29.0.1",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"globals": "^16.4.0", "globals": "^16.3.0",
"jest": "^30.2.0", "jest": "^30.0.4",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",
"jsdom": "^27.4.0",
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",
"stylelint": "^16.25.0", "stylelint": "^16.21.1",
"stylelint-config-recess-order": "^7.3.0", "stylelint-config-recess-order": "^7.1.0",
"stylelint-config-recommended": "^17.0.0", "stylelint-config-recommended": "^16.0.0",
"supertest": "^7.1.4" "supertest": "^7.1.3"
} }
} }

View File

@@ -161,7 +161,7 @@ fs.emptyDirSync('./build');
livereload('./build'); // Install the Chrome extension LiveReload to automatically refresh the browser livereload('./build'); // Install the Chrome extension LiveReload to automatically refresh the browser
watchFile('./server.js', { // Restart server when change detected to this file or any nested directory from here watchFile('./server.js', { // Restart server when change detected to this file or any nested directory from here
ignore : ['./build', './client', './themes'], // Ignore folders that are not running server code / avoids unneeded restarts ignore : ['./build', './client', './themes'], // Ignore folders that are not running server code / avoids unneeded restarts
ext : 'js json' // Extensions to watch (only .js/.json by default) ext : 'js' // Extensions to watch (only .js/.json by default)
//watch : ['./server', './themes'], // Watch additional folders if needed //watch : ['./server', './themes'], // Watch additional folders if needed
}); });
} }

View File

@@ -35,7 +35,6 @@ import contentNegotiation from './middleware/content-negotiation.js';
import bodyParser from 'body-parser'; import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser'; import cookieParser from 'cookie-parser';
import forceSSL from './forcessl.mw.js'; import forceSSL from './forcessl.mw.js';
import dbCheck from './middleware/dbCheck.js';
const sanitizeBrew = (brew, accessType)=>{ const sanitizeBrew = (brew, accessType)=>{
@@ -148,6 +147,109 @@ app.get('/', (req, res, next)=>{
return next(); return next();
}); });
app.get('/analyze', async (req, res, next) => {
const accounts = JSON.parse(fs.readFileSync('accounts.json', 'utf8'));
let totalBrewsStubbed = accounts.reduce((sum, account) => {
if (account.brewsStubbed) {
return sum + account.brewsStubbed;
}
return sum;
}, 0);
let totalAccountsProcessed = accounts.filter(account => account.fullyProcessed).length;
let totalAccountsWithInvalidCredentials = accounts.filter(account => account.invalidCredentials).length;
console.log(`Total Brews Stubbed: ${totalBrewsStubbed}`);
console.log(`Total Accounts Processed: ${totalAccountsProcessed}`);
console.log(`Total Accounts with Invalid Credentials: ${totalAccountsWithInvalidCredentials}`);
});
app.get('/destroy', async (req, res, next) => {
const accounts = JSON.parse(fs.readFileSync('accounts.json', 'utf8'));
let updated = false;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
for (let i = 40000; i < accounts.length; i++) {
if (accounts[i].fullyProcessed || accounts[i].invalidCredentials) continue;
const originalAccount = { ...accounts[i] };
const account = accounts[i];
console.log(`Processing account: ${account.username}`);
let googleBrews;
let auth;
try {
auth = await GoogleActions.authCheck(account, res);
googleBrews = await GoogleActions.listGoogleBrews(auth);
} catch (err) {
console.error(`Auth error for ${account.username}`);
accounts[i] = { ...originalAccount, invalidCredentials: true };
updated = true;
continue;
}
if (!auth) {
accounts[i] = { ...originalAccount, missingAuth: true };
updated = true;
continue;
}
console.log('Google Brews:', googleBrews.length);
if (googleBrews.length === 0) {
accounts[i] = { ...originalAccount, fullyProcessed: true, brewsStubbed: 0 };
updated = true;
continue;
}
const fields = ['googleId', 'title', 'editId', 'shareId'];
let brews = await HomebrewModel.getByUser(account.username, true, fields).catch(err => console.error(err));
const stubbedEditIds = new Set(brews.map(b => b.editId));
googleBrews = googleBrews.filter(b => !stubbedEditIds.has(b.editId));
console.log('Unstubbed Google Brews:', googleBrews.length);
const results = await Promise.all(
googleBrews.map(async (brew) => {
let brewFromServer = await GoogleActions.getGoogleBrew(auth, brew.googleId, brew.editId, 'edit');
splitTextStyleAndMetadata(brewFromServer);
brewFromServer.authors = [account.username];
api.excludeStubProps(brewFromServer);
console.log(`Trying to Stub: ${brewFromServer.title} (${brewFromServer.shareId})`);
let saved = await new HomebrewModel(brewFromServer).save().catch(err => console.error(err));
if (saved) {
console.log(`Saved Stub: ${saved.title} (${saved.shareId})`);
return true;
}
return false;
})
);
const stubCount = results.filter(Boolean).length;
console.log('Brews stubbed:', stubCount);
accounts[i] = {
...originalAccount,
brewsStubbed: stubCount,
fullyProcessed: stubCount === googleBrews.length
};
updated = true;
sleep(1000);
}
if (updated) {
fs.writeFileSync('accounts.json', JSON.stringify(accounts, null, 2), 'utf8');
console.log('accounts.json updated');
}
res.send('One account processed');
});
//Home page Legacy //Home page Legacy
app.get('/legacy', (req, res, next)=>{ app.get('/legacy', (req, res, next)=>{
req.brew = { req.brew = {
@@ -275,7 +377,7 @@ app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{
app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);}); app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);});
//User Page //User Page
app.get('/user/:username', dbCheck, async (req, res, next)=>{ app.get('/user/:username', async (req, res, next)=>{
const ownAccount = req.account && (req.account.username == req.params.username); const ownAccount = req.account && (req.account.username == req.params.username);
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,
@@ -347,7 +449,7 @@ app.get('/user/:username', dbCheck, async (req, res, next)=>{
}); });
//Change author name on brews //Change author name on brews
app.put('/api/user/rename', dbCheck, async (req, res)=>{ app.put('/api/user/rename', async (req, res)=>{
const { username, newUsername } = req.body; const { username, newUsername } = req.body;
const ownAccount = req.account && (req.account.username == newUsername); const ownAccount = req.account && (req.account.username == newUsername);
@@ -433,7 +535,7 @@ app.get('/new', asyncHandler(async(req, res, next)=>{
})); }));
//Share Page //Share Page
app.get('/share/:id', dbCheck, asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
const { brew } = req; const { brew } = req;
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,
title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`, title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`,
@@ -460,7 +562,7 @@ app.get('/share/:id', dbCheck, asyncHandler(getBrew('share')), asyncHandler(asyn
})); }));
//Account Page //Account Page
app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{ app.get('/account', asyncHandler(async (req, res, next)=>{
const data = {}; const data = {};
data.title = 'Account Information Page'; data.title = 'Account Information Page';
@@ -488,8 +590,8 @@ app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{
const query = { authors: req.account.username, googleId: { $exists: false } }; const query = { authors: req.account.username, googleId: { $exists: false } };
const mongoCount = await HomebrewModel.countDocuments(query) const mongoCount = await HomebrewModel.countDocuments(query)
.catch((err)=>{ .catch((err)=>{
mongoCount = 0;
console.log(err); console.log(err);
return 0;
}); });
data.accountDetails = { data.accountDetails = {
@@ -563,6 +665,8 @@ const renderPage = async (req, res)=>{
brews : req.brews, brews : req.brews,
googleBrews : req.googleBrews, googleBrews : req.googleBrews,
account : req.account, account : req.account,
enable_v3 : config.get('enable_v3'),
enable_themes : config.get('enable_themes'),
config : configuration, config : configuration,
ogMeta : req.ogMeta, ogMeta : req.ogMeta,
userThemes : req.userThemes userThemes : req.userThemes

View File

@@ -8,7 +8,8 @@
import Mongoose from 'mongoose'; import Mongoose from 'mongoose';
const getMongoDBURL = (config)=>{ const getMongoDBURL = (config)=>{
return config.get('mongodb_uri') || console.log('mongodb uri', config.get('MONGODB_URI'));
return config.get('MONGODB_URI') ||
config.get('mongolab_uri') || config.get('mongolab_uri') ||
'mongodb://127.0.0.1/homebrewery'; // changed from mongodb://localhost/homebrewery to accommodate versions 16+ of node. 'mongodb://127.0.0.1/homebrewery'; // changed from mongodb://localhost/homebrewery to accommodate versions 16+ of node.
}; };
@@ -22,24 +23,12 @@ const handleConnectionError = (error)=>{
} }
}; };
const addListeners = (conn)=>{
conn.connection.on('disconnecting', ()=>{console.log('Mongo disconnecting...');});
conn.connection.on('disconnected', ()=>{console.log('Mongo disconnected!');});
conn.connection.on('connecting', ()=>{console.log('Mongo connecting...');});
conn.connection.on('connected', ()=>{console.log('Mongo connected!');});
return conn;
};
const disconnect = async ()=>{ const disconnect = async ()=>{
return await Mongoose.disconnect(); return await Mongoose.disconnect();
}; };
const connect = async (config)=>{ const connect = async (config)=>{
return await Mongoose.connect(getMongoDBURL(config), { return await Mongoose.connect(getMongoDBURL(config), { retryWrites: false })
retryWrites : false,
autoIndex : (config.get('local_environments').includes(config.get('node_env')))
})
.then(addListeners(Mongoose))
.catch((error)=>handleConnectionError(error)); .catch((error)=>handleConnectionError(error));
}; };
@@ -47,4 +36,3 @@ export default {
connect, connect,
disconnect disconnect
}; };

View File

@@ -6,7 +6,6 @@ import config from './config.js';
let serviceAuth; let serviceAuth;
let clientEmail;
if(!config.get('service_account')){ if(!config.get('service_account')){
const reset = '\x1b[0m'; // Reset to default style const reset = '\x1b[0m'; // Reset to default style
const yellow = '\x1b[33m'; // yellow color const yellow = '\x1b[33m'; // yellow color
@@ -16,10 +15,6 @@ if(!config.get('service_account')){
JSON.parse(config.get('service_account')) : JSON.parse(config.get('service_account')) :
config.get('service_account'); config.get('service_account');
if(keys?.client_email) {
clientEmail = keys.client_email;
}
try { try {
serviceAuth = googleDrive.auth.fromJSON(keys); serviceAuth = googleDrive.auth.fromJSON(keys);
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive']; serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
@@ -232,22 +227,6 @@ const GoogleActions = {
if(!obj) return; if(!obj) return;
if(clientEmail) {
await drive.permissions.create({
resource : {
type : 'user',
emailAddress : clientEmail,
role : 'writer'
},
fileId : obj.data.id,
fields : 'id',
})
.catch((err)=>{
console.log('Error adding Service Account permissions on Google Drive file');
console.error(err);
});
}
await drive.permissions.create({ await drive.permissions.create({
resource : { type : 'anyone', resource : { type : 'anyone',
role : 'writer' }, role : 'writer' },
@@ -255,7 +234,7 @@ const GoogleActions = {
fields : 'id', fields : 'id',
}) })
.catch((err)=>{ .catch((err)=>{
console.log('Error adding "Anyone" permissions on Google Drive file'); console.log('Error updating permissions');
console.error(err); console.error(err);
}); });

View File

@@ -4,7 +4,7 @@ import { model as HomebrewModel } from './homebrew.model.js';
import express from 'express'; import express from 'express';
import zlib from 'zlib'; import zlib from 'zlib';
import GoogleActions from './googleActions.js'; import GoogleActions from './googleActions.js';
import Markdown from '../shared/markdown.js'; import Markdown from '../shared/naturalcrit/markdown.js';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import asyncHandler from 'express-async-handler'; import asyncHandler from 'express-async-handler';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
@@ -13,7 +13,6 @@ import { md5 } from 'hash-wasm';
import { splitTextStyleAndMetadata, import { splitTextStyleAndMetadata,
brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js'; brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js';
import checkClientVersion from './middleware/check-client-version.js'; import checkClientVersion from './middleware/check-client-version.js';
import dbCheck from './middleware/dbCheck.js';
const router = express.Router(); const router = express.Router();
@@ -378,7 +377,7 @@ const api = {
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error. // Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[0]); const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[0]);
if(patchedResult != brewFromClient.text) if(patchedResult != brewFromClient.text)
throw ('Patches did not apply cleanly, text mismatch detected'); throw("Patches did not apply cleanly, text mismatch detected");
// brew.text = applyPatches(patches, brewFromServer.text)[0]; // brew.text = applyPatches(patches, brewFromServer.text)[0];
} catch (err) { } catch (err) {
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`); //debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
@@ -481,7 +480,6 @@ const api = {
await HomebrewModel.deleteOne({ editId: id }); await HomebrewModel.deleteOne({ editId: id });
return next(); return next();
} }
throw(err);
} }
let brew = req.brew; let brew = req.brew;
@@ -532,8 +530,6 @@ const api = {
} }
}; };
router.use(dbCheck);
router.post('/api', checkClientVersion, asyncHandler(api.newBrew)); router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ const brewSnippetsToJSON = (menuTitle, userBrewSnippets, themeBundleSnippets=nul
const mpAsSnippets = []; const mpAsSnippets = [];
// Snippets from Themes first. // Snippets from Themes first.
if(themeBundleSnippets) { if(themeBundleSnippets) {
for (const themes of themeBundleSnippets) { for (let themes of themeBundleSnippets) {
if(typeof themes !== 'string') { if(typeof themes !== 'string') {
const userSnippets = []; const userSnippets = [];
const snipSplit = themes.snippets.trim().split(textSplit).slice(1); const snipSplit = themes.snippets.trim().split(textSplit).slice(1);
@@ -77,8 +77,8 @@ const yamlSnippetsToText = (yamlObj)=>{
let snippetsText = ''; let snippetsText = '';
for (const snippet of yamlObj) { for (let snippet of yamlObj) {
for (const subSnippet of snippet.subsnippets) { for (let subSnippet of snippet.subsnippets) {
snippetsText = `${snippetsText}\\snippet ${subSnippet.name}\n${subSnippet.gen || ''}\n`; snippetsText = `${snippetsText}\\snippet ${subSnippet.name}\n${subSnippet.gen || ''}\n`;
} }
} }
@@ -116,21 +116,27 @@ const printCurrentBrew = ()=>{
} }
}; };
const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{ const fetchThemeBundle = async (obj, renderer, theme)=>{
if(!renderer || !theme) return; if(!renderer || !theme) return;
const res = await request const res = await request
.get(`/api/theme/${renderer}/${theme}`) .get(`/api/theme/${renderer}/${theme}`)
.catch((err)=>{ .catch((err)=>{
setError(err); obj.setState({ error: err });
}); });
if(!res) { if(!res) {
setThemeBundle({}); obj.setState((prevState)=>({
...prevState,
themeBundle : {}
}));
return; return;
} }
const themeBundle = res.body; const themeBundle = res.body;
themeBundle.joinedStyles = themeBundle.styles.map((style)=>`<style>${style}</style>`).join('\n\n'); themeBundle.joinedStyles = themeBundle.styles.map((style)=>`<style>${style}</style>`).join('\n\n');
setThemeBundle(themeBundle); obj.setState((prevState)=>({
setError(null); ...prevState,
themeBundle : themeBundle,
error : null
}));
}; };
const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => { const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
@@ -166,7 +172,7 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label)=>{
break; break;
} }
} }
}; }
export { export {
splitTextStyleAndMetadata, splitTextStyleAndMetadata,

View File

@@ -1,11 +1,11 @@
import './renderWarnings.less'; require('./renderWarnings.less');
import React from 'react'; const React = require('react');
import createReactClass from 'create-react-class'; const createClass = require('create-react-class');
import _ from 'lodash'; const _ = require('lodash');
import Dialog from '../dialog.jsx'; import Dialog from '../../../client/components/dialog.jsx';
const RenderWarnings = createReactClass({ const RenderWarnings = createClass({
displayName : 'RenderWarnings', displayName : 'RenderWarnings',
getInitialState : function() { getInitialState : function() {
return { return {
@@ -25,7 +25,7 @@ const RenderWarnings = createReactClass({
if(!isChrome){ if(!isChrome){
return <li key='chrome'> return <li key='chrome'>
<em>Built for Chrome </em> <br /> <em>Built for Chrome </em> <br />
Other browsers have not been tested for compatibility. If you Other browsers have not been tested for compatiblilty. If you
experience issues with your document not rendering or printing experience issues with your document not rendering or printing
properly, please try using the latest version of Chrome before properly, please try using the latest version of Chrome before
submitting a bug report. submitting a bug report.
@@ -57,4 +57,4 @@ const RenderWarnings = createReactClass({
} }
}); });
export default RenderWarnings; module.exports = RenderWarnings;

View File

@@ -1,7 +1,7 @@
import diceFont from 'themes/fonts/iconFonts/diceFont.js'; import diceFont from '../../../themes/fonts/iconFonts/diceFont.js';
import elderberryInn from 'themes/fonts/iconFonts/elderberryInn.js'; import elderberryInn from '../../../themes/fonts/iconFonts/elderberryInn.js';
import fontAwesome from 'themes/fonts/iconFonts/fontAwesome.js'; import fontAwesome from '../../../themes/fonts/iconFonts/fontAwesome.js';
import gameIcons from 'themes/fonts/iconFonts/gameIcons.js'; import gameIcons from '../../../themes/fonts/iconFonts/gameIcons.js';
const emojis = { const emojis = {
...diceFont, ...diceFont,
@@ -79,6 +79,6 @@ const showAutocompleteEmoji = function(CodeMirror, editor) {
}); });
}; };
export default { module.exports = {
showAutocompleteEmoji showAutocompleteEmoji
}; };

View File

@@ -38,11 +38,11 @@ const autoCloseCurlyBraces = function(CodeMirror, cm, typingClosingBrace) {
} }
}; };
export default { module.exports = {
autoCloseCurlyBraces : function(CodeMirror, codeMirror) { autoCloseCurlyBraces : function(CodeMirror, codeMirror) {
const map = { name: 'autoCloseCurlyBraces' }; const map = { name: 'autoCloseCurlyBraces' };
map[`'{'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm); }; map[`'{'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm); };
map[`'}'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm, true); }; map[`'}'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm, true); };
codeMirror?.addKeyMap(map); codeMirror.addKeyMap(map);
} }
}; };

View File

@@ -1,13 +1,51 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import './codeEditor.less'; require('./codeEditor.less');
import React from 'react'; const React = require('react');
import createReactClass from 'create-react-class'; const createClass = require('create-react-class');
import _ from 'lodash'; const _ = require('lodash');
import closeTag from './close-tag'; const closeTag = require('./close-tag');
import autoCompleteEmoji from './autocompleteEmoji'; const autoCompleteEmoji = require('./autocompleteEmoji');
let CodeMirror;
const CodeEditor = createReactClass({ let CodeMirror;
if(typeof window !== 'undefined'){
CodeMirror = require('codemirror');
//Language Modes
require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown
require('codemirror/mode/css/css.js');
require('codemirror/mode/javascript/javascript.js');
//Addons
//Code folding
require('codemirror/addon/fold/foldcode.js');
require('codemirror/addon/fold/foldgutter.js');
//Search and replace
require('codemirror/addon/search/search.js');
require('codemirror/addon/search/searchcursor.js');
require('codemirror/addon/search/jump-to-line.js');
require('codemirror/addon/search/match-highlighter.js');
require('codemirror/addon/search/matchesonscrollbar.js');
require('codemirror/addon/dialog/dialog.js');
//Trailing space highlighting
// require('codemirror/addon/edit/trailingspace.js');
//Active line highlighting
// require('codemirror/addon/selection/active-line.js');
//Scroll past last line
require('codemirror/addon/scroll/scrollpastend.js');
//Auto-closing
//XML code folding is a requirement of the auto-closing tag feature and is not enabled
require('codemirror/addon/fold/xml-fold.js');
require('codemirror/addon/edit/closetag.js');
//Autocompletion
require('codemirror/addon/hint/show-hint.js');
const foldPagesCode = require('./fold-pages');
foldPagesCode.registerHomebreweryHelper(CodeMirror);
const foldCSSCode = require('./fold-css');
foldCSSCode.registerHomebreweryHelper(CodeMirror);
}
const CodeEditor = createClass({
displayName : 'CodeEditor', displayName : 'CodeEditor',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -28,54 +66,23 @@ const CodeEditor = createReactClass({
editor : React.createRef(null), editor : React.createRef(null),
async componentDidMount() { componentDidMount : function() {
CodeMirror = (await import('codemirror')).default;
this.CodeMirror = CodeMirror;
await import('codemirror/mode/gfm/gfm.js');
await import('codemirror/mode/css/css.js');
await import('codemirror/mode/javascript/javascript.js');
// addons
await import('codemirror/addon/fold/foldcode.js');
await import('codemirror/addon/fold/foldgutter.js');
await import('codemirror/addon/fold/xml-fold.js');
await import('codemirror/addon/search/search.js');
await import('codemirror/addon/search/searchcursor.js');
await import('codemirror/addon/search/jump-to-line.js');
await import('codemirror/addon/search/match-highlighter.js');
await import('codemirror/addon/search/matchesonscrollbar.js');
await import('codemirror/addon/dialog/dialog.js');
await import('codemirror/addon/scroll/scrollpastend.js');
await import('codemirror/addon/edit/closetag.js');
await import('codemirror/addon/hint/show-hint.js');
// import 'codemirror/addon/selection/active-line.js';
// import 'codemirror/addon/edit/trailingspace.js';
// register helpers dynamically as well
const foldPagesCode = (await import('./fold-pages')).default;
const foldCSSCode = (await import('./fold-css')).default;
foldPagesCode.registerHomebreweryHelper(CodeMirror);
foldCSSCode.registerHomebreweryHelper(CodeMirror);
this.buildEditor(); this.buildEditor();
const newDoc = CodeMirror?.Doc(this.props.value, this.props.language); const newDoc = CodeMirror.Doc(this.props.value, this.props.language);
this.codeMirror?.swapDoc(newDoc); this.codeMirror.swapDoc(newDoc);
}, },
componentDidUpdate : function(prevProps) { componentDidUpdate : function(prevProps) {
if(prevProps.view !== this.props.view){ //view changed; swap documents if(prevProps.view !== this.props.view){ //view changed; swap documents
let newDoc; let newDoc;
if(!this.state.docs[this.props.view]) { if(!this.state.docs[this.props.view]) {
newDoc = CodeMirror?.Doc(this.props.value, this.props.language); newDoc = CodeMirror.Doc(this.props.value, this.props.language);
} else { } else {
newDoc = this.state.docs[this.props.view]; newDoc = this.state.docs[this.props.view];
} }
const oldDoc = { [prevProps.view]: this.codeMirror?.swapDoc(newDoc) }; const oldDoc = { [prevProps.view]: this.codeMirror.swapDoc(newDoc) };
this.setState((prevState)=>({ this.setState((prevState)=>({
docs : _.merge({}, prevState.docs, oldDoc) docs : _.merge({}, prevState.docs, oldDoc)
@@ -83,17 +90,17 @@ const CodeEditor = createReactClass({
this.props.rerenderParent(); this.props.rerenderParent();
} else if(this.codeMirror?.getValue() != this.props.value) { //update editor contents if brew.text is changed from outside } else if(this.codeMirror?.getValue() != this.props.value) { //update editor contents if brew.text is changed from outside
this.codeMirror?.setValue(this.props.value); this.codeMirror.setValue(this.props.value);
} }
if(this.props.enableFolding) { if(this.props.enableFolding) {
this.codeMirror?.setOption('foldOptions', this.foldOptions(this.codeMirror)); this.codeMirror.setOption('foldOptions', this.foldOptions(this.codeMirror));
} else { } else {
this.codeMirror?.setOption('foldOptions', false); this.codeMirror.setOption('foldOptions', false);
} }
if(prevProps.editorTheme !== this.props.editorTheme){ if(prevProps.editorTheme !== this.props.editorTheme){
this.codeMirror?.setOption('theme', this.props.editorTheme); this.codeMirror.setOption('theme', this.props.editorTheme);
} }
}, },
@@ -181,8 +188,8 @@ const CodeEditor = createReactClass({
closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror); closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror);
autoCompleteEmoji.showAutocompleteEmoji(CodeMirror, this.codeMirror); autoCompleteEmoji.showAutocompleteEmoji(CodeMirror, this.codeMirror);
// Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror?. Either one works. // Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works.
this.codeMirror?.on('change', (cm)=>{this.props.onChange(cm.getValue());}); this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue());});
this.updateSize(); this.updateSize();
}, },
@@ -196,84 +203,84 @@ const CodeEditor = createReactClass({
}, },
dedent : function () { dedent : function () {
this.codeMirror?.execCommand('indentLess'); this.codeMirror.execCommand('indentLess');
}, },
makeHeader : function (number) { makeHeader : function (number) {
const selection = this.codeMirror?.getSelection(); const selection = this.codeMirror.getSelection();
const header = Array(number).fill('#').join(''); const header = Array(number).fill('#').join('');
this.codeMirror?.replaceSelection(`${header} ${selection}`, 'around'); this.codeMirror.replaceSelection(`${header} ${selection}`, 'around');
const cursor = this.codeMirror?.getCursor(); const cursor = this.codeMirror.getCursor();
this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch + selection.length + number + 1 }); this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch + selection.length + number + 1 });
}, },
makeBold : function() { makeBold : function() {
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 2) === '**' && selection.slice(-2) === '**'; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '**' && selection.slice(-2) === '**';
this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `**${selection}**`, 'around'); this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `**${selection}**`, 'around');
if(selection.length === 0){ if(selection.length === 0){
const cursor = this.codeMirror?.getCursor(); const cursor = this.codeMirror.getCursor();
this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 2 }); this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
} }
}, },
makeItalic : function() { makeItalic : function() {
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 1) === '*' && selection.slice(-1) === '*'; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '*' && selection.slice(-1) === '*';
this.codeMirror?.replaceSelection(t ? selection.slice(1, -1) : `*${selection}*`, 'around'); this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `*${selection}*`, 'around');
if(selection.length === 0){ if(selection.length === 0){
const cursor = this.codeMirror?.getCursor(); const cursor = this.codeMirror.getCursor();
this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 1 }); this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
} }
}, },
makeSuper : function() { makeSuper : function() {
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 1) === '^' && selection.slice(-1) === '^'; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '^' && selection.slice(-1) === '^';
this.codeMirror?.replaceSelection(t ? selection.slice(1, -1) : `^${selection}^`, 'around'); this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `^${selection}^`, 'around');
if(selection.length === 0){ if(selection.length === 0){
const cursor = this.codeMirror?.getCursor(); const cursor = this.codeMirror.getCursor();
this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 1 }); this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
} }
}, },
makeSub : function() { makeSub : function() {
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 2) === '^^' && selection.slice(-2) === '^^'; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '^^' && selection.slice(-2) === '^^';
this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `^^${selection}^^`, 'around'); this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `^^${selection}^^`, 'around');
if(selection.length === 0){ if(selection.length === 0){
const cursor = this.codeMirror?.getCursor(); const cursor = this.codeMirror.getCursor();
this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 2 }); this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
} }
}, },
makeNbsp : function() { makeNbsp : function() {
this.codeMirror?.replaceSelection('&nbsp;', 'end'); this.codeMirror.replaceSelection('&nbsp;', 'end');
}, },
makeSpace : function() { makeSpace : function() {
const selection = this.codeMirror?.getSelection(); const selection = this.codeMirror.getSelection();
const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}'; const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}';
if(t){ if(t){
const percent = parseInt(selection.slice(8, -4)) + 10; const percent = parseInt(selection.slice(8, -4)) + 10;
this.codeMirror?.replaceSelection(percent < 90 ? `{{width:${percent}% }}` : '{{width:100% }}', 'around'); this.codeMirror.replaceSelection(percent < 90 ? `{{width:${percent}% }}` : '{{width:100% }}', 'around');
} else { } else {
this.codeMirror?.replaceSelection(`{{width:10% }}`, 'around'); this.codeMirror.replaceSelection(`{{width:10% }}`, 'around');
} }
}, },
removeSpace : function() { removeSpace : function() {
const selection = this.codeMirror?.getSelection(); const selection = this.codeMirror.getSelection();
const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}'; const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}';
if(t){ if(t){
const percent = parseInt(selection.slice(8, -4)) - 10; const percent = parseInt(selection.slice(8, -4)) - 10;
this.codeMirror?.replaceSelection(percent > 10 ? `{{width:${percent}% }}` : '', 'around'); this.codeMirror.replaceSelection(percent > 10 ? `{{width:${percent}% }}` : '', 'around');
} }
}, },
newColumn : function() { newColumn : function() {
this.codeMirror?.replaceSelection('\n\\column\n\n', 'end'); this.codeMirror.replaceSelection('\n\\column\n\n', 'end');
}, },
newPage : function() { newPage : function() {
this.codeMirror?.replaceSelection('\n\\page\n\n', 'end'); this.codeMirror.replaceSelection('\n\\page\n\n', 'end');
}, },
injectText : function(injectText, overwrite=true) { injectText : function(injectText, overwrite=true) {
@@ -286,29 +293,29 @@ const CodeEditor = createReactClass({
}, },
makeUnderline : function() { makeUnderline : function() {
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 3) === '<u>' && selection.slice(-4) === '</u>'; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 3) === '<u>' && selection.slice(-4) === '</u>';
this.codeMirror?.replaceSelection(t ? selection.slice(3, -4) : `<u>${selection}</u>`, 'around'); this.codeMirror.replaceSelection(t ? selection.slice(3, -4) : `<u>${selection}</u>`, 'around');
if(selection.length === 0){ if(selection.length === 0){
const cursor = this.codeMirror?.getCursor(); const cursor = this.codeMirror.getCursor();
this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 4 }); this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 4 });
} }
}, },
makeSpan : function() { makeSpan : function() {
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}'; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}';
this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `{{ ${selection}}}`, 'around'); this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `{{ ${selection}}}`, 'around');
if(selection.length === 0){ if(selection.length === 0){
const cursor = this.codeMirror?.getCursor(); const cursor = this.codeMirror.getCursor();
this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 2 }); this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
} }
}, },
makeDiv : function() { makeDiv : function() {
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}'; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}';
this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `{{\n${selection}\n}}`, 'around'); this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `{{\n${selection}\n}}`, 'around');
if(selection.length === 0){ if(selection.length === 0){
const cursor = this.codeMirror?.getCursor(); const cursor = this.codeMirror.getCursor();
this.codeMirror?.setCursor({ line: cursor.line - 1, ch: cursor.ch }); // set to -2? if wanting to enter classes etc. if so, get rid of first \n when replacing selection this.codeMirror.setCursor({ line: cursor.line - 1, ch: cursor.ch }); // set to -2? if wanting to enter classes etc. if so, get rid of first \n when replacing selection
} }
}, },
@@ -316,7 +323,7 @@ const CodeEditor = createReactClass({
let regex; let regex;
let cursorPos; let cursorPos;
let newComment; let newComment;
const selection = this.codeMirror?.getSelection(); const selection = this.codeMirror.getSelection();
if(this.props.language === 'gfm'){ if(this.props.language === 'gfm'){
regex = /^\s*(<!--\s?)(.*?)(\s?-->)\s*$/gs; regex = /^\s*(<!--\s?)(.*?)(\s?-->)\s*$/gs;
cursorPos = 4; cursorPos = 4;
@@ -326,44 +333,44 @@ const CodeEditor = createReactClass({
cursorPos = 3; cursorPos = 3;
newComment = `/* ${selection} */`; newComment = `/* ${selection} */`;
} }
this.codeMirror?.replaceSelection(regex.test(selection) == true ? selection.replace(regex, '$2') : newComment, 'around'); this.codeMirror.replaceSelection(regex.test(selection) == true ? selection.replace(regex, '$2') : newComment, 'around');
if(selection.length === 0){ if(selection.length === 0){
const cursor = this.codeMirror?.getCursor(); const cursor = this.codeMirror.getCursor();
this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - cursorPos }); this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - cursorPos });
}; };
}, },
makeLink : function() { makeLink : function() {
const isLink = /^\[(.*)\]\((.*)\)$/; const isLink = /^\[(.*)\]\((.*)\)$/;
const selection = this.codeMirror?.getSelection().trim(); const selection = this.codeMirror.getSelection().trim();
let match; let match;
if(match = isLink.exec(selection)){ if(match = isLink.exec(selection)){
const altText = match[1]; const altText = match[1];
const url = match[2]; const url = match[2];
this.codeMirror?.replaceSelection(`${altText} ${url}`); this.codeMirror.replaceSelection(`${altText} ${url}`);
const cursor = this.codeMirror?.getCursor(); const cursor = this.codeMirror.getCursor();
this.codeMirror?.setSelection({ line: cursor.line, ch: cursor.ch - url.length }, { line: cursor.line, ch: cursor.ch }); this.codeMirror.setSelection({ line: cursor.line, ch: cursor.ch - url.length }, { line: cursor.line, ch: cursor.ch });
} else { } else {
this.codeMirror?.replaceSelection(`[${selection || 'alt text'}](url)`); this.codeMirror.replaceSelection(`[${selection || 'alt text'}](url)`);
const cursor = this.codeMirror?.getCursor(); const cursor = this.codeMirror.getCursor();
this.codeMirror?.setSelection({ line: cursor.line, ch: cursor.ch - 4 }, { line: cursor.line, ch: cursor.ch - 1 }); this.codeMirror.setSelection({ line: cursor.line, ch: cursor.ch - 4 }, { line: cursor.line, ch: cursor.ch - 1 });
} }
}, },
makeList : function(listType) { makeList : function(listType) {
const selectionStart = this.codeMirror?.getCursor('from'), selectionEnd = this.codeMirror?.getCursor('to'); const selectionStart = this.codeMirror.getCursor('from'), selectionEnd = this.codeMirror.getCursor('to');
this.codeMirror?.setSelection( this.codeMirror.setSelection(
{ line: selectionStart.line, ch: 0 }, { line: selectionStart.line, ch: 0 },
{ line: selectionEnd.line, ch: this.codeMirror?.getLine(selectionEnd.line).length } { line: selectionEnd.line, ch: this.codeMirror.getLine(selectionEnd.line).length }
); );
const newSelection = this.codeMirror?.getSelection(); const newSelection = this.codeMirror.getSelection();
const regex = /^\d+\.\s|^-\s/gm; const regex = /^\d+\.\s|^-\s/gm;
if(newSelection.match(regex) != null){ // if selection IS A LIST if(newSelection.match(regex) != null){ // if selection IS A LIST
this.codeMirror?.replaceSelection(newSelection.replace(regex, ''), 'around'); this.codeMirror.replaceSelection(newSelection.replace(regex, ''), 'around');
} else { // if selection IS NOT A LIST } else { // if selection IS NOT A LIST
listType == 'UL' ? this.codeMirror?.replaceSelection(newSelection.replace(/^/gm, `- `), 'around') : listType == 'UL' ? this.codeMirror.replaceSelection(newSelection.replace(/^/gm, `- `), 'around') :
this.codeMirror?.replaceSelection(newSelection.replace(/^/gm, (()=>{ this.codeMirror.replaceSelection(newSelection.replace(/^/gm, (()=>{
let n = 1; let n = 1;
return ()=>{ return ()=>{
return `${n++}. `; return `${n++}. `;
@@ -373,39 +380,39 @@ const CodeEditor = createReactClass({
}, },
foldAllCode : function() { foldAllCode : function() {
this.codeMirror?.execCommand('foldAll'); this.codeMirror.execCommand('foldAll');
}, },
unfoldAllCode : function() { unfoldAllCode : function() {
this.codeMirror?.execCommand('unfoldAll'); this.codeMirror.execCommand('unfoldAll');
}, },
//=-- Externally used -==// //=-- Externally used -==//
setCursorPosition : function(line, char){ setCursorPosition : function(line, char){
setTimeout(()=>{ setTimeout(()=>{
this.codeMirror?.focus(); this.codeMirror.focus();
this.codeMirror?.doc.setCursor(line, char); this.codeMirror.doc.setCursor(line, char);
}, 10); }, 10);
}, },
getCursorPosition : function(){ getCursorPosition : function(){
return this.codeMirror?.getCursor(); return this.codeMirror.getCursor();
}, },
getTopVisibleLine : function(){ getTopVisibleLine : function(){
const rect = this.codeMirror?.getWrapperElement().getBoundingClientRect(); const rect = this.codeMirror.getWrapperElement().getBoundingClientRect();
const topVisibleLine = this.codeMirror?.lineAtHeight(rect.top, 'window'); const topVisibleLine = this.codeMirror.lineAtHeight(rect.top, 'window');
return topVisibleLine; return topVisibleLine;
}, },
updateSize : function(){ updateSize : function(){
this.codeMirror?.refresh(); this.codeMirror.refresh();
}, },
redo : function(){ redo : function(){
return this.codeMirror?.redo(); return this.codeMirror.redo();
}, },
undo : function(){ undo : function(){
return this.codeMirror?.undo(); return this.codeMirror.undo();
}, },
historySize : function(){ historySize : function(){
return this.codeMirror?.doc.historySize(); return this.codeMirror.doc.historySize();
}, },
foldOptions : function(cm){ foldOptions : function(cm){
@@ -419,7 +426,7 @@ const CodeEditor = createReactClass({
let foldPreviewText = ''; let foldPreviewText = '';
while (currentLine <= to.line && text.length <= maxLength) { while (currentLine <= to.line && text.length <= maxLength) {
const currentText = this.codeMirror?.getLine(currentLine); const currentText = this.codeMirror.getLine(currentLine);
currentLine++; currentLine++;
if(currentText[0] == '#'){ if(currentText[0] == '#'){
foldPreviewText = currentText; foldPreviewText = currentText;
@@ -454,5 +461,5 @@ const CodeEditor = createReactClass({
} }
}); });
export default CodeEditor; module.exports = CodeEditor;

View File

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

View File

@@ -1,4 +1,4 @@
export default { module.exports = {
registerHomebreweryHelper : function(CodeMirror) { registerHomebreweryHelper : function(CodeMirror) {
CodeMirror.registerHelper('fold', 'homebrewerycss', function(cm, start) { CodeMirror.registerHelper('fold', 'homebrewerycss', function(cm, start) {

View File

@@ -1,4 +1,4 @@
export default { module.exports = {
registerHomebreweryHelper : function(CodeMirror) { registerHomebreweryHelper : function(CodeMirror) {
CodeMirror.registerHelper('fold', 'homebrewery', function(cm, start) { CodeMirror.registerHelper('fold', 'homebrewery', function(cm, start) {
const matcher = /^\\page.*/; const matcher = /^\\page.*/;

View File

@@ -1,29 +1,117 @@
/* eslint-disable max-depth */ /* eslint-disable max-depth */
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import _ from 'lodash'; import _ from 'lodash';
import { Parser as MathParser } from 'expr-eval';
import { marked as Marked } from 'marked'; import { marked as Marked } from 'marked';
import MarkedExtendedTables from 'marked-extended-tables'; import MarkedExtendedTables from 'marked-extended-tables';
import MarkedDefinitionLists from 'marked-definition-lists'; import MarkedDefinitionLists from 'marked-definition-lists';
import MarkedAlignedParagraphs from 'marked-alignment-paragraphs'; import MarkedAlignedParagraphs from 'marked-alignment-paragraphs';
import MarkedNonbreakingSpaces from 'marked-nonbreaking-spaces'; import MarkedNonbreakingSpaces from 'marked-nonbreaking-spaces';
import MarkedSubSuperText from 'marked-subsuper-text'; import MarkedSubSuperText from 'marked-subsuper-text';
import { markedVariables,
setMarkedVariablePage,
setMarkedVariable,
getMarkedVariable } from 'marked-variables';
import { markedSmartypantsLite as MarkedSmartypantsLite } from 'marked-smartypants-lite'; import { markedSmartypantsLite as MarkedSmartypantsLite } from 'marked-smartypants-lite';
import { gfmHeadingId as MarkedGFMHeadingId, resetHeadings as MarkedGFMResetHeadingIDs } from 'marked-gfm-heading-id'; import { gfmHeadingId as MarkedGFMHeadingId, resetHeadings as MarkedGFMResetHeadingIDs } from 'marked-gfm-heading-id';
import { markedEmoji as MarkedEmojis } from 'marked-emoji'; import { markedEmoji as MarkedEmojis } from 'marked-emoji';
import { romanize } from 'romans';
import writtenNumber from 'written-number';
//Icon fonts included so they can appear in emoji autosuggest dropdown //Icon fonts included so they can appear in emoji autosuggest dropdown
import diceFont from '../themes/fonts/iconFonts/diceFont.js'; import diceFont from '../../themes/fonts/iconFonts/diceFont.js';
import elderberryInn from '../themes/fonts/iconFonts/elderberryInn.js'; import elderberryInn from '../../themes/fonts/iconFonts/elderberryInn.js';
import gameIcons from '../themes/fonts/iconFonts/gameIcons.js'; import gameIcons from '../../themes/fonts/iconFonts/gameIcons.js';
import fontAwesome from '../themes/fonts/iconFonts/fontAwesome.js'; import fontAwesome from '../../themes/fonts/iconFonts/fontAwesome.js';
const renderer = new Marked.Renderer(); const renderer = new Marked.Renderer();
const tokenizer = new Marked.Tokenizer(); const tokenizer = new Marked.Tokenizer();
//Limit math features to simple items
const mathParser = new MathParser({
operators : {
// These default to true, but are included to be explicit
add : true,
subtract : true,
multiply : true,
divide : true,
power : true,
round : true,
floor : true,
ceil : true,
abs : true,
sin : false, cos : false, tan : false, asin : false, acos : false,
atan : false, sinh : false, cosh : false, tanh : false, asinh : false,
acosh : false, atanh : false, sqrt : false, cbrt : false, log : false,
log2 : false, ln : false, lg : false, log10 : false, expm1 : false,
log1p : false, trunc : false, join : false, sum : false, indexOf : false,
'-' : false, '+' : false, exp : false, not : false, length : false,
'!' : false, sign : false, random : false, fac : false, min : false,
max : false, hypot : false, pyt : false, pow : false, atan2 : false,
'if' : false, gamma : false, roundTo : false, map : false, fold : false,
filter : false,
remainder : false, factorial : false,
comparison : false, concatenate : false,
logical : false, assignment : false,
array : false, fndef : false
}
});
// Add sign function
mathParser.functions.sign = function (a) {
if(a >= 0) return '+';
return '-';
};
// Add signed function
mathParser.functions.signed = function (a) {
if(a >= 0) return `+${a}`;
return `${a}`;
};
// Add Roman numeral functions
mathParser.functions.toRomans = function (a) {
return romanize(a);
};
mathParser.functions.toRomansUpper = function (a) {
return romanize(a).toUpperCase();
};
mathParser.functions.toRomansLower = function (a) {
return romanize(a).toLowerCase();
};
// Add character functions
mathParser.functions.toChar = function (a) {
if(a <= 0) return a;
const genChars = function (i) {
return (i > 26 ? genChars(Math.floor((i - 1) / 26)) : '') + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[(i - 1) % 26];
};
return genChars(a);
};
mathParser.functions.toCharUpper = function (a) {
return mathParser.functions.toChar(a).toUpperCase();
};
mathParser.functions.toCharLower = function (a) {
return mathParser.functions.toChar(a).toLowerCase();
};
// Add word functions
mathParser.functions.toWords = function (a) {
return writtenNumber(a);
};
mathParser.functions.toWordsUpper = function (a) {
return mathParser.functions.toWords(a).toUpperCase();
};
mathParser.functions.toWordsLower = function (a) {
return mathParser.functions.toWords(a).toLowerCase();
};
mathParser.functions.toWordsCaps = function (a) {
const words = mathParser.functions.toWords(a).split(' ');
return words.map((word)=>{
return word.replace(/(?:^|\b|\s)(\w)/g, function(w, index) {
return index === 0 ? w.toLowerCase() : w.toUpperCase();
});
}).join(' ');
};
// Normalize variable names; trim edge spaces and shorten blocks of whitespace to 1 space
const normalizeVarNames = (label)=>{
return label.trim().replace(/\s+/g, ' ');
};
//Processes the markdown within an HTML block if it's just a class-wrapper //Processes the markdown within an HTML block if it's just a class-wrapper
renderer.html = function (token) { renderer.html = function (token) {
let html = token.text; let html = token.text;
@@ -31,12 +119,7 @@ renderer.html = function (token) {
const openTag = html.substring(0, html.indexOf('>')+1); const openTag = html.substring(0, html.indexOf('>')+1);
html = html.substring(html.indexOf('>')+1); html = html.substring(html.indexOf('>')+1);
html = html.substring(0, html.lastIndexOf('</div>')); html = html.substring(0, html.lastIndexOf('</div>'));
return `${openTag} ${Marked.parse(html)} </div>`;
// Repeat the markdown processing for content inside the div, minus the preprocessing and postprocessing hooks which should only run once globally
const opts = Marked.defaults;
const tokens = Marked.lexer(html, opts);
Marked.walkTokens(tokens, opts.walkTokens);
return `${openTag} ${Marked.parser(tokens, opts)} </div>`;
} }
return html; return html;
}; };
@@ -102,7 +185,7 @@ const mustacheSpans = {
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-+*/()#%=?.&:!@$^;:\[\]_= ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g; const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g;
const match = completeSpan.exec(src); const match = completeSpan.exec(src);
if(match) { if(match) {
//Find closing delimiter //Find closing delimiter
@@ -159,7 +242,7 @@ const mustacheDivs = {
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-+*/()#%=?.&:!@$^;:\[\]_= ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm; const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm;
const match = completeBlock.exec(src); const match = completeBlock.exec(src);
if(match) { if(match) {
//Find closing delimiter //Find closing delimiter
@@ -214,7 +297,7 @@ const mustacheInjectInline = {
level : 'inline', level : 'inline',
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?.&:!@$^;:\[\]_= ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g; const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/g;
const match = inlineRegex.exec(src); const match = inlineRegex.exec(src);
if(match) { if(match) {
const lastToken = tokens[tokens.length - 1]; const lastToken = tokens[tokens.length - 1];
@@ -260,7 +343,7 @@ const mustacheInjectBlock = {
level : 'block', level : 'block',
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?.&:!@$^;:\[\]_= ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/ym; const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/ym;
const match = inlineRegex.exec(src); const match = inlineRegex.exec(src);
if(match) { if(match) {
const lastToken = tokens[tokens.length - 1]; const lastToken = tokens[tokens.length - 1];
@@ -328,6 +411,248 @@ const forcedParagraphBreaks = {
} }
}; };
//v=====--------------------< Variable Handling >-------------------=====v// 242 lines
const replaceVar = function(input, hoist=false, allowUnresolved=false) {
const regex = /([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g;
const match = regex.exec(input);
const prefix = match[1];
const label = normalizeVarNames(match[2]); // Ensure the label name is normalized as it should be in the var stack.
//v=====--------------------< HANDLE MATH >-------------------=====v//
const mathRegex = /[a-z]+\(|[+\-*/^(),]/g;
const matches = label.split(mathRegex);
const mathVars = matches.filter((match)=>isNaN(match))?.map((s)=>s.trim()); // Capture any variable names
let replacedLabel = label;
if(prefix[0] == '$' && mathVars?.[0] !== label.trim()) {// If there was mathy stuff not captured, let's do math!
mathVars?.forEach((variable)=>{
const foundVar = lookupVar(variable, globalPageNumber, hoist);
if(foundVar && foundVar.resolved && foundVar.content && !isNaN(foundVar.content)) // Only subsitute math values if fully resolved, not empty strings, and numbers
replacedLabel = replacedLabel.replaceAll(new RegExp(`(?<!\\w)(${variable})(?!\\w)`, 'g'), foundVar.content);
});
try {
return mathParser.evaluate(replacedLabel);
} catch (error) {
return undefined; // Return undefined if invalid math result
}
}
//^=====--------------------< HANDLE MATH >-------------------=====^//
const foundVar = lookupVar(label, globalPageNumber, hoist);
if(!foundVar || (!foundVar.resolved && !allowUnresolved))
return undefined; // Return undefined if not found, or parially-resolved vars are not allowed
// url or <url> "title" or 'title' or (title)
const linkRegex = /^([^<\s][^\s]*|<.*?>)(?: ("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\((?:\\\(|\\\)|[^()])*\)))?$/m;
const linkMatch = linkRegex.exec(foundVar.content);
const href = linkMatch ? linkMatch[1] : null; //TODO: TRIM OFF < > IF PRESENT
const title = linkMatch ? linkMatch[2]?.slice(1, -1) : null;
if(!prefix[0] && href) // Link
return `[${label}](${href}${title ? ` "${title}"` : ''})`;
if(prefix[0] == '!' && href) // Image
return `![${label}](${href} ${title ? ` "${title}"` : ''})`;
if(prefix[0] == '$') // Variable
return foundVar.content;
};
const lookupVar = function(label, index, hoist=false) {
while (index >= 0) {
if(globalVarsList[index]?.[label] !== undefined)
return globalVarsList[index][label];
index--;
}
if(hoist) { //If normal lookup failed, attempt hoisting
index = Object.keys(globalVarsList).length; // Move index to start from last page
while (index >= 0) {
if(globalVarsList[index]?.[label] !== undefined)
return globalVarsList[index][label];
index--;
}
}
return undefined;
};
const processVariableQueue = function() {
let resolvedOne = true;
let finalLoop = false;
while (resolvedOne || finalLoop) { // Loop through queue until no more variable calls can be resolved
resolvedOne = false;
for (const item of varsQueue) {
if(item.type == 'text')
continue;
if(item.type == 'varDefBlock') {
const regex = /[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g;
let match;
let resolved = true;
let tempContent = item.content;
while (match = regex.exec(item.content)) { // regex to find variable calls
const value = replaceVar(match[0], true);
if(value == undefined)
resolved = false;
else
tempContent = tempContent.replaceAll(match[0], value);
}
if(resolved == true || item.content != tempContent) {
resolvedOne = true;
item.content = tempContent;
}
globalVarsList[globalPageNumber][item.varName] = {
content : item.content,
resolved : resolved
};
if(resolved)
item.type = 'resolved';
}
if(item.type == 'varCallBlock' || item.type == 'varCallInline') {
const value = replaceVar(item.content, true, finalLoop); // final loop will just use the best value so far
if(value == undefined)
continue;
resolvedOne = true;
item.content = value;
item.type = 'text';
}
}
varsQueue = varsQueue.filter((item)=>item.type !== 'resolved'); // Remove any fully-resolved variable definitions
if(finalLoop)
break;
if(!resolvedOne)
finalLoop = true;
}
varsQueue = varsQueue.filter((item)=>item.type !== 'varDefBlock');
};
function MarkedVariables() {
return {
hooks : {
preprocess(src) {
const codeBlockSkip = /^(?: {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+|^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})(?:[^\n]*)(?:\n|$)(?:|(?:[\s\S]*?)(?:\n|$))(?: {0,3}\2[~`]* *(?=\n|$))|`[^`]*?`/;
const blockDefRegex = /^[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]:(?!\() *((?:\n? *[^\s].*)+)(?=\n+|$)/; //Matches 3, [4]:5
const blockCallRegex = /^[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\](?=\n|$)/; //Matches 6, [7]
const inlineDefRegex = /([!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\])\(([^\n]+)\)/; //Matches 8, 9[10](11)
const inlineCallRegex = /[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\](?!\()/; //Matches 12, [13]
// Combine regexes and wrap in parens like so: (regex1)|(regex2)|(regex3)|(regex4)
const combinedRegex = new RegExp([codeBlockSkip, blockDefRegex, blockCallRegex, inlineDefRegex, inlineCallRegex].map((s)=>`(${s.source})`).join('|'), 'gm');
let lastIndex = 0;
let match;
while ((match = combinedRegex.exec(src)) !== null) {
// Format any matches into tokens and store
if(match.index > lastIndex) { // Any non-variable stuff
varsQueue.push(
{ type : 'text',
varName : null,
content : src.slice(lastIndex, match.index)
});
}
if(match[1]) {
varsQueue.push(
{ type : 'text',
varName : null,
content : match[0]
});
}
if(match[3]) { // Block Definition
const label = match[4] ? normalizeVarNames(match[4]) : null;
const content = match[5] ? match[5].trim().replace(/[ \t]+/g, ' ') : null; // Normalize text content (except newlines for block-level content)
varsQueue.push(
{ type : 'varDefBlock',
varName : label,
content : content
});
}
if(match[6]) { // Block Call
const label = match[7] ? normalizeVarNames(match[7]) : null;
varsQueue.push(
{ type : 'varCallBlock',
varName : label,
content : match[0]
});
}
if(match[8]) { // Inline Definition
const label = match[10] ? normalizeVarNames(match[10]) : null;
let content = match[11] || null;
// In case of nested (), find the correct matching end )
let level = 0;
let i;
for (i = 0; i < content.length; i++) {
if(content[i] === '\\') {
i++;
} else if(content[i] === '(') {
level++;
} else if(content[i] === ')') {
level--;
if(level < 0)
break;
}
}
combinedRegex.lastIndex = combinedRegex.lastIndex - (content.length - i);
content = content.slice(0, i).trim().replace(/\s+/g, ' ');
varsQueue.push(
{ type : 'varDefBlock',
varName : label,
content : content
});
varsQueue.push(
{ type : 'varCallInline',
varName : label,
content : match[9]
});
}
if(match[12]) { // Inline Call
const label = match[13] ? normalizeVarNames(match[13]) : null;
varsQueue.push(
{ type : 'varCallInline',
varName : label,
content : match[0]
});
}
lastIndex = combinedRegex.lastIndex;
}
if(lastIndex < src.length) {
varsQueue.push(
{ type : 'text',
varName : null,
content : src.slice(lastIndex)
});
}
processVariableQueue();
const output = varsQueue.map((item)=>item.content).join('');
varsQueue = []; // Must clear varsQueue because custom HTML renderer uses Marked.parse which will preprocess again without clearing the array
return output;
}
}
};
};
//^=====--------------------< Variable Handling >-------------------=====^//
// Emoji options // Emoji options
// To add more icon fonts, need to do these things // To add more icon fonts, need to do these things
// 1) Add the font file as .woff2 to themes/fonts/iconFonts folder // 1) Add the font file as .woff2 to themes/fonts/iconFonts folder
@@ -353,7 +678,7 @@ const tableTerminators = [
` *{{[^{\n]*\n.*?\n}}` // mustacheDiv ` *{{[^{\n]*\n.*?\n}}` // mustacheDiv
]; ];
Marked.use(markedVariables()); Marked.use(MarkedVariables());
Marked.use(MarkedDefinitionLists()); Marked.use(MarkedDefinitionLists());
Marked.use({ extensions : [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] }); Marked.use({ extensions : [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] });
Marked.use(mustacheInjectBlock); Marked.use(mustacheInjectBlock);
@@ -480,17 +805,25 @@ const mergeHTMLTags = (originalTags, newTags)=>{
}; };
}; };
const globalVarsList = {};
let varsQueue = [];
let globalPageNumber = 0;
const Markdown = { const Markdown = {
marked : Marked, marked : Marked,
render : (rawBrewText, pageNumber=0)=>{ render : (rawBrewText, pageNumber=0)=>{
setMarkedVariablePage(pageNumber); const lastPageNumber = pageNumber > 0 ? globalVarsList[pageNumber - 1].HB_pageNumber.content : 0;
globalVarsList[pageNumber] = { //Reset global links for current page, to ensure values are parsed in order
const lastPageNumber = pageNumber > 0 ? getMarkedVariable('HB_pageNumber', pageNumber - 1) : 0; 'HB_pageNumber' : { //Add document variables for this page
setMarkedVariable('HB_pageNumber', //Add document variables for this page content : !isNaN(Number(lastPageNumber)) ? Number(lastPageNumber) + 1 : lastPageNumber,
!isNaN(Number(lastPageNumber)) ? Number(lastPageNumber) + 1 : lastPageNumber, resolved : true
pageNumber); }
};
if(pageNumber==0) MarkedGFMResetHeadingIDs(); varsQueue = []; //Could move into MarkedVariables()
globalPageNumber = pageNumber;
if(pageNumber==0) {
MarkedGFMResetHeadingIDs();
}
rawBrewText = rawBrewText.replace(/^\\column(?:break)?$/gm, `\n<div class='columnSplit'></div>\n`); rawBrewText = rawBrewText.replace(/^\\column(?:break)?$/gm, `\n<div class='columnSplit'></div>\n`);

View File

@@ -1,5 +1,5 @@
import _ from 'lodash'; const _ = require('lodash');
import Markdown from 'markedLegacy'; const Markdown = require('markedLegacy');
const renderer = new Markdown.Renderer(); const renderer = new Markdown.Renderer();
//Processes the markdown within an HTML block if it's just a class-wrapper //Processes the markdown within an HTML block if it's just a class-wrapper
@@ -49,7 +49,7 @@ const cleanUrl = function (sanitize, base, href) {
prot = decodeURIComponent(unescape(href)) prot = decodeURIComponent(unescape(href))
.replace(nonWordAndColonTest, '') .replace(nonWordAndColonTest, '')
.toLowerCase(); .toLowerCase();
} catch { } catch (e) {
return null; return null;
} }
if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) { if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
@@ -58,7 +58,7 @@ const cleanUrl = function (sanitize, base, href) {
} }
try { try {
href = encodeURI(href).replace(/%25/g, '%'); href = encodeURI(href).replace(/%25/g, '%');
} catch { } catch (e) {
return null; return null;
} }
return href; return href;
@@ -103,7 +103,7 @@ const voidTags = new Set([
]); ]);
export default { module.exports = {
marked : Markdown, marked : Markdown,
render : (rawBrewText)=>{ render : (rawBrewText)=>{
return Markdown( return Markdown(

View File

@@ -1,13 +1,14 @@
import './navbar.less'; require('client/homebrew/navbar/navbar.less');
import React, { useState, useRef, useEffect } from 'react'; const React = require('react');
import createReactClass from 'create-react-class'; const { useState, useRef, useEffect } = React;
import _ from 'lodash'; const createClass = require('create-react-class');
import cx from 'classnames'; const _ = require('lodash');
const cx = require('classnames');
import NaturalCritIcon from '../../components/svg/naturalcrit-d20.svg.jsx'; const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
const Nav = { const Nav = {
base : createReactClass({ base : createClass({
displayName : 'Nav.base', displayName : 'Nav.base',
render : function(){ render : function(){
return <nav> return <nav>
@@ -24,7 +25,7 @@ const Nav = {
</a>; </a>;
}, },
section : createReactClass({ section : createClass({
displayName : 'Nav.section', displayName : 'Nav.section',
render : function(){ render : function(){
return <div className={`navSection ${this.props.className ?? ''}`}> return <div className={`navSection ${this.props.className ?? ''}`}>
@@ -33,7 +34,7 @@ const Nav = {
} }
}), }),
item : createReactClass({ item : createClass({
displayName : 'Nav.item', displayName : 'Nav.item',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -116,4 +117,4 @@ const Nav = {
}; };
export default Nav; module.exports = Nav;

View File

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

View File

@@ -0,0 +1,3 @@
module.exports = function(props){
return <svg version='1.1' x='0px' y='0px' viewBox='0 0 80 100' enableBackground='new 0 0 80 80'><g><g><polygon fill='#000000' points='12.9,71.4 7.6,66.1 19.3,54.4 20.7,55.8 10.4,66.1 12.9,68.6 23.2,58.3 24.6,59.7 '/></g><g><path fill='#000000' d='M29,61.6c-1.7,0-3.4-0.7-4.6-1.9l-5.1-5.1c-2.5-2.5-2.5-6.6,0-9.2l0.7-0.7L34.3,59l-0.7,0.7 C32.4,60.9,30.8,61.6,29,61.6z M20.1,47.6c-1.1,1.7-0.9,4.1,0.6,5.6l5.1,5.1c0.8,0.8,2,1.3,3.2,1.3c0.9,0,1.7-0.2,2.4-0.7 L20.1,47.6z'/></g><g><path fill='#000000' d='M12.3,74.8c-0.8,0-1.5-0.3-2-0.8l-5.2-5.2c-0.5-0.5-0.8-1.2-0.8-2c0-0.8,0.3-1.5,0.8-2 c1.1-1.1,2.9-1.1,4,0l5.2,5.2c1.1,1.1,1.1,2.9,0,4C13.8,74.5,13.1,74.8,12.3,74.8z M7.1,65.9c-0.2,0-0.4,0.1-0.6,0.2 c-0.2,0.2-0.2,0.4-0.2,0.6s0.1,0.4,0.2,0.6l5.2,5.2c0.3,0.3,0.9,0.3,1.2,0c0.3-0.3,0.3-0.8,0-1.2l-5.2-5.2 C7.5,66,7.3,65.9,7.1,65.9z'/></g><g><polygon fill='#000000' points='31.7,58.7 30.3,57.3 70,17.6 70,9 62.4,9 23.3,49.4 21.9,48 61.6,7 72,7 72,18.4 '/></g><g><rect x='46' y='6.7' transform='matrix(0.7168 0.6973 -0.6973 0.7168 35.9716 -23.568)' fill='#000000' width='2' height='51.6'/></g><g><rect x='13' y='61' fill='#000000' width='2' height='7'/></g><g><rect x='17' y='57' fill='#000000' width='2' height='7'/></g></g><g><g><polygon fill='#000000' points='68.4,71.4 56.7,59.7 58.1,58.3 68.4,68.6 70.8,66.1 60.5,55.8 61.9,54.4 73.6,66.1 '/></g><g><path fill='#000000' d='M52.2,61.6c-1.7,0-3.4-0.7-4.6-1.9L46.9,59l14.3-14.3l0.7,0.7c2.5,2.5,2.5,6.6,0,9.2l-5.1,5.1 C55.6,60.9,53.9,61.6,52.2,61.6z M49.8,58.9c0.7,0.4,1.5,0.7,2.4,0.7c1.2,0,2.3-0.5,3.2-1.3l5.1-5.1c1.5-1.5,1.7-3.8,0.6-5.6 L49.8,58.9z'/></g><g><path fill='#000000' d='M68.9,74.8c-0.8,0-1.5-0.3-2-0.8c-1.1-1.1-1.1-2.9,0-4l5.2-5.2c1.1-1.1,2.9-1.1,4,0c0.5,0.5,0.8,1.2,0.8,2 c0,0.8-0.3,1.5-0.8,2L70.9,74C70.4,74.5,69.7,74.8,68.9,74.8z M74.2,65.9c-0.2,0-0.4,0.1-0.6,0.2l-5.2,5.2c-0.3,0.3-0.3,0.8,0,1.2 c0.3,0.3,0.9,0.3,1.2,0l5.2-5.2c0.2-0.2,0.2-0.4,0.2-0.6s-0.1-0.4-0.2-0.6C74.6,66,74.4,65.9,74.2,65.9z'/></g><g><rect x='38.6' y='52.3' transform='matrix(0.7082 0.706 -0.706 0.7082 50.8397 -16.4875)' fill='#000000' width='13.4' height='2'/></g><g><polygon fill='#000000' points='30.6,39.9 9,18.4 9,7 19.7,7 41.1,29.1 39.7,30.5 18.8,9 11,9 11,17.6 32,38.5 '/></g><g><rect x='47.8' y='43.1' transform='matrix(0.6959 0.7181 -0.7181 0.6959 48.1381 -25.5246)' fill='#000000' width='12.8' height='2'/></g><g><rect x='12' y='23.1' transform='matrix(0.6974 0.7167 -0.7167 0.6974 25.1384 -11.3825)' fill='#000000' width='28.1' height='2'/></g><g><rect x='43.8' y='46.4' transform='matrix(0.6974 0.7167 -0.7167 0.6974 48.7492 -20.5985)' fill='#000000' width='10' height='2'/></g><g><rect x='66' y='61' fill='#000000' width='2' height='7'/></g><g><rect x='62' y='57' fill='#000000' width='2' height='7'/></g></g></svg>;
};

View File

@@ -1,6 +1,7 @@
import React from 'react'; const React = require('react');
const createClass = require('create-react-class');
export default function(props){ module.exports = function(props){
return <svg version='1.1' x='0px' y='0px' viewBox='0 0 90 112.5' enableBackground='new 0 0 90 90' > return <svg version='1.1' x='0px' y='0px' viewBox='0 0 90 112.5' enableBackground='new 0 0 90 90' >
<path d='M25.363,25.54c0,1.906,8.793,3.454,19.636,3.454c10.848,0,19.638-1.547,19.638-3.454c0-1.12-3.056-2.117-7.774-2.75 c-1.418,1.891-3.659,3.133-6.208,3.133c-2.85,0-5.315-1.547-6.67-3.833C33.617,22.185,25.363,23.692,25.363,25.54z'/><path d='M84.075,54.142c0-8.68-2.868-17.005-8.144-23.829c1.106-1.399,1.41-2.771,1.41-3.854c0-6.574-10.245-9.358-19.264-10.533 c0.209,0.706,0.359,1.439,0.359,2.215c0,0.09-0.022,0.17-0.028,0.26l0,0c-0.028,0.853-0.195,1.667-0.479,2.429 c9.106,1.282,14.508,3.754,14.508,5.63c0,2.644-10.688,6.486-27.439,6.486c-16.748,0-27.438-3.842-27.438-6.486 c0-2.542,9.904-6.183,25.559-6.459c-0.098-0.396-0.159-0.807-0.2-1.223c0.006,0,0.013,0,0.017,0 c-0.017-0.213-0.063-0.417-0.063-0.636c0-1.084,0.226-2.119,0.628-3.058c-6.788,0.129-30.846,1.299-30.846,11.376 c0,1.083,0.305,2.455,1.411,3.854c-5.276,6.823-8.145,15.149-8.145,23.829c0,11.548,5.187,20.107,14.693,25.115 c-0.902,3.146-1.391,7.056,1.111,8.181c2.626,1.178,5.364-2.139,7.111-5.005c4.73,1.261,10.13,1.923,16.161,1.923 c6.034,0,11.428-0.661,16.158-1.922c1.75,2.865,4.493,6.18,7.112,5.004c2.504-1.123,2.014-5.035,1.113-8.179 C78.889,74.249,84.075,65.689,84.075,54.142z M70.39,31.392c5.43,6.046,8.78,14,8.78,22.75c0,20.919-18.582,25.309-34.171,25.309 c-15.587,0-34.17-4.39-34.17-25.309c0-8.75,3.35-16.7,8.781-22.753c5.561,2.643,15.502,4.009,25.389,4.009 C54.886,35.397,64.829,34.031,70.39,31.392z'/><path d='M50.654,23.374c2.892,0,5.234-2.341,5.234-5.233c0-2.887-2.343-5.23-5.234-5.23c-2.887,0-5.231,2.343-5.231,5.23 C45.423,21.032,47.768,23.374,50.654,23.374z'/> <path d='M25.363,25.54c0,1.906,8.793,3.454,19.636,3.454c10.848,0,19.638-1.547,19.638-3.454c0-1.12-3.056-2.117-7.774-2.75 c-1.418,1.891-3.659,3.133-6.208,3.133c-2.85,0-5.315-1.547-6.67-3.833C33.617,22.185,25.363,23.692,25.363,25.54z'/><path d='M84.075,54.142c0-8.68-2.868-17.005-8.144-23.829c1.106-1.399,1.41-2.771,1.41-3.854c0-6.574-10.245-9.358-19.264-10.533 c0.209,0.706,0.359,1.439,0.359,2.215c0,0.09-0.022,0.17-0.028,0.26l0,0c-0.028,0.853-0.195,1.667-0.479,2.429 c9.106,1.282,14.508,3.754,14.508,5.63c0,2.644-10.688,6.486-27.439,6.486c-16.748,0-27.438-3.842-27.438-6.486 c0-2.542,9.904-6.183,25.559-6.459c-0.098-0.396-0.159-0.807-0.2-1.223c0.006,0,0.013,0,0.017,0 c-0.017-0.213-0.063-0.417-0.063-0.636c0-1.084,0.226-2.119,0.628-3.058c-6.788,0.129-30.846,1.299-30.846,11.376 c0,1.083,0.305,2.455,1.411,3.854c-5.276,6.823-8.145,15.149-8.145,23.829c0,11.548,5.187,20.107,14.693,25.115 c-0.902,3.146-1.391,7.056,1.111,8.181c2.626,1.178,5.364-2.139,7.111-5.005c4.73,1.261,10.13,1.923,16.161,1.923 c6.034,0,11.428-0.661,16.158-1.922c1.75,2.865,4.493,6.18,7.112,5.004c2.504-1.123,2.014-5.035,1.113-8.179 C78.889,74.249,84.075,65.689,84.075,54.142z M70.39,31.392c5.43,6.046,8.78,14,8.78,22.75c0,20.919-18.582,25.309-34.171,25.309 c-15.587,0-34.17-4.39-34.17-25.309c0-8.75,3.35-16.7,8.781-22.753c5.561,2.643,15.502,4.009,25.389,4.009 C54.886,35.397,64.829,34.031,70.39,31.392z'/><path d='M50.654,23.374c2.892,0,5.234-2.341,5.234-5.233c0-2.887-2.343-5.23-5.234-5.23c-2.887,0-5.231,2.343-5.231,5.23 C45.423,21.032,47.768,23.374,50.654,23.374z'/>
<circle cx='62.905' cy='10.089' r='3.595'/> <circle cx='62.905' cy='10.089' r='3.595'/>

View File

@@ -1,5 +1,6 @@
import React from 'react'; const React = require('react');
const createClass = require('create-react-class');
export default function(props){ module.exports = function(props){
return <svg version='1.1' x='0px' y='0px' viewBox='0 0 100 100' enableBackground='new 0 0 100 100'><path d='M80.644,87.982l16.592-41.483c0.054-0.128,0.088-0.26,0.108-0.394c0.006-0.039,0.007-0.077,0.011-0.116 c0.007-0.087,0.008-0.174,0.002-0.26c-0.003-0.046-0.007-0.091-0.014-0.137c-0.014-0.089-0.036-0.176-0.063-0.262 c-0.012-0.034-0.019-0.069-0.031-0.103c-0.047-0.118-0.106-0.229-0.178-0.335c-0.004-0.006-0.006-0.012-0.01-0.018L67.999,3.358 c-0.01-0.013-0.003-0.026-0.013-0.04L68,3.315V4c0,0-0.033,0-0.037,0c-0.403-1-1.094-1.124-1.752-0.976 c0,0.004-0.004-0.012-0.007-0.012C66.201,3.016,66.194,3,66.194,3H66.19h-0.003h-0.003h-0.004h-0.003c0,0-0.004,0-0.007,0 s-0.003-0.151-0.007-0.151L20.495,15.227c-0.025,0.007-0.046-0.019-0.071-0.011c-0.087,0.028-0.172,0.041-0.253,0.083 c-0.054,0.027-0.102,0.053-0.152,0.085c-0.051,0.033-0.101,0.061-0.147,0.099c-0.044,0.036-0.084,0.073-0.124,0.113 c-0.048,0.048-0.093,0.098-0.136,0.152c-0.03,0.039-0.059,0.076-0.085,0.117c-0.046,0.07-0.084,0.145-0.12,0.223 c-0.011,0.023-0.027,0.042-0.036,0.066L2.911,57.664C2.891,57.715,3,57.768,3,57.82v0.002c0,0.186,0,0.375,0,0.562 c0,0.004,0,0.004,0,0.008c0,0,0,0,0,0.002c0,0,0,0,0,0.004v0.004v0.002c0,0.074-0.002,0.15,0.012,0.223 C3.015,58.631,3,58.631,3,58.633c0,0.004,0,0.004,0,0.008c0,0,0,0,0,0.002c0,0,0,0,0,0.004v0.004c0,0,0,0,0,0.002v0.004 c0,0.191-0.046,0.377,0.06,0.545c0-0.002-0.03,0.004-0.03,0.004c0,0.004-0.03,0.004-0.03,0.004c0,0.002,0,0.002,0,0.002 l-0.045,0.004c0.03,0.047,0.036,0.09,0.068,0.133l29.049,37.359c0.002,0.004,0,0.006,0.002,0.01c0.002,0.002,0,0.004,0.002,0.008 c0.006,0.008,0.014,0.014,0.021,0.021c0.024,0.029,0.052,0.051,0.078,0.078c0.027,0.029,0.053,0.057,0.082,0.082 c0.03,0.027,0.055,0.062,0.086,0.088c0.026,0.02,0.057,0.033,0.084,0.053c0.04,0.027,0.081,0.053,0.123,0.076 c0.005,0.004,0.01,0.008,0.016,0.01c0.087,0.051,0.176,0.09,0.269,0.123c0.042,0.014,0.082,0.031,0.125,0.043 c0.021,0.006,0.041,0.018,0.062,0.021c0.123,0.027,0.249,0.043,0.375,0.043c0.099,0,0.202-0.012,0.304-0.027l45.669-8.303 c0.057-0.01,0.108-0.021,0.163-0.037C79.547,88.992,79.562,89,79.575,89c0.004,0,0.004,0,0.004,0c0.021,0,0.039-0.027,0.06-0.035 c0.041-0.014,0.08-0.034,0.12-0.052c0.021-0.01,0.044-0.019,0.064-0.03c0.017-0.01,0.026-0.015,0.033-0.017 c0.014-0.008,0.023-0.021,0.037-0.028c0.14-0.078,0.269-0.174,0.38-0.285c0.014-0.016,0.024-0.034,0.038-0.048 c0.109-0.119,0.201-0.252,0.271-0.398c0.006-0.01,0.016-0.018,0.021-0.029c0.004-0.008,0.008-0.017,0.011-0.026 c0.002-0.004,0.003-0.006,0.005-0.01C80.627,88.021,80.635,88.002,80.644,87.982z M77.611,84.461L48.805,66.453l32.407-25.202 L77.611,84.461z M46.817,63.709L35.863,23.542l43.818,14.608L46.817,63.709z M84.668,40.542l8.926,5.952l-11.902,29.75 L84.668,40.542z M89.128,39.446L84.53,36.38l-6.129-12.257L89.128,39.446z M79.876,34.645L37.807,20.622L65.854,6.599L79.876,34.645 z M33.268,19.107l-6.485-2.162l23.781-6.487L33.268,19.107z M21.92,18.895l8.67,2.891L10.357,47.798L21.92,18.895z M32.652,24.649 l10.845,39.757L7.351,57.178L32.652,24.649z M43.472,67.857L32.969,92.363L8.462,60.855L43.472,67.857z M46.631,69.09l27.826,17.393 l-38.263,6.959L46.631,69.09z'></path></svg>; return <svg version='1.1' x='0px' y='0px' viewBox='0 0 100 100' enableBackground='new 0 0 100 100'><path d='M80.644,87.982l16.592-41.483c0.054-0.128,0.088-0.26,0.108-0.394c0.006-0.039,0.007-0.077,0.011-0.116 c0.007-0.087,0.008-0.174,0.002-0.26c-0.003-0.046-0.007-0.091-0.014-0.137c-0.014-0.089-0.036-0.176-0.063-0.262 c-0.012-0.034-0.019-0.069-0.031-0.103c-0.047-0.118-0.106-0.229-0.178-0.335c-0.004-0.006-0.006-0.012-0.01-0.018L67.999,3.358 c-0.01-0.013-0.003-0.026-0.013-0.04L68,3.315V4c0,0-0.033,0-0.037,0c-0.403-1-1.094-1.124-1.752-0.976 c0,0.004-0.004-0.012-0.007-0.012C66.201,3.016,66.194,3,66.194,3H66.19h-0.003h-0.003h-0.004h-0.003c0,0-0.004,0-0.007,0 s-0.003-0.151-0.007-0.151L20.495,15.227c-0.025,0.007-0.046-0.019-0.071-0.011c-0.087,0.028-0.172,0.041-0.253,0.083 c-0.054,0.027-0.102,0.053-0.152,0.085c-0.051,0.033-0.101,0.061-0.147,0.099c-0.044,0.036-0.084,0.073-0.124,0.113 c-0.048,0.048-0.093,0.098-0.136,0.152c-0.03,0.039-0.059,0.076-0.085,0.117c-0.046,0.07-0.084,0.145-0.12,0.223 c-0.011,0.023-0.027,0.042-0.036,0.066L2.911,57.664C2.891,57.715,3,57.768,3,57.82v0.002c0,0.186,0,0.375,0,0.562 c0,0.004,0,0.004,0,0.008c0,0,0,0,0,0.002c0,0,0,0,0,0.004v0.004v0.002c0,0.074-0.002,0.15,0.012,0.223 C3.015,58.631,3,58.631,3,58.633c0,0.004,0,0.004,0,0.008c0,0,0,0,0,0.002c0,0,0,0,0,0.004v0.004c0,0,0,0,0,0.002v0.004 c0,0.191-0.046,0.377,0.06,0.545c0-0.002-0.03,0.004-0.03,0.004c0,0.004-0.03,0.004-0.03,0.004c0,0.002,0,0.002,0,0.002 l-0.045,0.004c0.03,0.047,0.036,0.09,0.068,0.133l29.049,37.359c0.002,0.004,0,0.006,0.002,0.01c0.002,0.002,0,0.004,0.002,0.008 c0.006,0.008,0.014,0.014,0.021,0.021c0.024,0.029,0.052,0.051,0.078,0.078c0.027,0.029,0.053,0.057,0.082,0.082 c0.03,0.027,0.055,0.062,0.086,0.088c0.026,0.02,0.057,0.033,0.084,0.053c0.04,0.027,0.081,0.053,0.123,0.076 c0.005,0.004,0.01,0.008,0.016,0.01c0.087,0.051,0.176,0.09,0.269,0.123c0.042,0.014,0.082,0.031,0.125,0.043 c0.021,0.006,0.041,0.018,0.062,0.021c0.123,0.027,0.249,0.043,0.375,0.043c0.099,0,0.202-0.012,0.304-0.027l45.669-8.303 c0.057-0.01,0.108-0.021,0.163-0.037C79.547,88.992,79.562,89,79.575,89c0.004,0,0.004,0,0.004,0c0.021,0,0.039-0.027,0.06-0.035 c0.041-0.014,0.08-0.034,0.12-0.052c0.021-0.01,0.044-0.019,0.064-0.03c0.017-0.01,0.026-0.015,0.033-0.017 c0.014-0.008,0.023-0.021,0.037-0.028c0.14-0.078,0.269-0.174,0.38-0.285c0.014-0.016,0.024-0.034,0.038-0.048 c0.109-0.119,0.201-0.252,0.271-0.398c0.006-0.01,0.016-0.018,0.021-0.029c0.004-0.008,0.008-0.017,0.011-0.026 c0.002-0.004,0.003-0.006,0.005-0.01C80.627,88.021,80.635,88.002,80.644,87.982z M77.611,84.461L48.805,66.453l32.407-25.202 L77.611,84.461z M46.817,63.709L35.863,23.542l43.818,14.608L46.817,63.709z M84.668,40.542l8.926,5.952l-11.902,29.75 L84.668,40.542z M89.128,39.446L84.53,36.38l-6.129-12.257L89.128,39.446z M79.876,34.645L37.807,20.622L65.854,6.599L79.876,34.645 z M33.268,19.107l-6.485-2.162l23.781-6.487L33.268,19.107z M21.92,18.895l8.67,2.891L10.357,47.798L21.92,18.895z M32.652,24.649 l10.845,39.757L7.351,57.178L32.652,24.649z M43.472,67.857L32.969,92.363L8.462,60.855L43.472,67.857z M46.631,69.09l27.826,17.393 l-38.263,6.959L46.631,69.09z'></path></svg>;
}; };

View File

@@ -1,5 +1,7 @@
import globalJsdom from 'jsdom-global';
globalJsdom();
require('jsdom-global')();
import { safeHTML } from '../../client/homebrew/brewRenderer/safeHTML'; import { safeHTML } from '../../client/homebrew/brewRenderer/safeHTML';
test('Exit if no document', function() { test('Exit if no document', function() {

View File

@@ -1,6 +1,6 @@
import Markdown from 'markdown.js'; import Markdown from 'naturalcrit/markdown.js';
test('Processes the markdown within an HTML block if its just a class wrapper', function() { test('Processes the markdown within an HTML block if its just a class wrapper', function() {
const source = '<div>*Bold text*</div>'; const source = '<div>*Bold text*</div>';

View File

@@ -1,6 +1,6 @@
import Markdown from 'markdown.js'; import Markdown from 'naturalcrit/markdown.js';
describe('Inline Definition Lists', ()=>{ describe('Inline Definition Lists', ()=>{
test('No Term 1 Definition', function() { test('No Term 1 Definition', function() {

View File

@@ -1,5 +1,5 @@
import Markdown from 'markdown.js'; import Markdown from 'naturalcrit/markdown.js';
import dedent from 'dedent'; const dedent = require('dedent-tabs').default;
// Marked.js adds line returns after closing tags on some default tokens. // Marked.js adds line returns after closing tags on some default tokens.
// This removes those line returns for comparison sake. // This removes those line returns for comparison sake.

View File

@@ -1,6 +1,6 @@
import Markdown from 'markdown.js'; import Markdown from 'naturalcrit/markdown.js';
describe('Hard Breaks', ()=>{ describe('Hard Breaks', ()=>{
test('Single Break', function() { test('Single Break', function() {

View File

@@ -1,7 +1,7 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import dedent from 'dedent'; const dedent = require('dedent-tabs').default;
import Markdown from 'markdown.js'; import Markdown from 'naturalcrit/markdown.js';
// Marked.js adds line returns after closing tags on some default tokens. // Marked.js adds line returns after closing tags on some default tokens.
// This removes those line returns for comparison sake. // This removes those line returns for comparison sake.

View File

@@ -1,6 +1,6 @@
import Markdown from 'markdown.js'; import Markdown from 'naturalcrit/markdown.js';
describe('Non-Breaking Spaces Interactions', ()=>{ describe('Non-Breaking Spaces Interactions', ()=>{
test('I am actually a single-line definition list!', function() { test('I am actually a single-line definition list!', function() {

View File

@@ -1,6 +1,6 @@
import Markdown from 'markdown.js'; import Markdown from 'naturalcrit/markdown.js';
describe('Justification', ()=>{ describe('Justification', ()=>{
test('Left Justify', function() { test('Left Justify', function() {

View File

@@ -1,7 +1,7 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import dedent from 'dedent'; const dedent = require('dedent-tabs').default;
import Markdown from 'markdown.js'; import Markdown from 'naturalcrit/markdown.js';
// Marked.js adds line returns after closing tags on some default tokens. // Marked.js adds line returns after closing tags on some default tokens.
// This removes those line returns for comparison sake. // This removes those line returns for comparison sake.

Some files were not shown because too many files have changed in this diff Show More