0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-05-07 18:48:39 +00:00

Merge branch 'update-Codemirror' of https://github.com/naturalcrit/homebrewery into add-cm-features

This commit is contained in:
Víctor Losada Hernández
2026-04-02 17:47:13 +02:00
20 changed files with 3124 additions and 1671 deletions
@@ -1,3 +1,5 @@
import { autocompletion } from '@codemirror/autocomplete';
import diceFont from '@themes/fonts/iconFonts/diceFont.js';
import elderberryInn from '@themes/fonts/iconFonts/elderberryInn.js';
import fontAwesome from '@themes/fonts/iconFonts/fontAwesome.js';
@@ -10,75 +12,64 @@ const emojis = {
...gameIcons
};
const showAutocompleteEmoji = function(CodeMirror, editor) {
CodeMirror.commands.autocomplete = function(editor) {
editor.showHint({
completeSingle : false,
hint : function(editor) {
const cursor = editor.getCursor();
const line = cursor.line;
const lineContent = editor.getLine(line);
const start = lineContent.lastIndexOf(':', cursor.ch - 1) + 1;
const end = cursor.ch;
const currentWord = lineContent.slice(start, end);
const emojiCompletionList = (context)=>{
const word = context.matchBefore(/:[^\s:]*/);
if(!word) return null;
const line = context.state.doc.lineAt(context.pos);
const textToCursor = line.text.slice(0, context.pos - line.from);
const list = Object.keys(emojis).filter(function(emoji) {
return emoji.toLowerCase().indexOf(currentWord.toLowerCase()) >= 0;
}).sort((a, b)=>{
const lowerA = a.replace(/\d+/g, function(match) { // Temporarily convert any numbers in emoji string
return match.padStart(4, '0'); // to 4-digits, left-padded with 0's, to aid in
}).toLowerCase(); // sorting numbers, i.e., "d6, d10, d20", not "d10, d20, d6"
const lowerB = b.replace(/\d+/g, function(match) { // Also make lowercase for case-insensitive alpha sorting
return match.padStart(4, '0');
}).toLowerCase();
if(textToCursor.includes('{')) {
const curlyToCursor = textToCursor.slice(textToCursor.indexOf('{'));
const curlySpanRegex = /{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1$/g;
if(curlySpanRegex.test(curlyToCursor)) return null;
}
if(lowerA < lowerB)
return -1;
return 1;
}).map(function(emoji) {
return {
text : `${emoji}:`, // Text to output to editor when option is selected
render : function(element, self, data) { // How to display the option in the dropdown
const div = document.createElement('div');
div.innerHTML = `<i class="emojiPreview ${emojis[emoji]}"></i> ${emoji}`;
element.appendChild(div);
}
};
});
const currentWord = word.text.slice(1); // remove ':'
return {
list : list.length ? list : [],
from : CodeMirror.Pos(line, start),
to : CodeMirror.Pos(line, end)
};
}
});
const options = Object.keys(emojis)
.filter((e)=>e.toLowerCase().includes(currentWord.toLowerCase()))
.sort((a, b)=>{
const normalize = (str)=>str.replace(/\d+/g, (m)=>m.padStart(4, '0')).toLowerCase();
return normalize(a) < normalize(b) ? -1 : 1;
})
.map((e)=>({
label : e,
apply : `${e}:`,
type : 'text',
info : ()=>{
const div = document.createElement('div');
div.innerHTML = `<i class="emojiPreview ${emojis[e]}"></i> ${e}`;
return div;
}
}));
//Label is the text in the list, comes with an icon that just
//renders example text "abc", hid that with css because i didn't see other choice
//Apply is the text that is set when the choice is selected
//Info is the tooltip
return {
from : word.from + 1,
options,
};
editor.on('inputRead', function(instance, change) {
const cursor = editor.getCursor();
const line = editor.getLine(cursor.line);
// Get the text from the start of the line to the cursor
const textToCursor = line.slice(0, cursor.ch);
// Do not autosuggest emojis in curly span/div/injector properties
if(line.includes('{')) {
const curlyToCursor = textToCursor.slice(textToCursor.indexOf(`{`));
const curlySpanRegex = /{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1$/g;
if(curlySpanRegex.test(curlyToCursor))
return;
}
// Check if the text ends with ':xyz'
if(/:[^\s:]+$/.test(textToCursor)) {
CodeMirror.commands.autocomplete(editor);
}
});
};
export default {
showAutocompleteEmoji
};
export const autocompleteEmoji = autocompletion({
override : [emojiCompletionList],
activateOnTyping : true,
addToOptions : [
{
render(completion) {
const e = completion.label;
const icon = document.createElement('i');
icon.className = `emojiPreview ${emojis[e]}`;
const fragment = document.createDocumentFragment();
fragment.appendChild(icon);
return fragment;
}
}
]
});
+333 -478
View File
@@ -1,490 +1,345 @@
/* eslint-disable max-lines */
/* eslint max-lines: ["error", { "max": 400 }] */
import './codeEditor.less';
import React from 'react';
import createReactClass from 'create-react-class';
import _ from 'lodash';
import closeTag from './close-tag';
import autoCompleteEmoji from './autocompleteEmoji';
let CodeMirror;
import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
const CodeEditor = createReactClass({
displayName : 'CodeEditor',
getDefaultProps : function() {
return {
language : '',
tab : 'brewText',
value : '',
wrap : true,
onChange : ()=>{},
enableFolding : true,
editorTheme : 'default'
};
},
import {
EditorView,
keymap,
lineNumbers,
highlightActiveLineGutter,
highlightActiveLine,
scrollPastEnd,
Decoration,
ViewPlugin,
WidgetType,
drawSelection,
dropCursor,
} from '@codemirror/view';
import { EditorState, Compartment } from '@codemirror/state';
import { foldAll as foldAllCmd, unfoldAll as unfoldAllCmd, foldGutter, foldKeymap, syntaxHighlighting } from '@codemirror/language';
import { defaultKeymap, history, undo, redo, undoDepth, redoDepth } from '@codemirror/commands';
import { languages } from '@codemirror/language-data';
import { css, cssLanguage } from '@codemirror/lang-css';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { html } from '@codemirror/lang-html';
import { autocompleteEmoji } from './autocompleteEmoji.js';
import { searchKeymap, search } from '@codemirror/search';
import { closeBrackets } from '@codemirror/autocomplete';
getInitialState : function() {
return {
docs : {}
};
},
const customClose = closeBrackets({ brackets: ['()', '[]', '{{}}'] });
editor : React.createRef(null),
import * as themesImport from '@uiw/codemirror-themes-all';
import defaultCM5Theme from '@themes/codeMirror/default.js';
import darkbrewery from '@themes/codeMirror/darkbrewery.js';
async componentDidMount() {
CodeMirror = (await import('codemirror')).default;
this.CodeMirror = CodeMirror;
const themes = { default: defaultCM5Theme, darkbrewery, ...themesImport };
const themeCompartment = new Compartment();
const highlightCompartment = new Compartment();
await import('codemirror/mode/gfm/gfm.js');
await import('codemirror/mode/css/css.js');
await import('codemirror/mode/javascript/javascript.js');
import customKeymap from './customKeyMap.js';
import pageFoldExtension from './customFolding.js';
import { customHighlightStyle, tokenizeCustomMarkdown, tokenizeCustomCSS } from './customHighlight.js';
import { legacyCustomHighlightStyle, legacyTokenizeCustomMarkdown } from './legacyCustomHighlight.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';
const createHighlightPlugin = (renderer, tab)=>{
//this function takes the custom tokens created in the tokenize function in customhighlight files
//takes the tokens defined by that function and assigns classes to them
//it also creates page number and snippet number widgets
let tokenize;
// 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();
const newDoc = CodeMirror?.Doc(this.props.value, this.props.language);
this.codeMirror?.swapDoc(newDoc);
},
componentDidUpdate : function(prevProps) {
if(prevProps.view !== this.props.view){ //view changed; swap documents
let newDoc;
if(!this.state.docs[this.props.view]) {
newDoc = CodeMirror?.Doc(this.props.value, this.props.language);
} else {
newDoc = this.state.docs[this.props.view];
}
const oldDoc = { [prevProps.view]: this.codeMirror?.swapDoc(newDoc) };
this.setState((prevState)=>({
docs : _.merge({}, prevState.docs, oldDoc)
}));
this.props.rerenderParent();
} else if(this.codeMirror?.getValue() != this.props.value) { //update editor contents if brew.text is changed from outside
this.codeMirror?.setValue(this.props.value);
}
if(this.props.enableFolding) {
this.codeMirror?.setOption('foldOptions', this.foldOptions(this.codeMirror));
} else {
this.codeMirror?.setOption('foldOptions', false);
}
if(prevProps.editorTheme !== this.props.editorTheme){
this.codeMirror?.setOption('theme', this.props.editorTheme);
}
},
buildEditor : function() {
this.codeMirror = CodeMirror(this.editor.current, {
lineNumbers : true,
lineWrapping : this.props.wrap,
indentWithTabs : false,
tabSize : 2,
smartIndent : false,
historyEventDelay : 250,
scrollPastEnd : true,
extraKeys : {
'Tab' : this.indent,
'Shift-Tab' : this.dedent,
'Ctrl-B' : this.makeBold,
'Cmd-B' : this.makeBold,
'Shift-Ctrl-=' : this.makeSuper,
'Shift-Cmd-=' : this.makeSuper,
'Ctrl-=' : this.makeSub,
'Cmd-=' : this.makeSub,
'Ctrl-I' : this.makeItalic,
'Cmd-I' : this.makeItalic,
'Ctrl-U' : this.makeUnderline,
'Cmd-U' : this.makeUnderline,
'Ctrl-.' : this.makeNbsp,
'Cmd-.' : this.makeNbsp,
'Shift-Ctrl-.' : this.makeSpace,
'Shift-Cmd-.' : this.makeSpace,
'Shift-Ctrl-,' : this.removeSpace,
'Shift-Cmd-,' : this.removeSpace,
'Ctrl-M' : this.makeSpan,
'Cmd-M' : this.makeSpan,
'Shift-Ctrl-M' : this.makeDiv,
'Shift-Cmd-M' : this.makeDiv,
'Ctrl-/' : this.makeComment,
'Cmd-/' : this.makeComment,
'Ctrl-K' : this.makeLink,
'Cmd-K' : this.makeLink,
'Ctrl-L' : ()=>this.makeList('UL'),
'Cmd-L' : ()=>this.makeList('UL'),
'Shift-Ctrl-L' : ()=>this.makeList('OL'),
'Shift-Cmd-L' : ()=>this.makeList('OL'),
'Shift-Ctrl-1' : ()=>this.makeHeader(1),
'Shift-Ctrl-2' : ()=>this.makeHeader(2),
'Shift-Ctrl-3' : ()=>this.makeHeader(3),
'Shift-Ctrl-4' : ()=>this.makeHeader(4),
'Shift-Ctrl-5' : ()=>this.makeHeader(5),
'Shift-Ctrl-6' : ()=>this.makeHeader(6),
'Shift-Cmd-1' : ()=>this.makeHeader(1),
'Shift-Cmd-2' : ()=>this.makeHeader(2),
'Shift-Cmd-3' : ()=>this.makeHeader(3),
'Shift-Cmd-4' : ()=>this.makeHeader(4),
'Shift-Cmd-5' : ()=>this.makeHeader(5),
'Shift-Cmd-6' : ()=>this.makeHeader(6),
'Shift-Ctrl-Enter' : this.newColumn,
'Shift-Cmd-Enter' : this.newColumn,
'Ctrl-Enter' : this.newPage,
'Cmd-Enter' : this.newPage,
'Ctrl-F' : 'findPersistent',
'Cmd-F' : 'findPersistent',
'Shift-Enter' : 'findPersistentPrevious',
'Ctrl-[' : this.foldAllCode,
'Cmd-[' : this.foldAllCode,
'Ctrl-]' : this.unfoldAllCode,
'Cmd-]' : this.unfoldAllCode
},
foldGutter : true,
foldOptions : this.foldOptions(this.codeMirror),
gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
autoCloseTags : true,
styleActiveLine : true,
showTrailingSpace : false,
theme : this.props.editorTheme
// specialChars : / /,
// specialCharPlaceholder : function(char) {
// const el = document.createElement('span');
// el.className = 'cm-space';
// el.innerHTML = ' ';
// return el;
// }
});
// Add custom behaviors (auto-close curlies and auto-complete emojis)
closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror);
autoCompleteEmoji.showAutocompleteEmoji(CodeMirror, this.codeMirror);
// 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.updateSize();
},
// Use for GFM tabs that use common hot-keys
isGFM : function() {
if((this.isGFM()) || (this.props.tab === 'brewSnippets')) return true;
return false;
},
isBrewText : function() {
if(this.isGFM()) return true;
return false;
},
isBrewSnippets : function() {
if(this.props.tab === 'brewSnippets') return true;
return false;
},
indent : function () {
const cm = this.codeMirror;
if(cm.somethingSelected()) {
cm.execCommand('indentMore');
} else {
cm.execCommand('insertSoftTab');
}
},
dedent : function () {
this.codeMirror?.execCommand('indentLess');
},
makeHeader : function (number) {
if(!this.isGFM()) return;
const selection = this.codeMirror?.getSelection();
const header = Array(number).fill('#').join('');
this.codeMirror?.replaceSelection(`${header} ${selection}`, 'around');
const cursor = this.codeMirror?.getCursor();
this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch + selection.length + number + 1 });
},
makeBold : function() {
if(!this.isGFM()) return;
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 2) === '**' && selection.slice(-2) === '**';
this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `**${selection}**`, 'around');
if(selection.length === 0){
const cursor = this.codeMirror?.getCursor();
this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
}
},
makeItalic : function() {
if(!this.isGFM()) return;
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '*' && selection.slice(-1) === '*';
this.codeMirror?.replaceSelection(t ? selection.slice(1, -1) : `*${selection}*`, 'around');
if(selection.length === 0){
const cursor = this.codeMirror?.getCursor();
this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
}
},
makeSuper : function() {
if(!this.isGFM()) return;
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '^' && selection.slice(-1) === '^';
this.codeMirror?.replaceSelection(t ? selection.slice(1, -1) : `^${selection}^`, 'around');
if(selection.length === 0){
const cursor = this.codeMirror?.getCursor();
this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
}
},
makeSub : function() {
if(!this.isGFM()) return;
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '^^' && selection.slice(-2) === '^^';
this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `^^${selection}^^`, 'around');
if(selection.length === 0){
const cursor = this.codeMirror?.getCursor();
this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
}
},
makeNbsp : function() {
if(!this.isGFM()) return;
this.codeMirror?.replaceSelection('&nbsp;', 'end');
},
makeSpace : function() {
if(!this.isGFM()) return;
const selection = this.codeMirror?.getSelection();
const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}';
if(t){
const percent = parseInt(selection.slice(8, -4)) + 10;
this.codeMirror?.replaceSelection(percent < 90 ? `{{width:${percent}% }}` : '{{width:100% }}', 'around');
} else {
this.codeMirror?.replaceSelection(`{{width:10% }}`, 'around');
}
},
removeSpace : function() {
if(!this.isGFM()) return;
const selection = this.codeMirror?.getSelection();
const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}';
if(t){
const percent = parseInt(selection.slice(8, -4)) - 10;
this.codeMirror?.replaceSelection(percent > 10 ? `{{width:${percent}% }}` : '', 'around');
}
},
newColumn : function() {
if(!this.isGFM()) return;
this.codeMirror?.replaceSelection('\n\\column\n\n', 'end');
},
newPage : function() {
if(!this.isGFM()) return;
this.codeMirror?.replaceSelection('\n\\page\n\n', 'end');
},
injectText : function(injectText, overwrite=true) {
const cm = this.codeMirror;
if(!overwrite) {
cm.setCursor(cm.getCursor('from'));
}
cm.replaceSelection(injectText, 'end');
cm.focus();
},
makeUnderline : function() {
if(!this.isGFM()) return;
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');
if(selection.length === 0){
const cursor = this.codeMirror?.getCursor();
this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 4 });
}
},
makeSpan : function() {
if(!this.isGFM()) return;
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}';
this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `{{ ${selection}}}`, 'around');
if(selection.length === 0){
const cursor = this.codeMirror?.getCursor();
this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
}
},
makeDiv : function() {
if(!this.isGFM()) return;
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');
if(selection.length === 0){
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
}
},
makeComment : function() {
let regex;
let cursorPos;
let newComment;
const selection = this.codeMirror?.getSelection();
if(this.isGFM()){
regex = /^\s*(<!--\s?)(.*?)(\s?-->)\s*$/gs;
cursorPos = 4;
newComment = `<!-- ${selection} -->`;
} else {
regex = /^\s*(\/\*\s?)(.*?)(\s?\*\/)\s*$/gs;
cursorPos = 3;
newComment = `/* ${selection} */`;
}
this.codeMirror?.replaceSelection(regex.test(selection) == true ? selection.replace(regex, '$2') : newComment, 'around');
if(selection.length === 0){
const cursor = this.codeMirror?.getCursor();
this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - cursorPos });
};
},
makeLink : function() {
if(!this.isGFM()) return;
const isLink = /^\[(.*)\]\((.*)\)$/;
const selection = this.codeMirror?.getSelection().trim();
let match;
if(match = isLink.exec(selection)){
const altText = match[1];
const url = match[2];
this.codeMirror?.replaceSelection(`${altText} ${url}`);
const cursor = this.codeMirror?.getCursor();
this.codeMirror?.setSelection({ line: cursor.line, ch: cursor.ch - url.length }, { line: cursor.line, ch: cursor.ch });
} else {
this.codeMirror?.replaceSelection(`[${selection || 'alt text'}](url)`);
const cursor = this.codeMirror?.getCursor();
this.codeMirror?.setSelection({ line: cursor.line, ch: cursor.ch - 4 }, { line: cursor.line, ch: cursor.ch - 1 });
}
},
makeList : function(listType) {
if(!this.isGFM()) return;
const selectionStart = this.codeMirror.getCursor('from'), selectionEnd = this.codeMirror.getCursor('to');
this.codeMirror?.setSelection(
{ line: selectionStart.line, ch: 0 },
{ line: selectionEnd.line, ch: this.codeMirror?.getLine(selectionEnd.line).length }
);
const newSelection = this.codeMirror?.getSelection();
const regex = /^\d+\.\s|^-\s/gm;
if(newSelection.match(regex) != null){ // if selection IS A LIST
this.codeMirror?.replaceSelection(newSelection.replace(regex, ''), 'around');
} else { // if selection IS NOT A LIST
listType == 'UL' ? this.codeMirror?.replaceSelection(newSelection.replace(/^/gm, `- `), 'around') :
this.codeMirror?.replaceSelection(newSelection.replace(/^/gm, (()=>{
let n = 1;
return ()=>{
return `${n++}. `;
};
})()), 'around');
}
},
foldAllCode : function() {
this.codeMirror?.execCommand('foldAll');
},
unfoldAllCode : function() {
this.codeMirror?.execCommand('unfoldAll');
},
//=-- Externally used -==//
setCursorPosition : function(line, char){
setTimeout(()=>{
this.codeMirror?.focus();
this.codeMirror?.doc.setCursor(line, char);
}, 10);
},
getCursorPosition : function(){
return this.codeMirror?.getCursor();
},
getTopVisibleLine : function(){
const rect = this.codeMirror?.getWrapperElement().getBoundingClientRect();
const topVisibleLine = this.codeMirror?.lineAtHeight(rect.top, 'window');
return topVisibleLine;
},
updateSize : function(){
this.codeMirror?.refresh();
},
redo : function(){
return this.codeMirror?.redo();
},
undo : function(){
return this.codeMirror?.undo();
},
historySize : function(){
return this.codeMirror?.doc.historySize();
},
foldOptions : function(cm){
return {
scanUp : true,
rangeFinder : this.props.language === 'css' ? CodeMirror.fold.homebrewerycss : CodeMirror.fold.homebrewery,
widget : (from, to)=>{
let text = '';
let currentLine = from.line;
let maxLength = 50;
let foldPreviewText = '';
while (currentLine <= to.line && text.length <= maxLength) {
const currentText = this.codeMirror?.getLine(currentLine);
currentLine++;
if(currentText[0] == '#'){
foldPreviewText = currentText;
break;
}
if(!foldPreviewText && currentText != '\n') {
foldPreviewText = currentText;
}
}
text = foldPreviewText || `Lines ${from.line+1}-${to.line+1}`;
text = text.replace('{', '').trim();
// Truncate data URLs at `data:`
const startOfData = text.indexOf('data:');
if(startOfData > 0)
maxLength = Math.min(startOfData + 5, maxLength);
if(text.length > maxLength)
text = `${text.slice(0, maxLength)}...`;
return `\u21A4 ${text} \u21A6`;
}
};
},
//----------------------//
render : function(){
return <>
<link href={`../homebrew/cm-themes/${this.props.editorTheme}.css`} type='text/css' rel='stylesheet' />
<div className='codeEditor' ref={this.editor} style={this.props.style}/>
</>;
if(tab === 'brewStyles') {
tokenize = tokenizeCustomCSS;
} else {
tokenize = renderer === 'V3' ? tokenizeCustomMarkdown : legacyTokenizeCustomMarkdown;
}
});
return ViewPlugin.fromClass(
class {
constructor(view) {
this.decorations = this.buildDecorations(view);
}
update(update) {
if(update.docChanged) {
this.decorations = this.buildDecorations(update.view);
}
}
buildDecorations(view) {
const decos = [];
const tokens = tokenize(view.state.doc.toString());
let pageCount = 1;
let snippetCount = 0;
tokens.forEach((tok)=>{
const line = view.state.doc.line(tok.line + 1);
if(tok.from != null && tok.to != null && tok.from < tok.to) {
decos.push(Decoration.mark({ class: `cm-${tok.type}` }).range(line.from + tok.from, line.from + tok.to));
} else {
decos.push(Decoration.line({ class: `cm-${tok.type}` }).range(line.from));
if(tok.type === 'pageLine' && tab === 'brewText') {
pageCount++;
line.from === 0 && pageCount--;
decos.push(Decoration.line({ attributes: { 'data-page-number': pageCount } }).range(line.from));
}
if(tok.type === 'snippetLine' && tab === 'brewSnippets') {
snippetCount++;
decos.push(Decoration.line({ attributes: { 'data-page-number': pageCount } }).range(line.from));
}
}
});
decos.sort((a, b)=>a.from - b.from || a.to - b.to);
return Decoration.set(decos);
}
},
{ decorations: (v)=>v.decorations }
);
};
const CodeEditor = forwardRef(
(
{
value = '',
onChange = ()=>{},
onCursorChange = ()=>{},
onViewChange = ()=>{},
language = '',
tab = 'brewText',
editorTheme = 'default',
view,
style,
renderer,
...props
},
ref,
)=>{
const editorRef = useRef(null);
const viewRef = useRef(null);
const docsRef = useRef({});
const prevTabRef = useRef(tab);
const createExtensions = ({ onChange, language, editorTheme })=>{
const setEventListeners = EditorView.updateListener.of((update)=>{
if(update.docChanged) {
onChange(update.state.doc.toString());
}
if(update.selectionSet) {
const pos = update.state.selection.main.head;
const line = update.state.doc.lineAt(pos).number;
onCursorChange(line);
}
if(update.viewportChanged) {
const { from } = update.view.viewport;
const line = update.state.doc.lineAt(from).number;
onViewChange(line);
}
});
const highlightExtension = renderer === 'V3'
? syntaxHighlighting(customHighlightStyle)
: syntaxHighlighting(legacyCustomHighlightStyle);
const customHighlightPlugin = createHighlightPlugin(renderer, tab);
const languageExtension = language === 'css' ? css() : [markdown({ base: markdownLanguage, codeLanguages: languages }), html({ autoCloseTags: true })];
const themeExtension = Array.isArray(themes[editorTheme]) ? themes[editorTheme] : [];
return [
history(), //allows for undo and redo
setEventListeners,
EditorView.lineWrapping,
scrollPastEnd(),
languageExtension,
lineNumbers(),
pageFoldExtension,
foldGutter({
openText : '▾',
closedText : '▸'
}),
highlightActiveLine(),
highlightActiveLineGutter(),
highlightCompartment.of([customHighlightPlugin, highlightExtension]),
themeCompartment.of(themeExtension),
...(tab !== 'brewStyles' ? [autocompleteEmoji] : []),
search(),
keymap.of([...defaultKeymap, foldKeymap, ...searchKeymap]),
customKeymap,
drawSelection(),
EditorState.allowMultipleSelections.of(true),
customClose,
dropCursor(),
];
};
useEffect(()=>{
if(!editorRef.current) return;
const state = EditorState.create({
doc : value,
extensions : createExtensions({ onChange, language, editorTheme }),
});
viewRef.current = new EditorView({
state,
parent : editorRef.current,
});
docsRef.current[tab] = state;
return ()=>viewRef.current?.destroy();
}, []);
useEffect(()=>{
const view = viewRef.current;
if(!view) return;
const prevTab = prevTabRef.current;
if(prevTab !== tab) {
docsRef.current[prevTab] = view.state;
let nextState = docsRef.current[tab];
if(!nextState) {
nextState = EditorState.create({
doc : value,
extensions : createExtensions({ onChange, language, editorTheme }),
});
}
view.setState(nextState);
prevTabRef.current = tab;
}
}, [tab]);
useEffect(()=>{
const view = viewRef.current;
if(!view) return;
const current = view.state.doc.toString();
if(value !== current) {
view.dispatch({
changes : { from: 0, to: current.length, insert: value },
});
}
}, [value]);
useEffect(()=>{
//rebuild theme extension on theme change
const view = viewRef.current;
if(!view) return;
const themeExtension = Array.isArray(themes[editorTheme]) ? themes[editorTheme] : [];
view.dispatch({
effects : themeCompartment.reconfigure(themeExtension),
});
}, [editorTheme]);
useEffect(()=>{
const view = viewRef.current;
if(!view) return;
const highlightExtension =renderer === 'V3'
? syntaxHighlighting(customHighlightStyle)
: syntaxHighlighting(legacyCustomHighlightStyle);
const customHighlightPlugin = createHighlightPlugin(renderer, tab);
view.dispatch({
effects : highlightCompartment.reconfigure([customHighlightPlugin, highlightExtension]),
});
}, [renderer, tab]);
useImperativeHandle(ref, ()=>({
getValue : ()=>viewRef.current.state.doc.toString(),
setValue : (text)=>{
const view = viewRef.current;
view.dispatch({
changes : { from: 0, to: view.state.doc.length, insert: text },
});
},
injectText : (text)=>{
const view = viewRef.current;
const changes = view.state.selection.ranges.map((range)=>({
from : range.from,
to : range.to,
insert : text
}));
const newRanges = view.state.selection.ranges.map((range)=>({
anchor : range.from + text.length
}));
view.dispatch({
changes,
selection : { ranges: newRanges }
});
view.focus();
},
getCursorPosition : ()=>viewRef.current.state.selection.main.head,
getScrollTop : ()=>viewRef.current.scrollDOM.scrollTop,
scrollToY : (y)=>{
viewRef.current.scrollDOM.scrollTo({ top: y });
},
getLineTop : (lineNumber)=>{
const view = viewRef.current;
if(!view) return 0;
const line = view.state.doc.line(lineNumber);
return view.coordsAtPos(line.from)?.top ?? 0;
},
setCursorToLine : (lineNumber)=>{
const view = viewRef.current;
const line = view.state.doc.line(lineNumber);
view.dispatch({
selection : { anchor: line.from }
});
view.focus();
},
undo : ()=>undo(viewRef.current),
redo : ()=>redo(viewRef.current),
historySize : ()=>{
const view = viewRef.current;
if(!view) return { done: 0, undone: 0 };
return {
done : undoDepth(view.state),
undone : redoDepth(view.state),
};
},
foldAll : ()=>{
const view = viewRef.current;
if(!view) return;
view.dispatch(foldAllCmd(view));
},
unfoldAll : ()=>{
const view = viewRef.current;
if(!view) return;
view.dispatch(unfoldAllCmd(view));
},
focus : ()=>viewRef.current.focus(),
}));
return <div className={`codeEditor ${tab}`} ref={editorRef} style={style} />;
},
);
export default CodeEditor;
+44 -32
View File
@@ -1,60 +1,72 @@
@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';
//Icon fonts included so they can appear in emoji autosuggest dropdown
@import (less) '@themes/fonts/iconFonts/diceFont.less';
@import (less) '@themes/fonts/iconFonts/elderberryInn.less';
@import (less) '@themes/fonts/iconFonts/gameIcons.less';
@import (less) '@themes/fonts/iconFonts/fontAwesome.less';
// Icon fonts for emoji/autocomplete
@import (less) "@themes/fonts/iconFonts/diceFont.less";
@import (less) "@themes/fonts/iconFonts/elderberryInn.less";
@import (less) "@themes/fonts/iconFonts/gameIcons.less";
@import (less) "@themes/fonts/iconFonts/fontAwesome.less";
@keyframes sourceMoveAnimation {
50% { color : white;background-color : red;}
100% { color : unset;background-color : unset;}
50% {
color: white;
background-color: red;
}
100% {
color: unset;
background-color: unset;
}
}
.codeEditor {
@media screen and (pointer : coarse) {
font-size : 16px;
:where(.codeEditor) {
font-family: monospace;
height: 100%;
width:100%;
.cm-content {
tab-size:2 !important;
}
.CodeMirror-foldmarker {
@media screen and (pointer: coarse) {
font-size: 16px;
}
.cm-gutterElement span {
font-family : inherit;
font-weight : 600;
color : grey;
text-shadow : none;
}
.CodeMirror-foldgutter {
.cm-foldGutter {
cursor : pointer;
border-left : 1px solid #EEEEEE;
transition : background 0.1s;
&:hover { background : #DDDDDD; }
}
.sourceMoveFlash .CodeMirror-line {
animation-name : sourceMoveAnimation;
animation-duration : 0.4s;
/* Flash animation for source moves */
.sourceMoveFlash .cm-line {
animation-name: sourceMoveAnimation;
animation-duration: 0.4s;
}
.CodeMirror-search-field {
width:25em !important;
outline:1px inset #00000055 !important;
/* Search input */
.cm-searchField {
width: 25em !important;
outline: 1px inset #00000055 !important;
}
/* Tab character visualization (optional) */
//.cm-tab {
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
// background: url(...) no-repeat right;
//}
//.cm-trailingspace {
// .cm-space {
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
// }
/* Trailing space visualization (optional) */
//.cm-trailingSpace .cm-space {
// background: url(...) no-repeat right;
//}
}
/* Emoji preview styling */
.emojiPreview {
font-size : 1.5em;
line-height : 1.2em;
}
font-size: 1.5em;
line-height: 1.2em;
}
@@ -0,0 +1,46 @@
import { foldService, codeFolding } from '@codemirror/language';
const pageFoldExtension = [
foldService.of((state, lineStart)=>{ //tells where to fold
const doc = state.doc;
const matcher = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
const startLine = doc.lineAt(lineStart);
const prevLineText = startLine.number > 1 ? doc.line(startLine.number - 1).text : '';
if(!matcher.test(prevLineText)) return null;
let endLine = startLine.number;
while (endLine < doc.lines && !matcher.test(doc.line(endLine + 1).text)) {
endLine++;
}
if(endLine === startLine.number) return null;
return { from: startLine.from, to: doc.line(endLine).to };
}),
codeFolding({
preparePlaceholder : (state, range)=>{
const doc = state.doc;
const start = doc.lineAt(range.from).number;
const end = doc.lineAt(range.to).number;
if(doc.line(start).text.trim()) return ` ↤ Lines ${start}-${end}`;
const preview = Array.from({ length: end - start }, (_, i)=>doc.line(start + 1 + i).text.trim()
).find(Boolean) || `Lines ${start}-${end}`;
return `${preview.replace('{', '').slice(0, 50).trim()}${preview.length > 50 ? '...' : ''}`;
},
placeholderDOM(view, onclick, prepared) {
const span = document.createElement('span');
span.className = 'cm-fold-placeholder';
span.textContent = prepared;
span.onclick = onclick;
span.style.color = '#989898';
return span;
},
}),
];
export default pageFoldExtension;
@@ -0,0 +1,293 @@
import { HighlightStyle } from '@codemirror/language';
import { tags } from '@lezer/highlight';
// Making the tokens
const customTags = {
pageLine : 'pageLine', // .cm-pageLine
snippetLine : 'snippetLine', // .cm-snippetLine
columnSplit : 'columnSplit', // .cm-columnSplit
block : 'block', // .cm-block
inlineBlock : 'inline-block', // .cm-inline-block
injection : 'injection', // .cm-injection
emoji : 'emoji', // .cm-emoji
superscript : 'superscript', // .cm-superscript
subscript : 'subscript', // .cm-subscript
definitionList : 'definitionList', // .cm-definitionList
definitionTerm : 'definitionTerm', // .cm-definitionTerm
definitionDesc : 'definitionDesc', // .cm-definitionDesc
definitionColon : 'definitionColon', // .cm-definitionColon
//CSS
variable : 'variable',
};
export function tokenizeCustomMarkdown(text) {
const tokens = [];
const lines = text.split('\n');
lines.forEach((lineText, lineNumber)=>{
// --- Page / snippet lines ---
if(/^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m.test(lineText)) tokens.push({ line: lineNumber, type: customTags.pageLine });
if(/^\\snippet\ .*$/.test(lineText)) tokens.push({ line: lineNumber, type: customTags.snippetLine });
if(/^\\column(?:break)?$/.test(lineText)) tokens.push({ line: lineNumber, type: customTags.columnSplit });
// --- Emoji ---
if(/:.\w+?:/.test(lineText)) {
const emojiRegex = /(:\w+?:)/g;
let match;
while ((match = emojiRegex.exec(lineText)) !== null) {
tokens.push({
line : lineNumber,
type : customTags.emoji,
from : match.index,
to : match.index + match[0].length,
});
}
}
// --- Superscript / Subscript ---
if(/\^/.test(lineText)) {
let startIndex = lineText.indexOf('^');
const superRegex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/gy;
const subRegex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/gy;
while (startIndex >= 0) {
superRegex.lastIndex = subRegex.lastIndex = startIndex;
let match = subRegex.exec(lineText);
let type = customTags.subscript;
if(!match) {
match = superRegex.exec(lineText);
type = customTags.superscript;
}
if(match) {
tokens.push({
line : lineNumber,
type,
from : match.index,
to : match.index + match[0].length,
});
}
startIndex = lineText.indexOf(
'^',
Math.max(startIndex + 1, superRegex.lastIndex || 0, subRegex.lastIndex || 0),
);
}
}
const singleLineRegex = /^([^:\n]*\S)(\s*)(::)([^\n]*)$/dmy;
const match = singleLineRegex.exec(lineText);
if(match) {
const [full, term, spaces, colons, desc] = match;
let offset = 0;
tokens.push({
line : lineNumber,
type : customTags.definitionList,
});
// Term
tokens.push({
line : lineNumber,
type : customTags.definitionTerm,
from : offset,
to : offset + term.length,
});
offset += term.length;
// Spaces before ::
if(spaces) {
offset += spaces.length;
}
// :: colons
tokens.push({
line : lineNumber,
type : customTags.definitionColon,
from : offset,
to : offset + colons.length,
});
offset += colons.length;
// Definition
tokens.push({
line : lineNumber,
type : customTags.definitionDesc,
from : offset,
to : offset + desc.length,
});
return;
}
// multiline def list
if(!/^::/.test(lines[lineNumber]) && lineNumber + 1 < lines.length && /^::/.test(lines[lineNumber + 1])) {
const startLine = lineNumber;
const defs = [];
let endLine = startLine;
// collect all following :: definitions
for (let i = lineNumber + 1; i < lines.length; i++) {
const nextLine = lines[i];
const onlyColonsMatch = /^:*$/.test(nextLine);
const defMatch = /^(::)(.*\S.*)?\s*$/.exec(nextLine);
if(!onlyColonsMatch && defMatch) {
defs.push({ colons: defMatch[1], desc: defMatch[2], line: i });
endLine = i;
} else break;
}
if(defs.length > 0) {
tokens.push({
line : startLine,
type : customTags.definitionList,
});
// term
tokens.push({
line : startLine,
type : customTags.definitionTerm,
from : 0,
to : lineText.length,
});
// definitions
defs.forEach((d)=>{
tokens.push({
line : d.line,
type : customTags.definitionList,
});
tokens.push({
line : d.line,
type : customTags.definitionColon,
from : 0,
to : d.colons.length,
});
tokens.push({
line : d.line,
type : customTags.definitionDesc,
from : d.colons.length,
to : d.colons.length + d.desc.length,
});
});
}
}
if(lineText.includes('{') && lineText.includes('}')) {
const injectionRegex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gm;
let match;
while ((match = injectionRegex.exec(lineText)) !== null) {
tokens.push({
line : lineNumber,
from : match.index +1,
to : match.index + match[1].length +1,
type : customTags.injection,
});
}
}
if(lineText.includes('{{') && lineText.includes('}}')) {
// Inline blocks: single-line {{…}}
const spanRegex = /{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *|}}/g;
let match;
let blockCount = 0;
while ((match = spanRegex.exec(lineText)) !== null) {
if(match[0].startsWith('{{')) {
blockCount += 1;
} else {
blockCount -= 1;
}
if(blockCount < 0) {
blockCount = 0;
continue;
}
tokens.push({
line : lineNumber,
from : match.index,
to : match.index + match[0].length,
type : customTags.inlineBlock,
});
}
} else if(lineText.trimLeft().startsWith('{{') || lineText.trimLeft().startsWith('}}')) {
// Highlight block divs {{\n Content \n}}
let endCh = lineText.length + 1;
const match = lineText.match(
/^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *$|^ *}}$/,
);
if(match) endCh = match.index + match[0].length;
tokens.push({ line: lineNumber, type: customTags.block });
}
});
return tokens;
}
export function tokenizeCustomCSS(text) {
const tokens = [];
const lines = text.split('\n');
lines.forEach((lineText, lineNumber)=>{
if(/--[a-zA-Z0-9-_]+/gm.test(lineText)) {
const varRegex =/--[a-zA-Z0-9-_]+/gm;
let match;
while ((match = varRegex.exec(lineText)) !== null) {
tokens.push({
line : lineNumber,
from : match.index +1,
to : match.index + match.length[1] +1,
type : customTags.varProperty,
});
}
}
});
return tokens;
}
//assign classes to tags provided by lezer, not unlike the function above
export const customHighlightStyle = HighlightStyle.define([
{ tag: tags.heading, class: 'cm-header' },
{ tag: tags.heading1, class: 'cm-header cm-header-1' },
{ tag: tags.heading2, class: 'cm-header cm-header-2' },
{ tag: tags.heading3, class: 'cm-header cm-header-3' },
{ tag: tags.heading4, class: 'cm-header cm-header-4' },
{ tag: tags.heading5, class: 'cm-header cm-header-5' },
{ tag: tags.heading6, class: 'cm-header cm-header-6' },
{ tag: tags.link, class: 'cm-link' },
{ tag: tags.string, class: 'cm-string' },
{ tag: tags.url, class: 'cm-string cm-url' },
{ tag: tags.list, class: 'cm-list' },
{ tag: tags.strong, class: 'cm-strong' },
{ tag: tags.emphasis, class: 'cm-em' },
{ tag: tags.quote, class: 'cm-quote' },
{ tag: tags.comment, class: 'cm-comment' },
{ tag: tags.monospace, class: 'cm-comment' },
//css tags
{ tag: tags.tagName, class: 'cm-tag' },
{ tag: tags.className, class: 'cm-class' },
{ tag: tags.propertyName, class: 'cm-property' },
{ tag: tags.attributeValue, class: 'cm-value' },
{ tag: tags.keyword, class: 'cm-keyword' },
{ tag: tags.atom, class: 'cm-atom' },
{ tag: tags.integer, class: 'cm-integer' },
{ tag: tags.unit, class: 'cm-unit' },
{ tag: tags.color, class: 'cm-color' },
{ tag: tags.paren, class: 'cm-paren' },
{ tag: tags.variableName, class: 'cm-variable' },
{ tag: tags.invalid, class: 'cm-error' },
]);
@@ -0,0 +1,238 @@
/* eslint max-lines: ["error", { "max": 300 }] */
import { keymap } from '@codemirror/view';
import { undo, redo } from '@codemirror/commands';
const insertTabAtCursor = (view)=>{
const { from } = view.state.selection.main;
view.dispatch({
changes : { from, insert: ' ' },
selection : { anchor: from + 1 }
});
return true;
};
const indentMore = (view)=>{
const { from, to } = view.state.selection.main;
const lines = [];
for (let l = view.state.doc.lineAt(from).number; l <= view.state.doc.lineAt(to).number; l++) {
const line = view.state.doc.line(l);
lines.push({ from: line.from, to: line.from, insert: ' ' }); // 2 spaces for tab
}
view.dispatch({ changes: lines });
return true;
};
const indentLess = (view)=>{
const { from, to } = view.state.selection.main;
const lines = [];
for (let l = view.state.doc.lineAt(from).number; l <= view.state.doc.lineAt(to).number; l++) {
const line = view.state.doc.line(l);
const match = line.text.match(/^ {1,2}/); // match up to 2 spaces
if(match) {
lines.push({ from: line.from, to: line.from + match[0].length, insert: '' });
}
}
if(lines.length > 0) view.dispatch({ changes: lines });
return true;
};
const makeBold = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const text = selected.startsWith('**') && selected.endsWith('**')
? selected.slice(2, -2)
: `**${selected}**`;
view.dispatch({
changes : { from, to, insert: text },
selection : { anchor: from + text.length },
});
return true;
};
const makeItalic = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const text = selected.startsWith('*') && selected.endsWith('*')
? selected.slice(1, -1)
: `*${selected}*`;
view.dispatch({
changes : { from, to, insert: text },
selection : { anchor: from + text.length },
});
return true;
};
const makeUnderline = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const text = selected.startsWith('<u>') && selected.endsWith('</u>')
? selected.slice(3, -4)
: `<u>${selected}</u>`;
view.dispatch({
changes : { from, to, insert: text },
selection : { anchor: from + text.length },
});
return true;
};
const makeSuper = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const text = selected.startsWith('^') && selected.endsWith('^')
? selected.slice(1, -1)
: `^${selected}^`;
view.dispatch({
changes : { from, to, insert: text },
selection : { anchor: from + text.length },
});
return true;
};
const makeSub = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const text = selected.startsWith('^^') && selected.endsWith('^^')
? selected.slice(2, -2)
: `^^${selected}^^`;
view.dispatch({
changes : { from, to, insert: text },
selection : { anchor: from + text.length },
});
return true;
};
const makeNbsp = (view)=>{
const { from, to } = view.state.selection.main;
view.dispatch({ changes: { from, to, insert: '&nbsp;' } });
return true;
};
const makeSpace = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const match = selected.match(/^{{width:(\d+)% }}$/);
let newText = '{{width:10% }}';
if(match) {
const percent = Math.min(parseInt(match[1], 10) + 10, 100);
newText = `{{width:${percent}% }}`;
}
view.dispatch({ changes: { from, to, insert: newText } });
return true;
};
const removeSpace = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const match = selected.match(/^{{width:(\d+)% }}$/);
if(match) {
const percent = parseInt(match[1], 10) - 10;
const newText = percent > 0 ? `{{width:${percent}% }}` : '';
view.dispatch({ changes: { from, to, insert: newText } });
}
return true;
};
const makeSpan = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const text = selected.startsWith('{{') && selected.endsWith('}}')
? selected.slice(2, -2)
: `{{${selected}}}`;
view.dispatch({ changes: { from, to, insert: text } });
return true;
};
const makeDiv = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const text = selected.startsWith('{{') && selected.endsWith('}}')
? selected.slice(2, -2)
: `{{\n${selected}\n}}`;
view.dispatch({ changes: { from, to, insert: text } });
return true;
};
const makeComment = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const isHtmlComment = selected.startsWith('<!--') && selected.endsWith('-->');
const text = isHtmlComment
? selected.slice(4, -3)
: `<!-- ${selected} -->`;
view.dispatch({ changes: { from, to, insert: text } });
return true;
};
const makeLink = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to).trim();
const isLink = /^\[(.*)\]\((.*)\)$/.exec(selected);
const text = isLink ? `${isLink[1]} ${isLink[2]}` : `[${selected || 'alt text'}](url)`;
view.dispatch({ changes: { from, to, insert: text } });
return true;
};
const makeList = (type)=>(view)=>{
const { from, to } = view.state.selection.main;
const lines = [];
for (let l = from; l <= to; l++) {
const lineText = view.state.doc.line(l + 1).text;
lines.push(lineText);
}
const joined = lines.join('\n');
let newText;
if(type === 'UL') newText = joined.replace(/^/gm, '- ');
else newText = joined.replace(/^/gm, (m, i)=>`${i + 1}. `);
view.dispatch({ changes: { from, to, insert: newText } });
return true;
};
const makeHeader = (level)=>(view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const text = `${'#'.repeat(level)} ${selected}`;
view.dispatch({ changes: { from, to, insert: text } });
return true;
};
const newColumn = (view)=>{
const { from, to } = view.state.selection.main;
view.dispatch({ changes: { from, to, insert: '\n\\column\n\n' } });
return true;
};
const newPage = (view)=>{
const { from, to } = view.state.selection.main;
view.dispatch({ changes: { from, to, insert: '\n\\page\n\n' } });
return true;
};
export default keymap.of([
{ key: 'Tab', run: insertTabAtCursor },
//{ key: 'Shift-Tab', run: indentMore },
{ key: 'Shift-Tab', run: indentLess },
{ key: 'Mod-b', run: makeBold },
{ key: 'Mod-i', run: makeItalic },
{ key: 'Mod-u', run: makeUnderline },
{ key: 'Shift-Mod-=', run: makeSuper },
{ key: 'Mod-=', run: makeSub },
{ key: 'Mod-.', run: makeNbsp },
{ key: 'Shift-Mod-.', run: makeSpace },
{ key: 'Shift-Mod-,', run: removeSpace },
{ key: 'Mod-m', run: makeSpan },
{ key: 'Shift-Mod-m', run: makeDiv },
{ key: 'Mod-/', run: makeComment },
{ key: 'Mod-k', run: makeLink },
{ key: 'Mod-l', run: makeList('UL') },
{ key: 'Shift-Mod-l', run: makeList('OL') },
{ key: 'Shift-Mod-1', run: makeHeader(1) },
{ key: 'Shift-Mod-2', run: makeHeader(2) },
{ key: 'Shift-Mod-3', run: makeHeader(3) },
{ key: 'Shift-Mod-4', run: makeHeader(4) },
{ key: 'Shift-Mod-5', run: makeHeader(5) },
{ key: 'Shift-Mod-6', run: makeHeader(6) },
{ key: 'Shift-Mod-Enter', run: newColumn },
{ key: 'Mod-Enter', run: newPage },
{ key: 'Mod-z', run: undo }, //i think it may be unnecessary
{ key: 'Mod-Shift-z', run: redo },
]);
-44
View File
@@ -1,44 +0,0 @@
export default {
registerHomebreweryHelper : function(CodeMirror) {
CodeMirror.registerHelper('fold', 'homebrewerycss', function(cm, start) {
// BRACE FOLDING
const startMatcher = /\{[ \t]*$/;
const endMatcher = /\}[ \t]*$/;
const activeLine = cm.getLine(start.line);
if(activeLine.match(startMatcher)) {
const lastLineNo = cm.lastLine();
let end = start.line + 1;
let braceCount = 1;
while (end < lastLineNo) {
const curLine = cm.getLine(end);
if(curLine.match(startMatcher)) braceCount++;
if(curLine.match(endMatcher)) braceCount--;
if(braceCount == 0) break;
++end;
}
return {
from : CodeMirror.Pos(start.line, 0),
to : CodeMirror.Pos(end, cm.getLine(end).length)
};
}
// @import and data-url folding
const importMatcher = /^@import.*?;/;
const dataURLMatcher = /url\(.*?data\:.*\)/;
if(activeLine.match(importMatcher) || activeLine.match(dataURLMatcher)) {
return {
from : CodeMirror.Pos(start.line, 0),
to : CodeMirror.Pos(start.line, activeLine.length)
};
}
return null;
});
}
};
@@ -1,26 +0,0 @@
export default {
registerHomebreweryHelper : function(CodeMirror) {
CodeMirror.registerHelper('fold', 'homebrewery', function(cm, start) {
const matcher = /^\\page.*/;
const prevLine = cm.getLine(start.line - 1);
if(start.line === cm.firstLine() || prevLine.match(matcher)) {
const lastLineNo = cm.lastLine();
let end = start.line;
while (end < lastLineNo) {
if(cm.getLine(end + 1).match(matcher))
break;
++end;
}
return {
from : CodeMirror.Pos(start.line, 0),
to : CodeMirror.Pos(end, cm.getLine(end).length)
};
}
return null;
});
}
};
@@ -0,0 +1,54 @@
import { HighlightStyle } from '@codemirror/language';
import { tags } from '@lezer/highlight';
const customTags = {
pageLine : 'pageLine', // .cm-pageLine
snippetLine : 'snippetLine', // .cm-snippetLine
};
export function legacyTokenizeCustomMarkdown(text) {
const tokens = [];
const lines = text.split('\n');
lines.forEach((lineText, lineNumber)=>{
// --- Page / snippet lines ---
if(/^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m.test(lineText)) tokens.push({ line: lineNumber, type: customTags.pageLine });
if(/^\\snippet\ .*$/.test(lineText)) tokens.push({ line: lineNumber, type: customTags.snippetLine });
});
return tokens;
}
export const legacyCustomHighlightStyle = HighlightStyle.define([
{ tag: tags.heading, class: 'cm-header' },
{ tag: tags.heading1, class: 'cm-header cm-header-1' },
{ tag: tags.heading2, class: 'cm-header cm-header-2' },
{ tag: tags.heading3, class: 'cm-header cm-header-3' },
{ tag: tags.heading4, class: 'cm-header cm-header-4' },
{ tag: tags.heading5, class: 'cm-header cm-header-5' },
{ tag: tags.heading6, class: 'cm-header cm-header-6' },
{ tag: tags.link, class: 'cm-link' },
{ tag: tags.string, class: 'cm-string' },
{ tag: tags.url, class: 'cm-string cm-url' },
{ tag: tags.list, class: 'cm-list' },
{ tag: tags.strong, class: 'cm-strong' },
{ tag: tags.emphasis, class: 'cm-em' },
{ tag: tags.quote, class: 'cm-quote' },
//css tags
{ tag: tags.tagName, class: 'cm-tag' },
{ tag: tags.className, class: 'cm-class' },
{ tag: tags.propertyName, class: 'cm-property' },
{ tag: tags.attributeValue, class: 'cm-value' },
{ tag: tags.keyword, class: 'cm-keyword' },
{ tag: tags.atom, class: 'cm-atom' },
{ tag: tags.integer, class: 'cm-integer' },
{ tag: tags.unit, class: 'cm-unit' },
{ tag: tags.color, class: 'cm-color' },
{ tag: tags.paren, class: 'cm-paren' },
{ tag: tags.variableName, class: 'cm-variable' },
{ tag: tags.invalid, class: 'cm-error' },
{ tag: tags.comment, class: 'cm-comment' },
]);
+28 -209
View File
@@ -4,7 +4,6 @@ import React from 'react';
import createReactClass from 'create-react-class';
import _ from 'lodash';
import dedent from 'dedent';
import Markdown from '@shared/markdown.js';
import CodeEditor from '../../components/codeEditor/codeEditor.jsx';
import SnippetBar from './snippetbar/snippetbar.jsx';
@@ -13,7 +12,7 @@ import MetadataEditor from './metadataEditor/metadataEditor.jsx';
const EDITOR_THEME_KEY = 'HB_editor_theme';
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
//const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
const DEFAULT_STYLE_TEXT = dedent`
/*=======--- Example CSS styling ---=======*/
/* Any CSS here will apply to your document! */
@@ -72,13 +71,9 @@ const Editor = createReactClass({
componentDidMount : function() {
this.highlightCustomMarkdown();
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));
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
if(editorTheme) {
this.setState({
@@ -98,7 +93,6 @@ const Editor = createReactClass({
componentDidUpdate : function(prevProps, prevState, snapshot) {
this.highlightCustomMarkdown();
if(prevProps.moveBrew !== this.props.moveBrew)
this.brewJump();
@@ -132,15 +126,15 @@ const Editor = createReactClass({
}
},
updateCurrentCursorPage : function(cursor) {
const lines = this.props.brew.text.split('\n').slice(1, cursor.line + 1);
updateCurrentCursorPage : function(lineNumber) {
const lines = this.props.brew.text.split('\n').slice(0, lineNumber);
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);
},
updateCurrentViewPage : function(topScrollLine) {
const lines = this.props.brew.text.split('\n').slice(1, topScrollLine + 1);
updateCurrentViewPage : function(topLine) {
const lines = this.props.brew.text.split('\n').slice(0, topLine);
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.onViewPageChange(currentPage);
@@ -160,175 +154,6 @@ const Editor = createReactClass({
});
},
highlightCustomMarkdown : function(){
if(!this.codeEditor.current?.codeMirror) return;
if((this.state.view === 'text') ||(this.state.view === 'snippet')) {
const codeMirror = this.codeEditor.current.codeMirror;
codeMirror?.operation(()=>{ // Batch CodeMirror styling
const foldLines = [];
//reset custom text styles
const customHighlights = codeMirror?.getAllMarks().filter((mark)=>{
// Record details of folded sections
if(mark.__isFold) {
const fold = mark.find();
foldLines.push({ from: fold.from?.line, to: fold.to?.line });
}
return !mark.__isFold;
}); //Don't undo code folding
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
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');
// Don't process lines inside folded text
// If the current lineNumber is inside any folded marks, skip line styling
if(foldLines.some((fold)=>lineNumber >= fold.from && lineNumber <= fold.to))
return;
// Styling for \page breaks
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
(this.props.renderer == 'V3' && line.match(textOrSnip ? PAGEBREAK_REGEX_V3 : SNIPPETBREAK_REGEX_V3))) {
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', tabHighlight);
const pageCountElement = Object.assign(document.createElement('span'), {
className : 'editor-page-count',
textContent : textOrSnip ? editorPageCount : userSnippetCount
});
codeMirror?.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement);
};
// New CodeMirror styling for V3 renderer
if(this.props.renderer === 'V3') {
if(line.match(/^\\column(?:break)?$/)){
codeMirror?.addLineClass(lineNumber, 'text', 'columnSplit');
}
// definition lists
if(line.includes('::')){
if(/^:*$/.test(line) == true){ return; };
const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error.
let match;
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[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' });
const ddIndex = match.indices[2][0];
const colons = /::/g;
const colonMatches = colons.exec(match[2]);
if(colonMatches !== null){
codeMirror?.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight' });
}
}
}
// Subscript & Superscript
if(line.includes('^')) {
let startIndex = line.indexOf('^');
const superRegex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/gy;
const subRegex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/gy;
while (startIndex >= 0) {
superRegex.lastIndex = subRegex.lastIndex = startIndex;
let isSuper = false;
const match = subRegex.exec(line) || superRegex.exec(line);
if(match) {
isSuper = !subRegex.lastIndex;
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));
}
}
// Highlight injectors {style}
if(line.includes('{') && line.includes('}')){
const regex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gm;
let match;
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' });
}
}
// Highlight inline spans {{content}}
if(line.includes('{{') && line.includes('}}')){
const regex = /{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *|}}/g;
let match;
let blockCount = 0;
while ((match = regex.exec(line)) != null) {
if(match[0].startsWith('{')) {
blockCount += 1;
} else {
blockCount -= 1;
}
if(blockCount < 0) {
blockCount = 0;
continue;
}
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('}}')){
// Highlight block divs {{\n Content \n}}
let endCh = line.length+1;
const match = line.match(/^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *$|^ *}}$/);
if(match)
endCh = match.index+match[0].length;
codeMirror?.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
}
// Emojis
if(line.match(/:[^\s:]+:/g)) {
let startIndex = line.indexOf(':');
const emojiRegex = /:[^\s:]+:/gy;
while (startIndex >= 0) {
emojiRegex.lastIndex = startIndex;
const match = emojiRegex.exec(line);
if(match) {
let tokens = Markdown.marked.lexer(match[0]);
tokens = tokens[0].tokens.filter((t)=>t.type == 'emoji');
if(!tokens.length)
return;
const startPos = { line: lineNumber, ch: match.index };
const endPos = { line: lineNumber, ch: match.index + match[0].length };
// Iterate over conflicting marks and clear them
const marks = codeMirror?.findMarks(startPos, endPos);
marks.forEach(function(marker) {
if(!marker.__isFold) marker.clear();
});
codeMirror?.markText(startPos, endPos, { className: 'emoji' });
}
startIndex = line.indexOf(':', Math.max(startIndex + 1, emojiRegex.lastIndex));
}
}
}
});
});
}
},
brewJump : function(targetPage=this.props.currentEditorCursorPageNum, smooth=true){
if(!window || !this.isText() || isJumping)
return;
@@ -376,51 +201,41 @@ const Editor = createReactClass({
const textSplit = this.props.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
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;
let currentY = this.codeEditor.current.codeMirror?.getScrollInfo().top;
let targetY = this.codeEditor.current.codeMirror?.heightAtLine(targetLine, 'local', true);
const editor = this.codeEditor.current;
let currentY = editor.getScrollTop();
const targetY = editor.getLineTop(targetLine);
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
scrollingTimeout = setTimeout(()=>{
isJumping = false;
this.codeEditor.current.codeMirror?.off('scroll', checkIfScrollComplete);
}, 150); // If 150 ms pass without a scroll event, assume scrolling is done
};
isJumping = true;
checkIfScrollComplete();
if(this.codeEditor.current?.codeMirror) {
this.codeEditor.current.codeMirror?.on('scroll', checkIfScrollComplete);
}
if(smooth) {
//Scroll 1/10 of the way every 10ms until 1px off.
const incrementalScroll = setInterval(()=>{
currentY += (targetY - currentY) / 10;
this.codeEditor.current.codeMirror?.scrollTo(null, currentY);
editor.scrollToY(currentY);
// Update target: target height is not accurate until within +-10 lines of the visible window
if(Math.abs(targetY - currentY > 100))
targetY = this.codeEditor.current.codeMirror?.heightAtLine(targetLine, 'local', true);
// End when close enough
if(Math.abs(targetY - currentY) < 1) {
this.codeEditor.current.codeMirror?.scrollTo(null, targetY); // Scroll any remaining difference
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
this.codeEditor.current.codeMirror?.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
editor.scrollToY(targetY);
editor.setCursorToLine(targetLine);
clearInterval(incrementalScroll);
}
}, 10);
} else {
this.codeEditor.current.codeMirror?.scrollTo(null, targetY); // Scroll any remaining difference
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
this.codeEditor.current.codeMirror?.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
editor.scrollToY(targetY);
editor.setCursorToLine(targetLine);
}
},
//Called when there are changes to the editor's dimensions
update : function(){},
@@ -446,7 +261,10 @@ const Editor = createReactClass({
view={this.state.view}
value={this.props.brew.text}
onChange={this.props.onBrewChange('text')}
onCursorChange={(line)=>this.updateCurrentCursorPage(line)}
onViewChange={(line)=>this.updateCurrentViewPage(line)}
editorTheme={this.state.editorTheme}
renderer={this.props.brew.renderer}
rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }} />
</>;
@@ -462,6 +280,7 @@ const Editor = createReactClass({
onChange={this.props.onBrewChange('style')}
enableFolding={true}
editorTheme={this.state.editorTheme}
renderer={this.props.brew.renderer}
rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }} />
</>;
@@ -492,8 +311,9 @@ const Editor = createReactClass({
onChange={this.props.onBrewChange('snippets')}
enableFolding={true}
editorTheme={this.state.editorTheme}
renderer={this.props.brew.renderer}
rerenderParent={this.rerenderParent}
style={{ height: `calc(100% -${this.state.snippetBarHeight}px)` }} />
style={{ height: `calc(100% - 25px)` }} />
</>;
}
},
@@ -510,14 +330,13 @@ const Editor = createReactClass({
return this.codeEditor.current?.undo();
},
foldCode : function(){
return this.codeEditor.current?.foldAllCode();
},
unfoldCode : function(){
return this.codeEditor.current?.unfoldAllCode();
},
foldCode: function() {
return this.codeEditor.current?.foldAll();
},
unfoldCode: function() {
return this.codeEditor.current?.unfoldAll();
},
render : function(){
return (
<div className='editor' ref={this.editor}>
@@ -547,4 +366,4 @@ const Editor = createReactClass({
}
});
export default Editor;
export default Editor;
+107 -76
View File
@@ -1,89 +1,120 @@
@import '@sharedStyles/core.less';
@import '@themes/codeMirror/customEditorStyles.less';
.editor {
position : relative;
width : 100%;
height : 100%;
container : editor / inline-size;
background:white;
.codeEditor {
:where(.editor) {
position : relative;
width : 100%;
height : 100%;
container : editor / inline-size;
background : white;
:where(.codeEditor) {
height : calc(100% - 25px);
.CodeMirror { height : 100%; }
.pageLine, .snippetLine {
.cm-editor { height : 100%;
outline:none !important;
}
&.brewSnippets .cm-snippetLine {
background : #33333328;
border-top : #333399 solid 1px;
}
.editor-page-count {
float : right;
color : grey;
:where(&.brewText) .cm-pageLine {
background : #33333328;
border-top : #333399 solid 1px;
}
.editor-snippet-count {
float : right;
color : grey;
}
.columnSplit {
font-style : italic;
color : grey;
background-color : fade(#229999, 15%);
border-bottom : #229999 solid 1px;
}
.define {
&:not(.term):not(.definition) {
font-weight : bold;
color : #949494;
background : #E5E5E5;
border-radius : 3px;
&.brewSnippets {
.cm-pageLine {
background : #3e4e3e1b;
border-top : #3399423b solid 1px;
color:#777;
}
&.term { color : rgb(96, 117, 143); }
&.definition { color : rgb(97, 57, 178); }
}
.block:not(.cm-comment) {
font-weight : bold;
color : purple;
//font-style: italic;
}
.inline-block:not(.cm-comment) {
font-weight : bold;
color : red;
//font-style: italic;
}
.injection:not(.cm-comment) {
font-weight : bold;
color : green;
}
.emoji:not(.cm-comment) {
padding-bottom : 1px;
margin-left : 2px;
font-weight : bold;
color : #360034;
outline : solid 2px #FF96FC;
outline-offset : -2px;
background : #FFC8FF;
border-radius : 6px;
}
.superscript:not(.cm-comment) {
font-size : 0.9em;
font-weight : bold;
vertical-align : super;
color : goldenrod;
}
.subscript:not(.cm-comment) {
font-size : 0.9em;
font-weight : bold;
vertical-align : sub;
color : rgb(123, 123, 15);
}
.dl-highlight {
&.dl-colon-highlight {
font-weight : bold;
color : #949494;
background : #E5E5E5;
border-radius : 3px;
&:where(.brewText), &.brewSnippets {
.cm-tooltip-autocomplete {
li {
display : flex;
gap : 10px;
align-items : center;
justify-content : flex-start;
.cm-completionIcon { display : none; }
.cm-tooltip-autocomplete .cm-completionLabel { translate : 0 -2px; }
}
}
&.dt-highlight { color : rgb(96, 117, 143); }
&.dd-highlight { color : rgb(97, 57, 178); }
}
.cm-pageLine[data-page-number]::after {
content:attr(data-page-number);
float:right;
color : grey;
}
.cm-columnSplit {
font-style : italic;
color : grey;
background-color : fade(#229999, 15%);
border-bottom : #229999 solid 1px;
}
.cm-define {
&:not(.term):not(.definition) {
font-weight : bold;
color : #949494;
background : #E5E5E5;
border-radius : 3px;
}
&.term { color : rgb(96, 117, 143); }
&.definition { color : rgb(97, 57, 178); }
}
.cm-block:not(.cm-comment) {
font-weight : bold;
color : purple;
}
.cm-inline-block:not(.cm-comment) {
font-weight : bold;
color : red ;
span { color : inherit }
}
.cm-injection:not(.cm-comment) {
font-weight : bold;
color : green;
span { color : inherit }
}
.cm-emoji:not(.cm-comment) {
padding-bottom : 1px;
margin-left : 2px;
font-weight : bold;
color : #360034;
outline : solid 2px #FF96FC;
outline-offset : -2px;
background : #FFC8FF;
border-radius : 6px;
}
.cm-superscript:not(.cm-comment) {
font-size : 0.9em;
font-weight : bold;
vertical-align : super;
color : goldenrod;
}
.cm-subscript:not(.cm-comment) {
font-size : 0.9em;
font-weight : bold;
vertical-align : sub;
color : rgb(123, 123, 15);
}
.cm-definitionList {
.cm-definitionTerm { color : rgb(96, 117, 143); }
.cm-definitionColon {
font-weight : bold;
color : #949494;
background : #E5E5E5;
border-radius : 3px;
}
.cm-definitionDesc { color : rgb(97, 57, 178); }
}
}
}
.brewJump {
@@ -23,7 +23,20 @@ const ThemeSnippets = {
V3_Blank : V3_Blank,
};
import EditorThemes from '../../../../build/homebrew/codeMirror/editorThemes.json';
//import EditorThemes from '../../../../build/homebrew/codeMirror/editorThemes.json';
import * as themesImport from '@uiw/codemirror-themes-all';
import defaultCM5Theme from '@themes/codeMirror/default.js';
import darkbrewery from '@themes/codeMirror/darkbrewery.js';
const themes = { default: defaultCM5Theme, darkbrewery, ...themesImport };
const EditorThemes = Object.entries(themes)
.filter(([name, value]) =>
Array.isArray(value) &&
!name.endsWith('Init') &&
!name.endsWith('Style')
)
.map(([name]) => name);
const execute = function(val, props){
if(_.isFunction(val)) return val(props);
@@ -232,11 +245,11 @@ const Snippetbar = createReactClass({
<i className='fas fa-clock-rotate-left' />
{ this.state.showHistory && this.renderHistoryItems() }
</div>
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
<div className={`editorTool undo ${this.props.historySize.done ? 'active' : ''}`}
onClick={this.props.undo} >
<i className='fas fa-undo' />
</div>
<div className={`editorTool redo ${this.props.historySize.redo ? 'active' : ''}`}
<div className={`editorTool redo ${this.props.historySize.undone ? 'active' : ''}`}
onClick={this.props.redo} >
<i className='fas fa-redo' />
</div>
+1693 -505
View File
File diff suppressed because it is too large Load Diff
+14 -1
View File
@@ -91,13 +91,26 @@
"@babel/preset-env": "^7.29.2",
"@babel/preset-react": "^7.28.5",
"@babel/runtime": "^7.29.2",
"@codemirror/autocomplete": "^6.20.1",
"@codemirror/commands": "^6.10.3",
"@codemirror/highlight": "^0.19.8",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-javascript": "^6.2.5",
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/language": "^6.12.2",
"@codemirror/language-data": "^6.5.2",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.6.0",
"@codemirror/view": "^6.40.0",
"@dmsnell/diff-match-patch": "^1.1.0",
"@googleapis/drive": "^20.1.0",
"@lezer/highlight": "^1.2.3",
"@sanity/diff-match-patch": "^3.2.0",
"@uiw/codemirror-themes-all": "^4.25.8",
"@vitejs/plugin-react": "^5.1.2",
"body-parser": "^2.2.0",
"classnames": "^2.5.1",
"codemirror": "^5.65.6",
"cm6-theme-basic-light": "^0.2.0",
"cookie-parser": "^1.4.7",
"core-js": "^3.49.0",
"cors": "^2.8.5",
-83
View File
@@ -1,83 +0,0 @@
.editor .codeEditor .CodeMirror {
// Themes with dark backgrounds
&.cm-s-3024-night,
&.cm-s-abbott,
&.cm-s-abcdef,
&.cm-s-ambiance,
&.cm-s-ayu-dark,
&.cm-s-ayu-mirage,
&.cm-s-base16-dark,
&.cm-s-bespin,
&.cm-s-blackboard,
&.cm-s-cobalt,
&.cm-s-colorforth,
&.cm-s-darcula,
&.cm-s-dracula,
&.cm-s-duotone-dark,
&.cm-s-erlang-dark,
&.cm-s-gruvbox-dark,
&.cm-s-hopscotch,
&.cm-s-icecoder,
&.cm-s-isotope,
&.cm-s-lesser-dark,
&.cm-s-liquibyte,
&.cm-s-lucario,
&.cm-s-material,
&.cm-s-material-darker,
&.cm-s-material-ocean,
&.cm-s-material-palenight,
&.cm-s-mbo,
&.cm-s-midnight,
&.cm-s-monokai,
&.cm-s-moxer,
&.cm-s-night,
&.cm-s-nord,
&.cm-s-oceanic-next,
&.cm-s-panda-syntax,
&.cm-s-paraiso-dark,
&.cm-s-pastel-on-dark,
&.cm-s-railscasts,
&.cm-s-rubyblue,
&.cm-s-seti,
&.cm-s-shadowfox,
&.cm-s-the-matrix,
&.cm-s-tomorrow-night-bright,
&.cm-s-tomorrow-night-eighties,
&.cm-s-twilight,
&.cm-s-vibrant-ink,
&.cm-s-xq-dark,
&.cm-s-yonce,
&.cm-s-zenburn {
.CodeMirror-code {
.block:not(.cm-comment) { color : magenta; }
.columnSplit {
color : black;
background-color : rgba(35,153,153,0.5);
}
.pageLine {
background-color : rgba(255,255,255,0.5);
& ~ pre.CodeMirror-line { color : black; }
}
}
}
// Themes with light backgrounds
&.cm-s-default,
&.cm-s-3024-day,
&.cm-s-ambiance-mobile,
&.cm-s-base16-light,
&.cm-s-duotone-light,
&.cm-s-eclipse,
&.cm-s-elegant,
&.cm-s-juejin,
&.cm-s-neat,
&.cm-s-neo,
&.cm-s-paraiso-lightm
&.cm-s-solarized,
&.cm-s-ssms,
&.cm-s-ttcn,
&.cm-s-xq-light,
&.cm-s-yeti {
// Future styling for themes with light backgrounds
--dummyVar : 'currently unused';
}
}
@@ -1,134 +0,0 @@
/*stylelint-disable*/
.editor .snippetBar {
color: white;
background-color: #2F393C;
.dropdown {
background-color: #2F393C;
}
.editors {
border-color: #ccc;
}
}
/* Main BG color and normal text color */
.CodeMirror {
--bg: #293134;
--highlight: #bcbcbc;
color: #91A6AA;
background: var(--bg);
.CodeMirror-scroll {
.CodeMirror-gutters {
border-right: 1px solid #555;
background: var(--bg);
.CodeMirror-gutter {
background-color: var(--bg);
&.CodeMirror-foldgutter {
cursor: pointer;
border-left: 1px solid #555;
transition: background 0.1s;
&:hover {
background: #555;
}
}
}
}
.CodeMirror-lines {
/* Line numbers*/
.CodeMirror-linenumber.CodeMirror-gutter-elt {
background-color: var(--bg);
color: #81969A;
}
/* Blinking cursor */
.CodeMirror-cursor {
border-left: 1px solid #E0E2E4;
}
.pageLine {
color: #000000;
background: #000000;
border-bottom: 1px solid #FFFFFF;
}
.CodeMirror-code .CodeMirror-line {
&.columnSplit {
font-style: italic;
color: inherit;
background-color: #1F5763;
border-bottom: #229999 solid 1px;
}
/*syntax*/
.cm-header {
font-weight: bold;
color: #C51B1B;
-webkit-text-stroke-width: 0.1px;
-webkit-text-stroke-color: #000000;
}
.cm-strong {
color: #309DD2;
}
.cm-em {
/*italics*/
}
.cm-link {
color: #DD6300;
}
.cm-string {
color: #AA8261;
}
/* @import */
.cm-def {
color: #2986CC;
}
/* Bullets and such */
.cm-variable-2 {
color: #3CBF30;
}
.block:not(.cm-comment) {
color: #E3E3E3;
}
.inline-block {
color: #E3E3E3;
}
.cm-tag {
color: #E3FF00;
}
.cm-attribute {
color: #E3FF00;
}
.cm-atom {
color: #c1939a;
}
.cm-number {
color: #2986CC;
}
.cm-property:not(.cm-error) ~ .cm-variable {
color:#9e1f9e;
}
.cm-qualifier {
color: #EE1919;
}
.cm-comment {
color: #BBC700;
}
.cm-keyword {
color: white;
}
.cm-error {
color: #C50202;
}
.CodeMirror-foldmarker {
color: #F0FF00;
}
.cm-builtin {
color: #FFFFFF;
}
.dt-highlight {
background: #ffffff14;
}
.dl-colon-highlight {
background: #ccc;
}
.dl-highlight.dd-highlight {
color: #b5858d;
}
}
}
}
}
+114
View File
@@ -0,0 +1,114 @@
import { EditorView } from '@codemirror/view';
export default EditorView.theme({
'&' : {
backgroundColor : '#293134',
color : '#91a6aa',
},
'.cm-content' : {
padding : '4px 0',
fontFamily : 'monospace',
fontSize : '13px',
lineHeight : '1',
},
'.cm-line' : {
padding : '0 4px',
},
'.cm-gutters' : {
borderRight : '1px solid #555',
backgroundColor : '#293134',
whiteSpace : 'nowrap',
},
'.cm-foldGutter' : {
borderLeft : '1px solid #555',
backgroundColor : '#293134',
},
'.cm-foldGutter:hover' : {
backgroundColor : '#555',
},
'.cm-gutterElement' : {
color : '#81969a',
},
'.cm-linenumber' : {
padding : '0 3px 0 5px',
minWidth : '20px',
textAlign : 'right',
color : '#999',
whiteSpace : 'nowrap',
},
'.cm-cursor' : {
borderLeft : '1px solid #E0E2E4',
},
'.cm-fat-cursor' : {
width : 'auto',
backgroundColor : '#7e7',
caretColor : 'transparent',
},
'.cm-activeLine' : {
backgroundColor : '#868c9323',
},
'.cm-gutterElement.cm-activeLineGutter' : {
backgroundColor : '#868c9323',
},
'.cm-activeLine' : {
backgroundColor : '#868c9323',
},
'.cm-selected' : {
backgroundColor : '#d7d4f0',
},
'.cm-pageLine' : {
backgroundColor : '#7ca97c',
color : '#000',
fontWeight : 'bold',
letterSpacing : '.5px',
borderTop : '1px solid #ff0',
},
'.cm-columnSplit' : {
backgroundColor : '#7ca97c',
color : 'black',
fontWeight : 'bold',
letterSpacing : '1px',
borderBottom : '1px solid #ff0',
},
'.cm-line.cm-block, .cm-line .cm-inline-block' : {
color : '#E3E3E3',
},
'.cm-definitionList .cm-definitionTerm' : {
color : '#E3E3E3',
},
'.cm-definitionList .cm-definitionColon' : {
backgroundColor : '#0000',
color : '#e3FF00',
},
'.cm-definitionList .cm-definitionDesc' : {
color : '#b5858d',
},
// Semantic classes
'.cm-header' : { color: '#C51B1B', fontWeight: 'bold' },
'.cm-strong' : { color: '#309dd2', fontWeight: 'bold' },
'.cm-em' : { fontStyle: 'italic' },
'.cm-keyword' : { color: '#fff' },
'.cm-atom, cm-value, cm-color' : { color: '#c1939a' },
'.cm-number' : { color: '#2986cc' },
'.cm-def' : { color: '#2986cc' },
'.cm-list' : { color: '#3cbf30' },
'.cm-variable, .cm-type' : { color: '#085' },
'.cm-comment' : { color: '#bbc700' },
'.cm-link' : { color: '#DD6300', textDecoration: 'underline' },
'.cm-string' : { color: '#AA8261', textDecoration: 'none' },
'.cm-string-2' : { color: '#f50', textDecoration: 'none' },
'.cm-meta, .cm-qualifier, .cm-class' : { color: '#19ee2b' },
'.cm-builtin' : { color: '#fff' },
'.cm-bracket' : { color: '#997' },
'.cm-tag, .cm-attribute' : { color: '#e3ff00' },
'.cm-hr' : { color: '#999' },
'.cm-negative' : { color: '#d44' },
'.cm-positive' : { color: '#292' },
'.cm-error, .cm-invalidchar' : { color: '#c50202' },
'.cm-matchingbracket' : { color: '#0b0' },
'.cm-nonmatchingbracket' : { color: '#a22' },
'.cm-matchingtag' : { backgroundColor: 'rgba(255, 150, 0, 0.3)' },
'.cm-quote' : { color: '#090' },
}, { dark: true });
@@ -1,3 +1,8 @@
/*This document is old, from back when Codemirror was version 5,
if someone wants to update it, feel free, it needs to be like default.js or darkbrewery.js
Then imported in snippetbar.jsx and codeEditor.jsx.
*/
.CodeMirror {
background: #0C0C0C;
color: #B9BDB6;
@@ -18,13 +23,13 @@
}
/* Line number stuff */
.CodeMirror-gutter-elt {
.cm-gutter-elt {
color: #81969A;
}
.CodeMirror-linenumber {
background-color: #0C0C0C;
}
.CodeMirror-gutter {
.cm-gutter {
background-color: #0C0C0C;
}
+81
View File
@@ -0,0 +1,81 @@
import { EditorView } from '@codemirror/view';
//This theme is made of the base css for the codemirror 5 editor
export default EditorView.theme({
'&' : {
backgroundColor : 'white',
color : 'black',
},
'.cm-content' : {
padding : '4px 0',
fontFamily : 'monospace',
fontSize : '13px',
lineHeight : '1',
},
'.cm-line' : {
padding : '0 4px',
},
'.cm-gutters' : {
borderRight : '1px solid #ddd',
backgroundColor : '#f7f7f7',
whiteSpace : 'nowrap',
},
'.cm-linenumber' : {
padding : '0 3px 0 5px',
minWidth : '20px',
textAlign : 'right',
color : '#999',
whiteSpace : 'nowrap',
},
'.cm-cursor' : {
borderLeft : '1px solid black',
},
'.cm-fat-cursor' : {
width : 'auto',
backgroundColor : '#7e7',
caretColor : 'transparent',
},
'.cm-activeLine' : {
backgroundColor : '#becee374',
},
'.cm-gutterElement.cm-activeLineGutter' : {
backgroundColor : '#becee374',
},
'.cm-selected' : {
backgroundColor : '#d7d4f0',
},
'.cm-foldmarker' : {
color : 'blue',
fontFamily : 'arial',
lineHeight : '0.3',
cursor : 'pointer',
},
'.cm-header' : { color: 'blue', fontWeight: 'bold' },
'.cm-strong' : { fontWeight: 'bold' },
'.cm-em' : { fontStyle: 'italic' },
'.cm-keyword' : { color: '#708' },
'.cm-atom, cm-value, cm-color' : { color: '#219' },
'.cm-number' : { color: '#164' },
'.cm-def' : { color: '#00f' },
'.cm-list' : { color: '#05a' },
'.cm-variable, .cm-type' : { color: '#085' },
'.cm-comment' : { color: '#a50' },
'.cm-link' : { color: '#00c', textDecoration: 'underline' },
'.cm-string' : { color: '#a11', textDecoration: 'none' },
'.cm-string-2' : { color: '#f50', textDecoration: 'none' },
'.cm-meta, .cm-qualifier, .cm-class' : { color: '#555' },
'.cm-builtin' : { color: '#30a' },
'.cm-bracket' : { color: '#997' },
'.cm-tag' : { color: '#170' },
'.cm-attribute' : { color: '#00c' },
'.cm-hr' : { color: '#999' },
'.cm-negative' : { color: '#d44' },
'.cm-positive' : { color: '#292' },
'.cm-error, .cm-invalidchar' : { color: '#f00' },
'.cm-matchingbracket' : { color: '#0b0' },
'.cm-nonmatchingbracket' : { color: '#a22' },
'.cm-matchingtag' : { backgroundColor: '#ff96004d' },
'.cm-quote' : { color: '#090' },
}, { dark: false });
-13
View File
@@ -61,19 +61,6 @@ export function generateAssetsPlugin(isDev = false) {
await fs.copy('./themes/fonts', `${buildDir}/fonts`);
await fs.copy('./themes/assets', `${buildDir}/assets`);
await fs.copy('./client/icons', `${buildDir}/icons`);
// Compile CodeMirror editor themes
const editorThemesBuildDir = `${buildDir}/homebrew/cm-themes`;
await fs.copy('./node_modules/codemirror/theme', editorThemesBuildDir);
await fs.copy('./themes/codeMirror/customThemes', editorThemesBuildDir);
const editorThemeFiles = fs.readdirSync(editorThemesBuildDir);
await fs.outputFile(`${buildDir}/homebrew/codeMirror/editorThemes.json`,
JSON.stringify(['default', ...editorThemeFiles.map((f)=>f.slice(0, -4))], null, 2),
);
// Copy remaining CodeMirror assets
await fs.copy('./themes/codeMirror', `${buildDir}/homebrew/codeMirror`);
},
};
}