0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-24 16:22:44 +00:00
This commit is contained in:
Trevor Buckner
2025-11-17 16:15:44 +00:00
parent 677e8eaf6c
commit e497e42913
8 changed files with 290 additions and 50 deletions

View File

@@ -8,36 +8,36 @@ const autoCompleteEmoji = require('./autocompleteEmoji');
let CodeMirror;
if(typeof window !== 'undefined'){
CodeMirror = require('codemirror');
CodeMirror = require('codemirror5');
//Language Modes
require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown
require('codemirror/mode/css/css.js');
require('codemirror/mode/javascript/javascript.js');
require('codemirror5/mode/gfm/gfm.js'); //Github flavoured markdown
require('codemirror5/mode/css/css.js');
require('codemirror5/mode/javascript/javascript.js');
//Addons
//Code folding
require('codemirror/addon/fold/foldcode.js');
require('codemirror/addon/fold/foldgutter.js');
require('codemirror5/addon/fold/foldcode.js');
require('codemirror5/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');
require('codemirror5/addon/search/search.js');
require('codemirror5/addon/search/searchcursor.js');
require('codemirror5/addon/search/jump-to-line.js');
require('codemirror5/addon/search/match-highlighter.js');
require('codemirror5/addon/search/matchesonscrollbar.js');
require('codemirror5/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');
require('codemirror5/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');
require('codemirror5/addon/fold/xml-fold.js');
require('codemirror5/addon/edit/closetag.js');
//Autocompletion
require('codemirror/addon/hint/show-hint.js');
require('codemirror5/addon/hint/show-hint.js');
const foldPagesCode = require('./fold-pages');
foldPagesCode.registerHomebreweryHelper(CodeMirror);
@@ -462,4 +462,3 @@ const CodeEditor = createClass({
});
module.exports = CodeEditor;

View File

@@ -1,8 +1,8 @@
@import (less) 'codemirror/lib/codemirror.css';
@import (less) 'codemirror/addon/fold/foldgutter.css';
@import (less) 'codemirror/addon/search/matchesonscrollbar.css';
@import (less) 'codemirror/addon/dialog/dialog.css';
@import (less) 'codemirror/addon/hint/show-hint.css';
@import (less) 'codemirror5/lib/codemirror.css';
@import (less) 'codemirror5/addon/fold/foldgutter.css';
@import (less) 'codemirror5/addon/search/matchesonscrollbar.css';
@import (less) 'codemirror5/addon/dialog/dialog.css';
@import (less) 'codemirror5/addon/hint/show-hint.css';
//Icon fonts included so they can appear in emoji autosuggest dropdown
@import (less) './themes/fonts/iconFonts/diceFont.less';
@@ -57,4 +57,4 @@
.emojiPreview {
font-size : 1.5em;
line-height : 1.2em;
}
}

View File

@@ -0,0 +1,191 @@
import './codeEditor.less';
import React, { useEffect, useImperativeHandle, useMemo, useRef } from 'react';
import { Compartment, EditorSelection, EditorState } from '@codemirror/state';
import { EditorView, keymap, highlightActiveLine, lineNumbers, highlightActiveLineGutter } from '@codemirror/view';
import { history, historyKeymap, undo as historyUndo, redo as historyRedo, undoDepth, redoDepth } from '@codemirror/history';
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
import { defaultHighlightStyle } from '@codemirror/language';
import { syntaxHighlighting } from '@codemirror/language';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { languages } from '@codemirror/language-data';
import { javascript } from '@codemirror/lang-javascript';
import { css } from '@codemirror/lang-css';
const baseExtensions = [
lineNumbers(),
highlightActiveLineGutter(),
EditorView.lineWrapping,
history(),
keymap.of([
indentWithTab,
...closeBracketsKeymap,
...searchKeymap,
...historyKeymap,
...defaultKeymap
]),
closeBrackets(),
highlightSelectionMatches(),
highlightActiveLine(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true })
];
const languageExtension = (language)=>{
switch(language){
case 'css':
return css();
case 'javascript':
return javascript({ jsx: true, typescript: true });
case 'gfm':
default:
return markdown({ base: markdownLanguage, codeLanguages: languages });
}
};
const CodeEditorV6 = React.forwardRef(({
value = '',
onChange = ()=>{},
language = 'gfm',
readOnly = false,
style = {},
editorTheme,
onSelectionChange = ()=>{},
onScroll = ()=>{}
}, ref)=>{
const containerRef = useRef(null);
const viewRef = useRef(null);
const languageCompartment = useMemo(()=>new Compartment(), []);
const readOnlyCompartment = useMemo(()=>new Compartment(), []);
useImperativeHandle(ref, ()=>({
focus : ()=>viewRef.current?.focus(),
getView : ()=>viewRef.current,
getCM6View : ()=>viewRef.current,
getCursorPosition : ()=>{
const view = viewRef.current;
if(!view) return { line: 0, ch: 0 };
const { head } = view.state.selection.main;
const lineInfo = view.state.doc.lineAt(head);
return { line: lineInfo.number - 1, ch: head - lineInfo.from };
},
setCursorPosition : ({ line = 0, ch = 0 })=>{
const view = viewRef.current;
if(!view) return;
const docLine = view.state.doc.line(Math.max(1, line + 1));
const pos = Math.min(docLine.from + ch, docLine.to);
view.dispatch({ selection: EditorSelection.cursor(pos), scrollIntoView: true });
},
getTopVisibleLine : ()=>{
const view = viewRef.current;
if(!view) return 0;
const top = view.scrollDOM.scrollTop;
const block = view.lineBlockAtHeight(top);
const lineInfo = view.state.doc.lineAt(block.from);
return lineInfo.number - 1;
},
updateSize : ()=>viewRef.current?.requestMeasure(),
injectText : (text, overwrite=true)=>{
const view = viewRef.current;
if(!view) return;
const { from, to } = view.state.selection.main;
const insertFrom = overwrite ? from : from;
const insertTo = overwrite ? to : from;
view.dispatch({
changes : { from: insertFrom, to: insertTo, insert: text },
selection : EditorSelection.cursor(insertFrom + text.length),
scrollIntoView : true
});
view.focus();
},
undo : ()=>{
const view = viewRef.current;
if(!view) return;
historyUndo(view);
},
redo : ()=>{
const view = viewRef.current;
if(!view) return;
historyRedo(view);
},
historySize : ()=>{
const view = viewRef.current;
if(!view) return { undo: 0, redo: 0 };
return {
undo : undoDepth(view.state),
redo : redoDepth(view.state)
};
},
foldAllCode : ()=>{},
unfoldAllCode : ()=>{}
}), []);
useEffect(()=>{
if(!containerRef.current) return;
const initialExtensions = [
...baseExtensions,
languageCompartment.of(languageExtension(language)),
readOnlyCompartment.of(EditorState.readOnly.of(readOnly)),
EditorView.updateListener.of((update)=>{
if(update.docChanged) onChange(update.state.doc.toString());
if(update.selectionSet) onSelectionChange(update.view);
})
];
const state = EditorState.create({
doc : value,
extensions : initialExtensions
});
const view = new EditorView({
state,
parent : containerRef.current
});
viewRef.current = view;
const handleScroll = ()=>onScroll(view);
view.scrollDOM.addEventListener('scroll', handleScroll);
return ()=>{
view.scrollDOM.removeEventListener('scroll', handleScroll);
view.destroy();
viewRef.current = null;
};
}, []);
useEffect(()=>{
const view = viewRef.current;
if(!view) return;
view.dispatch({
effects : languageCompartment.reconfigure(languageExtension(language))
});
}, [language, languageCompartment]);
useEffect(()=>{
const view = viewRef.current;
if(!view) return;
view.dispatch({
effects : readOnlyCompartment.reconfigure(EditorState.readOnly.of(readOnly))
});
}, [readOnly, readOnlyCompartment]);
useEffect(()=>{
const view = viewRef.current;
if(!view) return;
const currentValue = view.state.doc.toString();
if(value === currentValue) return;
const transaction = view.state.update({
changes : { from: 0, to: currentValue.length, insert: value }
});
view.dispatch(transaction);
}, [value]);
return (
<div className='codeEditor' ref={containerRef} style={style} data-editor='cm6'></div>
);
});
CodeEditorV6.displayName = 'CodeEditorV6';
export default CodeEditorV6;

View File

@@ -7,6 +7,9 @@ const dedent = require('dedent-tabs').default;
import Markdown from '../../../shared/markdown.js';
const CodeEditor = require('client/components/codeEditor/codeEditor.jsx');
import CodeEditorV6 from 'client/components/codeEditor/codeEditorV6.jsx';
const USE_CM6 = global?.config?.enable_CM6 === true;
const SnippetBar = require('./snippetbar/snippetbar.jsx');
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
@@ -62,8 +65,34 @@ const Editor = createClass({
};
},
editor : React.createRef(null),
codeEditor : React.createRef(null),
editor : React.createRef(null),
codeEditor : React.createRef(null),
codeEditorV6 : React.createRef(null),
getActiveEditor : function(){
return USE_CM6 ? this.codeEditorV6.current : this.codeEditor.current;
},
focusActiveEditor : function(){
const editor = this.getActiveEditor();
if(!editor) return;
if(USE_CM6) editor.focus?.();
else editor.codeMirror?.focus();
},
handleCM6SelectionChange : function(view){
if(!USE_CM6 || !view) return;
const { head } = view.state.selection.main;
const lineInfo = view.state.doc.lineAt(head);
this.updateCurrentCursorPage({ line: lineInfo.number - 1 });
},
handleCM6Scroll : function(){
if(!USE_CM6) return;
const topLine = this.codeEditorV6.current?.getTopVisibleLine?.();
if(topLine == null) return;
this.updateCurrentViewPage(topLine);
},
isText : function() {return this.state.view == 'text';},
isStyle : function() {return this.state.view == 'style';},
@@ -76,8 +105,10 @@ const Editor = createClass({
document.getElementById('BrewRenderer').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('scroll', _.throttle(()=>{this.updateCurrentViewPage(this.codeEditor.current.getTopVisibleLine());}, 200));
if(!USE_CM6) {
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));
}
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
if(editorTheme) {
@@ -90,7 +121,7 @@ const Editor = createClass({
componentDidUpdate : function(prevProps, prevState, snapshot) {
this.highlightCustomMarkdown();
if(!USE_CM6) this.highlightCustomMarkdown();
if(prevProps.moveBrew !== this.props.moveBrew)
this.brewJump();
@@ -121,7 +152,8 @@ const Editor = createClass({
},
updateCurrentCursorPage : function(cursor) {
const lines = this.props.brew.text.split('\n').slice(1, cursor.line + 1);
const cursorLine = cursor.line ?? cursor;
const lines = this.props.brew.text.split('\n').slice(1, cursorLine + 1);
const pageRegex = this.props.brew.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
this.props.onCursorPageChange(currentPage);

View File

@@ -7,5 +7,6 @@
"local_environments" : ["docker", "local"],
"publicUrl" : "https://homebrewery.naturalcrit.com",
"hb_images" : null,
"hb_fonts" : null
"hb_fonts" : null,
"enable_CM6" : true
}

View File

@@ -95,7 +95,23 @@
"@sanity/diff-match-patch": "^3.2.0",
"body-parser": "^2.2.0",
"classnames": "^2.5.1",
"codemirror": "^5.65.6",
"@codemirror/autocomplete": "^6.18.2",
"@codemirror/commands": "^6.7.2",
"@codemirror/fold": "^6.7.1",
"@codemirror/history": "^6.3.0",
"@codemirror/lang-css": "^6.3.0",
"@codemirror/lang-html": "^6.4.7",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-markdown": "^6.2.3",
"@codemirror/language": "^6.10.2",
"@codemirror/lint": "^6.8.0",
"@codemirror/search": "^6.5.6",
"@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.2.1",
"@lezer/highlight": "^1.2.0",
"codemirror": "^6.0.1",
"codemirror5": "npm:codemirror@^5.65.19",
"cookie-parser": "^1.4.7",
"core-js": "^3.46.0",
"cors": "^2.8.5",

View File

@@ -8,24 +8,24 @@
"create-react-class",
"lodash",
"classnames",
"codemirror",
"codemirror/mode/gfm/gfm.js",
"codemirror/mode/css/css.js",
"codemirror/mode/javascript/javascript.js",
"codemirror/addon/fold/foldcode.js",
"codemirror/addon/fold/foldgutter.js",
"codemirror/addon/fold/xml-fold.js",
"codemirror/addon/scroll/scrollpastend.js",
"codemirror/addon/search/search.js",
"codemirror/addon/search/searchcursor.js",
"codemirror/addon/search/jump-to-line.js",
"codemirror/addon/search/match-highlighter.js",
"codemirror/addon/search/matchesonscrollbar.js",
"codemirror/addon/dialog/dialog.js",
"codemirror/addon/edit/closetag.js",
"codemirror/addon/edit/trailingspace.js",
"codemirror/addon/selection/active-line.js",
"codemirror/addon/hint/show-hint.js",
"codemirror5",
"codemirror5/mode/gfm/gfm.js",
"codemirror5/mode/css/css.js",
"codemirror5/mode/javascript/javascript.js",
"codemirror5/addon/fold/foldcode.js",
"codemirror5/addon/fold/foldgutter.js",
"codemirror5/addon/fold/xml-fold.js",
"codemirror5/addon/scroll/scrollpastend.js",
"codemirror5/addon/search/search.js",
"codemirror5/addon/search/searchcursor.js",
"codemirror5/addon/search/jump-to-line.js",
"codemirror5/addon/search/match-highlighter.js",
"codemirror5/addon/search/matchesonscrollbar.js",
"codemirror5/addon/dialog/dialog.js",
"codemirror5/addon/edit/closetag.js",
"codemirror5/addon/edit/trailingspace.js",
"codemirror5/addon/selection/active-line.js",
"codemirror5/addon/hint/show-hint.js",
"moment",
"superagent",
"@sanity/diff-match-patch",

View File

@@ -554,7 +554,8 @@ const renderPage = async (req, res)=>{
publicUrl : config.get('publicUrl') ?? '',
baseUrl : `${req.protocol}://${req.get('host')}`,
environment : nodeEnv,
deployment : config.get('heroku_app_name') ?? ''
deployment : config.get('heroku_app_name') ?? '',
enable_CM6 : config.get('enable_CM6') ?? false
};
const props = {
version : version,