mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-06-22 04:58:40 +00:00
Merge branches 'add-cm-features' and 'master' of https://github.com/naturalcrit/homebrewery into add-cm-features
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/* eslint max-lines: ["error", { "max": 500 }] */
|
||||
/* eslint max-lines: ["error", { "max": 405 }] */
|
||||
import './codeEditor.less';
|
||||
import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
|
||||
@@ -10,20 +10,26 @@ import {
|
||||
highlightActiveLine,
|
||||
scrollPastEnd,
|
||||
Decoration,
|
||||
ViewPlugin,
|
||||
drawSelection,
|
||||
dropCursor,
|
||||
rectangularSelection,
|
||||
crosshairCursor,
|
||||
} from '@codemirror/view';
|
||||
import { EditorState, Compartment, StateEffect, StateField } from '@codemirror/state';
|
||||
import { foldAll as foldAllCmd, unfoldAll as unfoldAllCmd, foldGutter, foldKeymap, foldEffect, foldState, syntaxHighlighting } from '@codemirror/language';
|
||||
import {
|
||||
unfoldAll as unfoldAllCmd,
|
||||
foldGutter,
|
||||
foldKeymap,
|
||||
foldEffect,
|
||||
foldState,
|
||||
syntaxHighlighting,
|
||||
} from '@codemirror/language';
|
||||
import { defaultKeymap, history, undo, redo, undoDepth, redoDepth } from '@codemirror/commands';
|
||||
import { languages } from '@codemirror/language-data';
|
||||
import { css } from '@codemirror/lang-css';
|
||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { autocompleteEmoji } from './autocompleteEmoji.js';
|
||||
import { autocompleteEmoji } from './extensions/autocompleteEmoji.js';
|
||||
import { searchKeymap, search } from '@codemirror/search';
|
||||
import { closeBrackets } from '@codemirror/autocomplete';
|
||||
|
||||
@@ -37,69 +43,13 @@ const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery };
|
||||
const themeCompartment = new Compartment();
|
||||
const highlightCompartment = new Compartment();
|
||||
|
||||
import { generalKeymap, markdownKeymap, cssKeymap, formatCSS } from './customKeyMaps.js';
|
||||
import foldOnPages from './customFolding.js';
|
||||
import { customHighlightStyle, tokenizeCustomMarkdown, tokenizeCustomCSS } from './customHighlight.js';
|
||||
import { legacyCustomHighlightStyle, legacyTokenizeCustomMarkdown } from './legacyCustomHighlight.js';
|
||||
import { generalKeymap, markdownKeymap, cssKeymap, formatCSS } from './extensions/customKeyMaps.js';
|
||||
import foldOnPages from './extensions/customFolding.js';
|
||||
import { customHighlightStyle } from './extensions/customHighlight.js';
|
||||
import { legacyCustomHighlightStyle } from './extensions/legacyCustomHighlight.js';
|
||||
|
||||
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
||||
|
||||
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;
|
||||
|
||||
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': snippetCount } }).range(line.from));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
decos.sort((a, b)=>a.from - b.from || a.to - b.to);
|
||||
return Decoration.set(decos);
|
||||
}
|
||||
},
|
||||
{ decorations: (v)=>v.decorations }
|
||||
);
|
||||
};
|
||||
|
||||
const setProgrammaticCursorLine = StateEffect.define();
|
||||
|
||||
const programmaticCursorLineField = StateField.define({
|
||||
@@ -206,8 +156,6 @@ const CodeEditor = forwardRef(
|
||||
? 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] : themes[editorTheme] || themes['default'];
|
||||
|
||||
@@ -230,7 +178,7 @@ const CodeEditor = forwardRef(
|
||||
}),
|
||||
|
||||
//highlights
|
||||
highlightCompartment.of([customHighlightPlugin, highlightExtension]),
|
||||
highlightCompartment.of([customHighlightPlugin(renderer, tab), highlightExtension]),
|
||||
themeCompartment.of(themeExtension),
|
||||
highlightActiveLine(),
|
||||
highlightActiveLineGutter(),
|
||||
@@ -371,10 +319,8 @@ const CodeEditor = forwardRef(
|
||||
? syntaxHighlighting(customHighlightStyle)
|
||||
: syntaxHighlighting(legacyCustomHighlightStyle);
|
||||
|
||||
const customHighlightPlugin = createHighlightPlugin(renderer, tab);
|
||||
|
||||
view.dispatch({
|
||||
effects : highlightCompartment.reconfigure([customHighlightPlugin, highlightExtension]),
|
||||
effects : highlightCompartment.reconfigure([customHighlightPlugin(renderer, tab), highlightExtension]),
|
||||
});
|
||||
}, [renderer, tab]);
|
||||
|
||||
@@ -383,7 +329,6 @@ const CodeEditor = forwardRef(
|
||||
injectText : (text)=>{
|
||||
const view = viewRef.current;
|
||||
|
||||
|
||||
view.dispatch(
|
||||
view.state.replaceSelection(text)
|
||||
);
|
||||
|
||||
@@ -92,7 +92,8 @@
|
||||
&.term { color : rgb(96, 117, 143); }
|
||||
&.definition { color : rgb(97, 57, 178); }
|
||||
}
|
||||
.cm-block:not(.cm-comment) {
|
||||
.cm-block:not(.cm-comment),
|
||||
.cm-block:not(.cm-comment) * {
|
||||
font-weight : bold;
|
||||
color : purple;
|
||||
}
|
||||
@@ -190,6 +191,33 @@
|
||||
outline : 1px inset #00000055 !important;
|
||||
}
|
||||
|
||||
.cm-image {
|
||||
position:relative;
|
||||
|
||||
.cm-preview {
|
||||
object-fit: contain;
|
||||
position:absolute;
|
||||
bottom:0;
|
||||
left:0;
|
||||
height:200px;
|
||||
width:200px;
|
||||
height:200px;
|
||||
padding:5px;
|
||||
background-color: #fff;
|
||||
border-radius:10px;
|
||||
border:3px solid grey;
|
||||
pointer-events: none;
|
||||
opacity:0;
|
||||
transition:0.2s opacity 0.5s;
|
||||
translate:0 100%;
|
||||
z-index:1000;
|
||||
}
|
||||
|
||||
&:hover .cm-preview {
|
||||
opacity:1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tab character visualization (optional) */
|
||||
//.cm-tab {
|
||||
// background: url(...) no-repeat right;
|
||||
|
||||
+166
-5
@@ -1,5 +1,15 @@
|
||||
/* eslint max-lines: ["error", { "max": 500 }] */
|
||||
import { HighlightStyle } from '@codemirror/language';
|
||||
import { tags } from '@lezer/highlight';
|
||||
import { legacyTokenizeCustomMarkdown } from './legacyCustomHighlight';
|
||||
import {
|
||||
Decoration,
|
||||
ViewPlugin,
|
||||
} from '@codemirror/view';
|
||||
import {
|
||||
syntaxTree,
|
||||
ensureSyntaxTree
|
||||
} from '@codemirror/language';
|
||||
|
||||
// Making the tokens
|
||||
const customTags = {
|
||||
@@ -23,7 +33,7 @@ const customTags = {
|
||||
variable : 'variable',
|
||||
};
|
||||
|
||||
export function tokenizeCustomMarkdown(text) {
|
||||
function tokenizeCustomMarkdown(text) {
|
||||
const tokens = [];
|
||||
const lines = text.split('\n');
|
||||
|
||||
@@ -149,7 +159,6 @@ export function tokenizeCustomMarkdown(text) {
|
||||
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++) {
|
||||
@@ -158,7 +167,6 @@ export function tokenizeCustomMarkdown(text) {
|
||||
const defMatch = /^(::)(.+)$/.exec(nextLine);
|
||||
if(!onlyColonsMatch && defMatch) {
|
||||
defs.push({ colons: defMatch[1], desc: defMatch[2], line: i });
|
||||
endLine = i;
|
||||
} else break;
|
||||
}
|
||||
|
||||
@@ -202,6 +210,7 @@ export function tokenizeCustomMarkdown(text) {
|
||||
if(lineText.includes('{') && lineText.includes('}')) {
|
||||
const injectionRegex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gmd;
|
||||
let match;
|
||||
|
||||
while ((match = injectionRegex.exec(lineText)) !== null) {
|
||||
tokens.push({
|
||||
line : lineNumber,
|
||||
@@ -241,14 +250,21 @@ export function tokenizeCustomMarkdown(text) {
|
||||
/^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *$|^ *}}$/,
|
||||
);
|
||||
if(match) endCh = match.index + match[0].length;
|
||||
tokens.push({ line: lineNumber, type: customTags.block });
|
||||
const closingMatch = lineText.match(/ *(}})/d);
|
||||
|
||||
if(closingMatch) {
|
||||
tokens.push({ line: lineNumber, from: closingMatch.indices[1][0], to: closingMatch.indices[1][1], type: customTags.block });
|
||||
} else {
|
||||
tokens.push({ line: lineNumber, type: customTags.block });
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export function tokenizeCustomCSS(text) {
|
||||
function tokenizeCustomCSS(text) {
|
||||
const tokens = [];
|
||||
const lines = text.split('\n');
|
||||
|
||||
@@ -307,5 +323,150 @@ export const customHighlightStyle = HighlightStyle.define([
|
||||
|
||||
]);
|
||||
|
||||
function getUrl(node, doc) {
|
||||
let url = null;
|
||||
|
||||
const cursor = node.node.cursor();
|
||||
|
||||
if(cursor.firstChild()) {
|
||||
do {
|
||||
if(cursor.name === 'URL') {
|
||||
url = doc.sliceString(cursor.from, cursor.to);
|
||||
break;
|
||||
}
|
||||
} while (cursor.nextSibling());
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
import { WidgetType } from '@codemirror/view';
|
||||
|
||||
class ImageWidget extends WidgetType {
|
||||
constructor(url) {
|
||||
super();
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
const img = document.createElement('img');
|
||||
img.loading = "lazy";
|
||||
img.className = 'cm-preview';
|
||||
img.src = this.url;
|
||||
|
||||
|
||||
img.onerror = ()=>{
|
||||
img.src = 'client/icons/broken-image.jpg';
|
||||
};
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
eq(other) {
|
||||
return other.url === this.url;
|
||||
}
|
||||
}
|
||||
|
||||
export function customHighlightPlugin(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;
|
||||
|
||||
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;
|
||||
|
||||
const tree = ensureSyntaxTree(view.state, view.state.doc.length, 50) || syntaxTree(view.state);
|
||||
tree.iterate({
|
||||
enter : (node)=>{
|
||||
if(node.name === 'Image') {
|
||||
const url = getUrl(node, view.state.doc);
|
||||
|
||||
const widgetPosition = node.node.lastChild.from;
|
||||
//this is not exactly standard, but should hold,
|
||||
//and is the shortest way i could find of positioning
|
||||
//the image inside the cm-image node
|
||||
|
||||
if(!url) return;
|
||||
|
||||
decos.push(
|
||||
Decoration.mark({
|
||||
class : 'cm-image'
|
||||
}).range(node.from, node.to)
|
||||
);
|
||||
decos.push(
|
||||
Decoration.widget({
|
||||
widget : new ImageWidget(url),
|
||||
side : 1
|
||||
}).range(widgetPosition)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tokens.forEach((token)=>{
|
||||
const line = view.state.doc.line(token.line + 1);
|
||||
|
||||
if(token.from != null && token.to != null && token.from < token.to) {
|
||||
const from = line.from + token.from;
|
||||
const to = line.from + token.to;
|
||||
|
||||
const attrs = {};
|
||||
if(token.type === 'Image' && token.url) {
|
||||
|
||||
attrs['data-url'] = token.url;
|
||||
}
|
||||
|
||||
decos.push(
|
||||
Decoration.mark({
|
||||
class : `cm-${token.type}`,
|
||||
...(Object.keys(attrs).length
|
||||
? { attributes: attrs }
|
||||
: {})
|
||||
}).range(from, to)
|
||||
);
|
||||
} else {
|
||||
decos.push(
|
||||
Decoration.line({
|
||||
class : `cm-${token.type}`
|
||||
}).range(line.from)
|
||||
);
|
||||
if(token.type === 'pageLine' && tab === 'brewText') {
|
||||
pageCount++;
|
||||
if(line.from === 0) pageCount--;
|
||||
decos.push(Decoration.line({ attributes: { 'data-page-number': pageCount } }).range(line.from));
|
||||
}
|
||||
if(token.type === 'snippetLine' && tab === 'brewSnippets') {
|
||||
snippetCount++;
|
||||
decos.push(Decoration.line({ attributes: { 'data-page-number': snippetCount } }).range(line.from));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
decos.sort((a, b)=>a.from - b.from || a.to - b.to);
|
||||
return Decoration.set(decos);
|
||||
}
|
||||
},
|
||||
{ decorations: (v)=>v.decorations }
|
||||
);
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Reference in New Issue
Block a user