0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-10 11:22:40 +00:00

Merge branch 'master' into fixFacingFlowPrintIssues

This commit is contained in:
G.Ambatte
2025-04-16 10:31:45 +12:00
committed by GitHub
28 changed files with 2201 additions and 566 deletions

View File

@@ -13,6 +13,7 @@ const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?: *{[^\n{}]*})?$)/m;
const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
const SNIPPETBAR_HEIGHT = 25;
const DEFAULT_STYLE_TEXT = dedent`
/*=======--- Example CSS styling ---=======*/
@@ -22,6 +23,13 @@ const DEFAULT_STYLE_TEXT = dedent`
color: black;
}`;
const DEFAULT_SNIPPET_TEXT = dedent`
\snippet example snippet
The text between \`\snippet title\` lines will become a snippet of name \`title\` as this example provides.
This snippet is accessible in the brew tab, and will be inherited if the brew is used as a theme.
`;
let isJumping = false;
const Editor = createClass({
@@ -36,6 +44,7 @@ const Editor = createClass({
onTextChange : ()=>{},
onStyleChange : ()=>{},
onMetaChange : ()=>{},
onSnipChange : ()=>{},
reportError : ()=>{},
onCursorPageChange : ()=>{},
@@ -52,7 +61,7 @@ const Editor = createClass({
getInitialState : function() {
return {
editorTheme : this.props.editorTheme,
view : 'text' //'text', 'style', 'meta'
view : 'text' //'text', 'style', 'meta', 'snippet'
};
},
@@ -62,12 +71,11 @@ const Editor = createClass({
isText : function() {return this.state.view == 'text';},
isStyle : function() {return this.state.view == 'style';},
isMeta : function() {return this.state.view == 'meta';},
isSnip : function() {return this.state.view == 'snippet';},
componentDidMount : function() {
this.updateEditorSize();
this.highlightCustomMarkdown();
window.addEventListener('resize', this.updateEditorSize);
document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys);
document.addEventListener('keydown', this.handleControlKeys);
@@ -82,10 +90,6 @@ const Editor = createClass({
}
},
componentWillUnmount : function() {
window.removeEventListener('resize', this.updateEditorSize);
},
componentDidUpdate : function(prevProps, prevState, snapshot) {
this.highlightCustomMarkdown();
@@ -118,14 +122,6 @@ const Editor = createClass({
}
},
updateEditorSize : function() {
if(this.codeEditor.current) {
let paneHeight = this.editor.current.parentNode.clientHeight;
paneHeight -= SNIPPETBAR_HEIGHT;
this.codeEditor.current.codeMirror.setSize(null, paneHeight);
}
},
updateCurrentCursorPage : function(cursor) {
const lines = this.props.brew.text.split('\n').slice(1, cursor.line + 1);
const pageRegex = this.props.brew.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
@@ -146,17 +142,17 @@ const Editor = createClass({
handleViewChange : function(newView){
this.props.setMoveArrows(newView === 'text');
this.setState({
view : newView
}, ()=>{
this.codeEditor.current?.codeMirror.focus();
this.updateEditorSize();
}); //TODO: not sure if updateeditorsize needed
});
},
highlightCustomMarkdown : function(){
if(!this.codeEditor.current) return;
if(this.state.view === 'text') {
if((this.state.view === 'text') ||(this.state.view === 'snippet')) {
const codeMirror = this.codeEditor.current.codeMirror;
codeMirror.operation(()=>{ // Batch CodeMirror styling
@@ -175,12 +171,18 @@ const Editor = createClass({
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
let userSnippetCount = 1; // start snippet count from snippet 1
let editorPageCount = 1; // start page count from page 1
_.forEach(this.props.brew.text.split('\n'), (line, lineNumber)=>{
const whichSource = this.state.view === 'text' ? this.props.brew.text : this.props.brew.snippets;
_.forEach(whichSource?.split('\n'), (line, lineNumber)=>{
const tabHighlight = this.state.view === 'text' ? 'pageLine' : 'snippetLine';
const textOrSnip = this.state.view === 'text';
//reset custom line styles
codeMirror.removeLineClass(lineNumber, 'background', 'pageLine');
codeMirror.removeLineClass(lineNumber, 'background', 'snippetLine');
codeMirror.removeLineClass(lineNumber, 'text');
codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash');
@@ -191,22 +193,24 @@ const Editor = createClass({
// Styling for \page breaks
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
(this.props.renderer == 'V3' && line.match(PAGEBREAK_REGEX_V3))) {
(this.props.renderer == 'V3' && line.match(textOrSnip ? PAGEBREAK_REGEX_V3 : SNIPPETBREAK_REGEX_V3))) {
if(lineNumber > 0) // Since \page is optional on first line of document,
if((lineNumber > 0) && (textOrSnip)) // Since \page is optional on first line of document,
editorPageCount += 1; // don't use it to increment page count; stay at 1
else if(this.state.view !== 'text') userSnippetCount += 1;
// add back the original class 'background' but also add the new class '.pageline'
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
codeMirror.addLineClass(lineNumber, 'background', tabHighlight);
const pageCountElement = Object.assign(document.createElement('span'), {
className : 'editor-page-count',
textContent : editorPageCount
textContent : textOrSnip ? editorPageCount : userSnippetCount
});
codeMirror.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement);
};
// New Codemirror styling for V3 renderer
if(this.props.renderer == 'V3') {
if(this.props.renderer === 'V3') {
if(line.match(/^\\column$/)){
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
}
@@ -462,6 +466,21 @@ const Editor = createClass({
userThemes={this.props.userThemes}/>
</>;
}
if(this.isSnip()){
if(!this.props.brew.snippets) { this.props.brew.snippets = DEFAULT_SNIPPET_TEXT; }
return <>
<CodeEditor key='codeEditor'
ref={this.codeEditor}
language='gfm'
view={this.state.view}
value={this.props.brew.snippets}
onChange={this.props.onSnipChange}
enableFolding={true}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} />
</>;
}
},
redo : function(){
@@ -502,7 +521,7 @@ const Editor = createClass({
historySize={this.historySize()}
currentEditorTheme={this.state.editorTheme}
updateEditorTheme={this.updateEditorTheme}
snippetBundle={this.props.snippetBundle}
themeBundle={this.props.themeBundle}
cursorPos={this.codeEditor.current?.getCursorPosition() || {}}
updateBrew={this.props.updateBrew}
/>

View File

@@ -2,11 +2,12 @@
.editor {
position : relative;
width : 100%;
height : 100%;
container : editor / inline-size;
.codeEditor {
height : 100%;
.pageLine {
.CodeMirror { height : 100%; }
.pageLine, .snippetLine {
background : #33333328;
border-top : #333399 solid 1px;
}
@@ -14,6 +15,10 @@
float : right;
color : grey;
}
.editor-snippet-count {
float : right;
color : grey;
}
.columnSplit {
font-style : italic;
color : grey;
@@ -104,3 +109,7 @@
}
}
@container editor (width < 553px) {
.editor .codeEditor .CodeMirror { height : calc(100% - 51px);}
}

View File

@@ -6,6 +6,7 @@ const _ = require('lodash');
const cx = require('classnames');
import { loadHistory } from '../../utils/versionHistory.js';
import { brewSnippetsToJSON } from '../../../../shared/helpers.js';
//Import all themes
const ThemeSnippets = {};
@@ -40,7 +41,7 @@ const Snippetbar = createClass({
unfoldCode : ()=>{},
updateEditorTheme : ()=>{},
cursorPos : {},
snippetBundle : [],
themeBundle : [],
updateBrew : ()=>{}
};
},
@@ -64,7 +65,10 @@ const Snippetbar = createClass({
},
componentDidUpdate : async function(prevProps, prevState) {
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) {
if(prevProps.renderer != this.props.renderer ||
prevProps.theme != this.props.theme ||
prevProps.themeBundle != this.props.themeBundle ||
prevProps.brew.snippets != this.props.brew.snippets) {
this.setState({
snippets : this.compileSnippets()
});
@@ -97,7 +101,7 @@ const Snippetbar = createClass({
if(key == 'snippets') {
const result = _.reverse(_.unionBy(_.reverse(newValue), _.reverse(oldValue), 'name')); // Join snippets together, with preference for the child theme over the parent theme
return result.filter((snip)=>snip.gen || snip.subsnippets);
}
};
},
compileSnippets : function() {
@@ -105,15 +109,21 @@ const Snippetbar = createClass({
let oldSnippets = _.keyBy(compiledSnippets, 'groupName');
for (let snippets of this.props.snippetBundle) {
if(typeof(snippets) == 'string') // load staticThemes as needed; they were sent as just a file name
snippets = ThemeSnippets[snippets];
if(this.props.themeBundle.snippets) {
for (let snippets of this.props.themeBundle.snippets) {
if(typeof(snippets) == 'string') // load staticThemes as needed; they were sent as just a file name
snippets = ThemeSnippets[snippets];
const newSnippets = _.keyBy(_.cloneDeep(snippets), 'groupName');
compiledSnippets = _.values(_.mergeWith(oldSnippets, newSnippets, this.mergeCustomizer));
const newSnippets = _.keyBy(_.cloneDeep(snippets), 'groupName');
compiledSnippets = _.values(_.mergeWith(oldSnippets, newSnippets, this.mergeCustomizer));
oldSnippets = _.keyBy(compiledSnippets, 'groupName');
oldSnippets = _.keyBy(compiledSnippets, 'groupName');
}
}
const userSnippetsasJSON = brewSnippetsToJSON(this.props.brew.title || 'New Document', this.props.brew.snippets, this.props.themeBundle.snippets);
compiledSnippets.push(userSnippetsasJSON);
return compiledSnippets;
},
@@ -207,8 +217,6 @@ const Snippetbar = createClass({
renderEditorButtons : function(){
if(!this.props.showEditButtons) return;
return (
<div className='editors'>
{this.props.view !== 'meta' && <><div className='historyTools'>
@@ -242,7 +250,6 @@ const Snippetbar = createClass({
</div>
</div></>}
<div className='tabs'>
<div className={cx('text', { selected: this.props.view === 'text' })}
onClick={()=>this.props.onViewChange('text')}>
@@ -252,6 +259,10 @@ const Snippetbar = createClass({
onClick={()=>this.props.onViewChange('style')}>
<i className='fa fa-paint-brush' />
</div>
<div className={cx('snippet', { selected: this.props.view === 'snippet' })}
onClick={()=>this.props.onViewChange('snippet')}>
<i className='fas fa-th-list' />
</div>
<div className={cx('meta', { selected: this.props.view === 'meta' })}
onClick={()=>this.props.onViewChange('meta')}>
<i className='fas fa-info-circle' />
@@ -272,11 +283,6 @@ const Snippetbar = createClass({
module.exports = Snippetbar;
const SnippetGroup = createClass({
displayName : 'SnippetGroup',
getDefaultProps : function() {
@@ -310,7 +316,8 @@ const SnippetGroup = createClass({
},
render : function(){
return <div className='snippetGroup snippetBarButton'>
const snippetGroup = `snippetGroup snippetBarButton ${this.props.snippets.length === 0 ? 'disabledSnippets' : ''}`;
return <div className={snippetGroup}>
<div className='text'>
<i className={this.props.icon} />
<span className='groupName'>{this.props.groupName}</span>

View File

@@ -14,13 +14,13 @@
.snippets {
display : flex;
justify-content : flex-start;
min-width : 327.58px;
min-width : 432.18px; //must be controlled every time an item is added, must be hardcoded for the wrapping as it is applied
}
.editors {
display : flex;
justify-content : flex-end;
min-width : 225px;
min-width : 250px; //must be controlled every time an item is added, must be hardcoded for the wrapping as it is applied
&:only-child {min-width : unset; margin-left : auto;}
@@ -51,6 +51,9 @@
&.meta {
.tooltipLeft('Properties');
}
&.snip {
.tooltipLeft('Snippets');
}
&.undo {
.tooltipLeft('Undo');
font-size : 0.75em;
@@ -226,8 +229,15 @@
}
}
}
.disabledSnippets {
color: grey;
cursor: not-allowed;
&:hover { background-color: #DDDDDD;}
}
}
@container editor (width < 553px) {
@container editor (width < 683px) {
.snippetBar {
.editors {
flex : 1;

View File

@@ -150,6 +150,18 @@ const EditPage = createClass({
}), ()=>{if(this.state.autoSave) this.trySave();});
},
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 },
isPending : true,
htmlErrors : htmlErrors,
}), ()=>{if(this.state.autoSave) this.trySave();});
},
handleStyleChange : function(style){
this.setState((prevState)=>({
brew : { ...prevState.brew, style: style }
@@ -443,7 +455,7 @@ const EditPage = createClass({
<Meta name='robots' content='noindex, nofollow' />
{this.renderNavbar()}
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} />}
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} reviewRequested={this.props.brew.lock.reviewRequested} />}
<div className='content'>
<SplitPane onDragFinish={this.handleSplitMove}>
<Editor
@@ -451,12 +463,12 @@ const EditPage = createClass({
brew={this.state.brew}
onTextChange={this.handleTextChange}
onStyleChange={this.handleStyleChange}
onSnipChange={this.handleSnipChange}
onMetaChange={this.handleMetaChange}
reportError={this.errorReported}
renderer={this.state.brew.renderer}
userThemes={this.props.userThemes}
themeBundle={this.state.themeBundle}
snippetBundle={this.state.themeBundle.snippets}
updateBrew={this.updateBrew}
onCursorPageChange={this.handleEditorCursorPageChange}
onViewPageChange={this.handleEditorViewPageChange}

View File

@@ -1,17 +1,30 @@
require('./lockNotification.less');
const React = require('react');
import './lockNotification.less';
import * as React from 'react';
import request from '../../../utils/request-middleware.js';
import Dialog from '../../../../components/dialog.jsx';
function LockNotification(props) {
props = {
shareId : 0,
disableLock : ()=>{},
message : '',
shareId : 0,
disableLock : ()=>{},
lock : {},
message : 'Unable to retrieve Lock Message',
reviewRequested : false,
...props
};
const removeLock = ()=>{
alert(`Not yet implemented - ID ${props.shareId}`);
const [reviewState, setReviewState] = React.useState(props.reviewRequested);
const removeLock = async ()=>{
await request.put(`/api/lock/review/request/${props.shareId}`)
.then(()=>{
setReviewState(true);
});
};
const renderReviewButton = function(){
if(reviewState){ return <button className='inactive'>REVIEW REQUESTED</button>; };
return <button onClick={removeLock}>REQUEST LOCK REMOVAL</button>;
};
return <Dialog className='lockNotification' blocking closeText='CONTINUE TO EDITOR' >
@@ -19,11 +32,11 @@ function LockNotification(props) {
<p>This brew been locked by the Administrators. It will not be accessible by any method other than the Editor until the lock is removed.</p>
<hr />
<h3>LOCK REASON</h3>
<p>{props.message || 'Unable to retrieve Lock Message'}</p>
<p>{props.message}</p>
<hr />
<p>Once you have resolved this issue, click REQUEST LOCK REMOVAL to notify the Administrators for review.</p>
<p>Click CONTINUE TO EDITOR to temporarily hide this notification; it will reappear the next time the page is reloaded.</p>
<button onClick={removeLock}>REQUEST LOCK REMOVAL</button>
{renderReviewButton()}
</Dialog>;
};

View File

@@ -11,10 +11,12 @@
&::backdrop { background-color : #000000AA; }
button {
padding : 2px 15px;
margin : 10px;
color : white;
background-color : #333333;
&.inactive,
&:hover { background-color : #777777; }
}

View File

@@ -194,13 +194,47 @@ const errorIndex = (props)=>{
**Brew ID:** ${props.brew.brewId}
**Brew Title:** ${escape(props.brew.brewTitle)}`,
**Brew Title:** ${escape(props.brew.brewTitle)}
**Brew Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}`,
// ####### Admin page error #######
'52' : dedent`
## Access Denied
You need to provide correct administrator credentials to access this page.`,
// ####### Lock Errors
'60' : dedent`Lock Error: General`,
'61' : dedent`Lock Get Error: Unable to get lock count`,
'62' : dedent`Lock Set Error: Cannot lock`,
'63' : dedent`Lock Set Error: Brew not found`,
'64' : dedent`Lock Set Error: Already locked`,
'65' : dedent`Lock Remove Error: Cannot unlock`,
'66' : dedent`Lock Remove Error: Brew not found`,
'67' : dedent`Lock Remove Error: Not locked`,
'68' : dedent`Lock Get Review Error: Cannot get review requests`,
'69' : dedent`Lock Set Review Error: Cannot set review request`,
'70' : dedent`Lock Set Review Error: Brew not found`,
'71' : dedent`Lock Set Review Error: Review already requested`,
'72' : dedent`Lock Remove Review Error: Cannot clear review request`,
'73' : dedent`Lock Remove Review Error: Brew not found`,
// ####### Other Errors
'90' : dedent` An unexpected error occurred while looking for these brews.
Try again in a few minutes.`,

View File

@@ -108,7 +108,7 @@ const HomePage = createClass({
onTextChange={this.handleTextChange}
renderer={this.state.brew.renderer}
showEditButtons={false}
snippetBundle={this.state.themeBundle.snippets}
themeBundle={this.state.themeBundle}
onCursorPageChange={this.handleEditorCursorPageChange}
onViewPageChange={this.handleEditorViewPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}

View File

@@ -141,6 +141,18 @@ const NewPage = createClass({
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 },
isPending : true,
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);
@@ -231,10 +243,10 @@ const NewPage = createClass({
onTextChange={this.handleTextChange}
onStyleChange={this.handleStyleChange}
onMetaChange={this.handleMetaChange}
onSnipChange={this.handleSnipChange}
renderer={this.state.brew.renderer}
userThemes={this.props.userThemes}
themeBundle={this.state.themeBundle}
snippetBundle={this.state.themeBundle.snippets}
onCursorPageChange={this.handleEditorCursorPageChange}
onViewPageChange={this.handleEditorViewPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}