0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-08-06 04:37:37 +00:00

Merge branch 'master' into Better-Dropdowns

This commit is contained in:
Gazook89
2026-05-27 22:22:34 -05:00
34 changed files with 1304 additions and 1113 deletions
@@ -1,71 +1,72 @@
import React from 'react';
import createReactClass from 'create-react-class';
import React, { useState } from 'react';
import request from 'superagent';
const BrewCleanup = createReactClass({
displayName : 'BrewCleanup',
getDefaultProps(){
return {};
},
getInitialState() {
return {
count : 0,
const BrewCleanup = ({})=>{
const [count, setCount] = useState(0);
const [pending, setPending] = useState(false);
const [primed, setPrimed] = useState(false);
const [error, setError] = useState(null);
pending : false,
primed : false,
err : null
};
},
prime(){
this.setState({ pending: true });
const prime = async ()=>{
setPending(true);
request.get('/admin/cleanup')
.then((res)=>this.setState({ count: res.body.count, primed: true }))
.catch((err)=>this.setState({ error: err }))
.finally(()=>this.setState({ pending: false }));
},
cleanup(){
this.setState({ pending: true });
try {
const res = await request.get('/admin/cleanup');
request.post('/admin/cleanup')
.then((res)=>this.setState({ count: res.body.count }))
.catch((err)=>this.setState({ error: err }))
.finally(()=>this.setState({ pending: false, primed: false }));
},
renderPrimed(){
if(!this.state.primed) return;
if(!this.state.count){
return <div className='result noBrews'>No Matching Brews found.</div>;
setCount(res.body.count);
setPrimed(true);
} catch (err) {
setError(err);
} finally {
setPending(false);
}
};
const cleanup = async ()=>{
setPending(true);
try {
const res = await request.post('/admin/cleanup');
setCount(res.body.count);
} catch (err) {
setError(err);
} finally {
setPending(false);
setPrimed(false);
}
};
const renderPrimed = ()=>{
if(!primed) return;
if(!count) return <div className='result noBrews'>No Matching Brews found.</div>;
return <div className='result'>
<button onClick={this.cleanup} className='remove'>
{this.state.pending
<button onClick={()=>cleanup()} className='remove'>
{pending
? <i className='fas fa-spin fa-spinner' />
: <span><i className='fas fa-times' /> Remove</span>
}
</button>
<span>Found {this.state.count} Brews that could be removed. </span>
<span>Found {count} Brews that could be removed. </span>
</div>;
},
render(){
return <div className='brewUtil brewCleanup'>
<h2> Brew Cleanup </h2>
<p>Removes very short brews to tidy up the database</p>
};
<button onClick={this.prime} className='query'>
{this.state.pending
? <i className='fas fa-spin fa-spinner' />
: 'Query Brews'
}
</button>
{this.renderPrimed()}
return <div className='brewUtil brewCleanup'>
<h2> Brew Cleanup </h2>
<p>Removes very short brews to tidy up the database</p>
{this.state.error
&& <div className='error noBrews'>{this.state.error.toString()}</div>
<button onClick={()=>prime()} className='query'>
{pending
? <i className='fas fa-spin fa-spinner' />
: 'Query Brews'
}
</div>;
}
});
</button>
{renderPrimed()}
{error && <div className='error noBrews'>{error.toString()}</div>}
</div>;
};
export default BrewCleanup;
+51 -78
View File
@@ -1,4 +1,4 @@
/* eslint max-lines: ["error", { "max": 400 }] */
/* eslint max-lines: ["error", { "max": 405 }] */
import './codeEditor.less';
import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
@@ -10,21 +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, syntaxHighlighting } from '@codemirror/language';
import { foldEffect } 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';
@@ -38,71 +43,13 @@ const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery };
const themeCompartment = new Compartment();
const highlightCompartment = new Compartment();
console.log(themes);
import { generalKeymap, markdownKeymap } from './customKeyMaps.js';
import foldOnPages from './customFolding.js';
import { customHighlightStyle, tokenizeCustomMarkdown, tokenizeCustomCSS } from './customHighlight.js';
import { legacyCustomHighlightStyle, legacyTokenizeCustomMarkdown } from './legacyCustomHighlight.js';
import { generalKeymap, markdownKeymap } from './extensions/customKeyMaps.js';
import foldOnPages from './extensions/customFolding.js';
import { customHighlightPlugin, 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({
@@ -151,11 +98,14 @@ const CodeEditor = forwardRef(
const editorRef = useRef(null);
const viewRef = useRef(null);
const docsRef = useRef({});
const tabRef = useRef(tab);
const prevTabRef = useRef(tab);
const scrollRef = useRef({});
const foldsRef = useRef({});
const pageMap = useRef([]);
const recomputePages = (doc)=>{
if(tab !== 'brewText') return;
const pages = [0];
const text = doc.toString();
let offset = 0;
@@ -181,6 +131,14 @@ const CodeEditor = forwardRef(
return page;
};
const getFoldRanges = (state)=>{
const folds = [];
state.field(foldState, false)?.between(0, state.doc.length, (from, to)=>{
folds.push({ from, to });
});
return folds;
};
const createExtensions = ({ onChange, language, editorTheme })=>{
const setEventListeners = EditorView.updateListener.of((update)=>{
if(update.docChanged) {
@@ -198,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'];
@@ -222,7 +178,7 @@ const CodeEditor = forwardRef(
}),
//highlights
highlightCompartment.of([customHighlightPlugin, highlightExtension]),
highlightCompartment.of([customHighlightPlugin(renderer, tab), highlightExtension]),
themeCompartment.of(themeExtension),
highlightActiveLine(),
highlightActiveLineGutter(),
@@ -267,11 +223,10 @@ const CodeEditor = forwardRef(
ticking = true;
requestAnimationFrame(()=>{
const top = view.scrollDOM.scrollTop;
scrollRef.current[tabRef.current] = top;
const block = view.lineBlockAtHeight(top);
const page = findPageFromPos(block.from); // CHANGED
const page = findPageFromPos(block.from);
onViewChange(page);
ticking = false;
});
};
@@ -286,12 +241,23 @@ const CodeEditor = forwardRef(
};
}, []);
const restoreFolds = (view, folds)=>{
if(!folds?.length) return;
view.dispatch({
effects : folds.map((f)=>foldEffect.of(f))
});
};
useEffect(()=>{
const view = viewRef.current;
if(!view) return;
tabRef.current = tab;
const prevTab = prevTabRef.current;
foldsRef.current[prevTab] = getFoldRanges(view.state);
if(prevTab !== tab) {
docsRef.current[prevTab] = view.state;
@@ -305,6 +271,16 @@ const CodeEditor = forwardRef(
}
view.setState(nextState);
restoreFolds(view, foldsRef.current[tab]);
const savedScroll = scrollRef.current[tab];
if(savedScroll != null) {
requestAnimationFrame(()=>{
view.scrollDOM.scrollTop = savedScroll;
});
}
prevTabRef.current = tab;
}
view.focus();
@@ -343,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]);
@@ -355,7 +329,6 @@ const CodeEditor = forwardRef(
injectText : (text)=>{
const view = viewRef.current;
view.dispatch(
view.state.replaceSelection(text)
);
+33 -1
View File
@@ -63,7 +63,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;
}
@@ -100,6 +101,10 @@
vertical-align : sub;
color : rgb(123, 123, 15);
}
.cm-strikethrough {
text-decoration: line-through;
}
.cm-definitionList {
.cm-definitionTerm { color : rgb(96, 117, 143); }
.cm-definitionColon:not(:has(.cm-comment)) {
@@ -157,6 +162,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;
@@ -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 = {
@@ -16,13 +26,14 @@ const customTags = {
definitionTerm : 'definitionTerm', // .cm-definitionTerm
definitionDesc : 'definitionDesc', // .cm-definitionDesc
definitionColon : 'definitionColon', // .cm-definitionColon
strikethrough : 'strikethrough', // .cm-strikethrough
//CSS
variable : 'variable',
};
export function tokenizeCustomMarkdown(text) {
function tokenizeCustomMarkdown(text) {
const tokens = [];
const lines = text.split('\n');
@@ -81,6 +92,23 @@ export function tokenizeCustomMarkdown(text) {
}
}
// --- Strikethrough ---
if(/\~/.test(lineText)) {
const strikethroughRegex = /~(?!\s)(.+?)(?<!\s)~/g;
const match = strikethroughRegex.exec(lineText);
const type = customTags.strikethrough;
if(match) {
tokens.push({
line : lineNumber,
type,
from : match.index,
to : match.index + match[0].length,
});
}
}
// --- single line def list ---
const singleLineRegex = /^(?=.*[^:])(.+?)(\s*)(::)([^\n]*)$/dmy;
const match = singleLineRegex.exec(lineText);
@@ -131,20 +159,18 @@ 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++) {
const nextLine = lines[i];
const onlyColonsMatch = /^:*$/.test(nextLine);
const defMatch = /^(::)(.*\S.*)?\s*$/.exec(nextLine);
const defMatch = /^(::)(.+)$/.exec(nextLine);
if(!onlyColonsMatch && defMatch) {
defs.push({ colons: defMatch[1], desc: defMatch[2], line: i });
endLine = i;
} else break;
}
if(defs.length > 0) {
if(defs.length > 0 && lineText.trim().length > 0) {
tokens.push({
line : startLine,
type : customTags.definitionList,
@@ -175,20 +201,21 @@ export function tokenizeCustomMarkdown(text) {
line : d.line,
type : customTags.definitionDesc,
from : d.colons.length,
to : d.colons.length + d.desc.length,
to : d.colons.length + d.desc?.length,
});
});
}
}
if(lineText.includes('{') && lineText.includes('}')) {
const injectionRegex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gm;
const injectionRegex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gmd;
let match;
while ((match = injectionRegex.exec(lineText)) !== null) {
tokens.push({
line : lineNumber,
from : match.index,
to : match.index + match[1].length,
from : match.indices[1][0],
to : match.indices[1][1],
type : customTags.injection,
});
}
@@ -223,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');
@@ -289,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 }
);
};
@@ -3,6 +3,17 @@ import { keymap } from '@codemirror/view';
import { undo, redo, indentMore, deleteLine } from '@codemirror/commands';
import { Prec } from '@codemirror/state';
const insertTab = (view)=>{
const { from, to } = view.state.selection.main;
view.dispatch({
changes : { from, to, insert: ' ' },
selection : { anchor: from + 2 }
});
return true;
};
const indentLess = (view)=>{
const { from, to } = view.state.selection.main;
const lines = [];
@@ -17,147 +28,46 @@ const indentLess = (view)=>{
return true;
};
const makeBold = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const wrapSelection = (prefix, suffix)=>(view)=>{
const changes = [];
let text, cursor;
for (const range of view.state.selection.ranges) {
const { from, to } = range;
const selected = view.state.doc.sliceString(from, to);
if(from === to) {
text = '****';
cursor = from + 2;
} else if(selected.startsWith('**') && selected.endsWith('**')) {
text = selected.slice(2, -2);
cursor = from + text.length;
} else {
text = `**${selected}**`;
cursor = from + text.length;
let text;
if(from === to) { text = prefix + suffix; } else if(selected.startsWith(prefix) && selected.endsWith(suffix)) {
text = selected.slice(prefix.length, -suffix.length);
} else {text = `${prefix}${selected}${suffix}`;}
changes.push({ from, to, insert: text });
}
view.dispatch({
changes : { from, to, insert: text },
selection : { anchor: cursor },
changes
});
return true;
};
const makeItalic = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const makeNbsp = (view)=>{
const { from } = view.state.selection.main;
let text, cursor;
const prev2 = from >= 2
? view.state.doc.sliceString(from - 2, from)
: '';
if(from === to) {
text = '**';
cursor = from + 1;
} else if(selected.startsWith('*') && selected.endsWith('*')) {
text = selected.slice(2, -2);
cursor = from + text.length;
} else {
text = `*${selected}*`;
cursor = from + text.length;
}
const insert = (prev2 === ':>' || prev2 === '>>') ? '>' : ':>';
view.dispatch({
changes : { from, to, insert: text },
selection : { anchor: cursor },
changes : { from, to: from, insert },
selection : { anchor: from + insert.length },
});
return true;
};
const makeUnderline = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
let text, cursor;
if(from === to) {
text = '<u></u>';
cursor = from + 3;
} else if(selected.startsWith('<u>') && selected.endsWith('</u>')) {
text = selected.slice(3, -4);
cursor = from + text.length;
} else {
text = `<u>${selected}</u>`;
cursor = from + text.length;
}
view.dispatch({
changes : { from, to, insert: text },
selection : { anchor: cursor },
});
return true;
};
const makeSuper = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
let text, cursor;
if(from === to) {
text = '^^';
cursor = from + 1;
} else if(selected.startsWith('^') && selected.endsWith('^')) {
text = selected.slice(1, -1);
cursor = from + text.length;
} else {
text = `^${selected}^`;
cursor = from + text.length;
}
view.dispatch({
changes : { from, to, insert: text },
selection : { anchor: cursor },
});
return true;
};
const makeSub = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
let text, cursor;
if(from === to) {
text = '^^^^';
cursor = from + 2;
} else if(selected.startsWith('^^') && selected.endsWith('^^')) {
text = selected.slice(2, -2);
cursor = from + text.length;
} else {
text = `^^${selected}^^`;
cursor = from + text.length;
}
view.dispatch({
changes : { from, to, insert: text },
selection : { anchor: cursor },
});
return true;
};
const makeNbsp = (view) => {
const { from } = view.state.selection.main;
const prev2 = from >= 2
? view.state.doc.sliceString(from - 2, from)
: '';
const insert = (prev2 === ':>' || prev2 === '>>') ? '>' : ':>';
view.dispatch({
changes: { from, to: from, insert },
selection: { anchor: from + insert.length },
});
return true;
};
const makeSpace = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
@@ -259,7 +169,7 @@ const newPage = (view)=>{
};
export const generalKeymap = Prec.high(keymap.of([
{ key: 'Tab', run: indentMore },
{ key: 'Tab', run: insertTab },
{ key: 'Mod-z', run: undo }, //i think it may be unnecessary
{ key: 'Mod-Shift-z', run: redo },
{ key: 'Mod-y', run: redo },
@@ -268,27 +178,27 @@ export const generalKeymap = Prec.high(keymap.of([
export const markdownKeymap = Prec.highest(keymap.of([
//{ 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-Tab', run: indentLess },
{ key: 'Mod-b', run: wrapSelection('**', '**') }, // makeBold
{ key: 'Mod-i', run: wrapSelection('*', '*') }, // makeItalic
{ key: 'Mod-u', run: wrapSelection('<u>', '</u>') }, // makeUnderline
{ key: 'Shift-Mod-=', run: wrapSelection('^', '^') }, // makeSuper
{ key: 'Mod-=', run: wrapSelection('^^', '^^') }, // 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: 'Mod-Enter', run: newPage },
{ key: 'Shift-Mod-Enter', run: newColumn },
{ key: 'Mod-Enter', run: newPage },
]));
+11 -4
View File
@@ -29,7 +29,6 @@ const TOOLBAR_STATE_KEY = 'HB_renderer_toolbarState';
const INITIAL_CONTENT = dedent`
<!DOCTYPE html><html><head>
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
<link href="${brewRendererStylesUrl}" rel="stylesheet" />
<link href="${headerNavStylesUrl}" rel="stylesheet" />
@@ -42,6 +41,7 @@ const BrewPage = (props)=>{
props = {
contents : '',
index : 0,
hoisted : false,
...props
};
const pageRef = useRef(null);
@@ -221,7 +221,8 @@ const BrewRenderer = (props)=>{
}
};
const renderPages = ()=>{
const renderPages = (checkHoists = false)=>{
if(props.errors && props.errors.length)
return renderedPages;
@@ -233,10 +234,16 @@ const BrewRenderer = (props)=>{
renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1);
_.forEach(rawPages, (page, index)=>{
if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){
const varsOnPageRegex = /([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g; // Find out if there are any vars on the page.
const forceRender = checkHoists &&
!props.hoisted &&
(page.match(varsOnPageRegex)); // forceRender forces pages outside of the PPR range to render if true.
// This is necessary on the first load to fully populate the variable table.
if((isInView(index) || !renderedPages[index] || forceRender) && typeof window !== 'undefined'){
renderedPages[index] = renderPage(page, index); // Render any page not yet rendered, but only re-render those in PPR range
}
});
if(!props.hoisted) { props.hoisted = true; } // Only fully hoist once.
return renderedPages;
};
@@ -276,7 +283,7 @@ const BrewRenderer = (props)=>{
window.addEventListener('hashchange', ()=>scrollToHash(window.location.hash));
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
renderPages(); //Make sure page is renderable before showing
renderPages(true); //Make sure page is renderable before showing
setState((prevState)=>({
...prevState,
isMounted : true,
@@ -31,18 +31,17 @@ import cm5Themes from 'codemirror-5-themes';
const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery };
const themeNames = Object.entries(themes)
.filter(([name, value]) =>
Array.isArray(value) &&
.filter(([name, value])=>Array.isArray(value) &&
!name.endsWith('Init') &&
!name.endsWith('Style')
)
.map(([name]) => name);
.map(([name])=>name);
const EditorThemes = [
'default',
...themeNames
.filter(name => name !== 'default')
.sort((a, b) => a.localeCompare(b))
'default',
...themeNames
.filter((name)=>name !== 'default')
.sort((a, b)=>a.localeCompare(b))
];
const execute = function(val, props){
@@ -171,7 +170,7 @@ const Snippetbar = createReactClass({
this.props.updateEditorTheme(e.target.value);
this.setState({
showThemeSelector : false,
themeSelector : false,
});
},
+18 -2
View File
@@ -1,9 +1,25 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import Nav from './nav.jsx';
import { printCurrentBrew } from '@shared/helpers.js';
export default function(){
const [printing, setPrinting] = useState(false);
// listen for print cycle events to display "loading" message since it can take some time.
useEffect(()=>{
document.addEventListener('print:startprep', handlePrintStartPrep);
document.addEventListener('print:finishedprep', handlePrintPrepFinished);
return ()=>{
document.removeEventListener('print:startprep', handlePrintStartPrep);
document.removeEventListener('print:finishedprep', handlePrintPrepFinished);
}
}, []);
const handlePrintStartPrep = ()=>{ setPrinting(true); };
const handlePrintPrepFinished = ()=>{ setPrinting(false); };
return <Nav.item onClick={printCurrentBrew} color='purple' icon='far fa-file-pdf'>
get PDF
{printing ? 'loading' : 'get PDF'}
</Nav.item>;
};
+7 -1
View File
@@ -17,7 +17,7 @@ const getRedditLink = (brew)=>{
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(brew.title.toWellFormed())}&text=${encodeURIComponent(text)}`;
};
export default ({ brew })=>(
export default ({ brew, currentPage })=>(
<Nav.dropdown>
<Nav.item color='teal' icon='fas fa-share-alt'>
share
@@ -28,6 +28,12 @@ export default ({ brew })=>(
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${getShareId(brew)}`);}}>
copy url
</Nav.item>
{currentPage > 1 &&
<Nav.item
color='blue'
onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${getShareId(brew)}#p${currentPage}`);}}>
copy url (page {currentPage})
</Nav.item>}
<Nav.item color='blue' href={getRedditLink(brew)} newTab rel='noopener noreferrer'>
post to reddit
</Nav.item>
+9 -11
View File
@@ -90,7 +90,7 @@ const EditPage = (props)=>{
const handleControlKeys = (e)=>{
if(!(e.ctrlKey || e.metaKey)) return;
if(e.keyCode === 83) trySaveRef.current(true);
if(e.keyCode === 83) trySaveRef.current(true, true, saveGoogle);
if(e.keyCode === 80) printCurrentBrew();
if([83, 80].includes(e.keyCode)) {
e.stopPropagation();
@@ -118,13 +118,9 @@ const EditPage = (props)=>{
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
setUnsavedChanges(hasChange);
if(autoSaveEnabled) trySave(false, hasChange);
if(autoSaveEnabled) trySave(false, hasChange, saveGoogle);
}, [currentBrew]);
useEffect(()=>{
trySave(true);
}, [saveGoogle]);
const handleSplitMove = ()=>{
editorRef.current?.update();
};
@@ -183,11 +179,13 @@ const EditPage = (props)=>{
};
const toggleGoogleStorage = ()=>{
const newSaveGoogle = !saveGoogle;
setSaveGoogle((prev)=>!prev);
setError(null);
trySave(true, true, newSaveGoogle);
};
const trySave = (immediate = false, hasChanges = true)=>{
const trySave = (immediate = false, hasChanges = true, saveToGoogle = false)=>{
clearTimeout(saveTimeout.current);
if(isSaving) return;
if(!hasChanges && !immediate) return;
@@ -196,7 +194,7 @@ const EditPage = (props)=>{
saveTimeout.current = setTimeout(async ()=>{
setIsSaving(true);
setError(null);
await save(currentBrew, saveGoogle)
await save(currentBrew, saveToGoogle)
.catch((err)=>{
setError(err);
});
@@ -216,7 +214,7 @@ const EditPage = (props)=>{
const brewToSave = {
...brew,
text : brew.text.normalize('NFC'),
pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1,
pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/gm)) || []).length + 1,
patches : stringifyPatches(makePatches(encodeURI(lastSavedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))),
hash : await md5(lastSavedBrew.current.text.normalize('NFC')),
textBin : undefined,
@@ -314,7 +312,7 @@ const EditPage = (props)=>{
// #3 - Unsaved changes exist, click to save, show SAVE NOW
if(unsavedChanges)
return <Nav.item className='save' onClick={()=>trySave(true)} color='blue' icon='fas fa-save'>save now</Nav.item>;
return <Nav.item className='save' onClick={()=>trySave(true, true, saveGoogle)} color='blue' icon='fas fa-save'>save now</Nav.item>;
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
if(autoSaveEnabled)
@@ -365,7 +363,7 @@ const EditPage = (props)=>{
<PrintNavItem />
<HelpNavItem />
<VaultNavItem />
<ShareNavItem brew={currentBrew} />
<ShareNavItem brew={currentBrew} currentPage={currentBrewRendererPageNum} />
<RecentNavItem brew={currentBrew} storageKey='edit' />
<AccountNavItem/>
</Nav.section>
+1 -1
View File
@@ -156,7 +156,7 @@ const NewPage = (props)=>{
const updatedBrew = { ...currentBrew };
splitTextStyleAndMetadata(updatedBrew);
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm;
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/gm;
updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1;
const res = await request
@@ -92,6 +92,19 @@ const SharePage = (props)=>{
<Nav.item color='blue' icon='fas fa-clone' href={`/new/${processShareId()}`}>
clone to new
</Nav.item>
<Nav.item
color='blue'
icon='fas fa-link'
onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${processShareId()}`);}}>
copy url
</Nav.item>
{currentBrewRendererPageNum > 1 &&
<Nav.item
color='blue'
icon='fas fa-hashtag'
onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${processShareId()}#p${currentBrewRendererPageNum}`);}}>
copy url (page {currentBrewRendererPageNum})
</Nav.item>}
</Nav.dropdown>
</>
)}
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB