mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-05-07 18:48:39 +00:00
Merge branch 'master' of https://github.com/naturalcrit/homebrewery into add-cm-features
This commit is contained in:
@@ -66,10 +66,6 @@ updates:
|
|||||||
- dependency-name: "@babel/preset-react"
|
- dependency-name: "@babel/preset-react"
|
||||||
versions:
|
versions:
|
||||||
- 7.13.13
|
- 7.13.13
|
||||||
- dependency-name: codemirror
|
|
||||||
versions:
|
|
||||||
- 5.59.3
|
|
||||||
- 5.60.0
|
|
||||||
- dependency-name: classnames
|
- dependency-name: classnames
|
||||||
versions:
|
versions:
|
||||||
- 2.3.0
|
- 2.3.0
|
||||||
|
|||||||
+29
-3
@@ -85,14 +85,40 @@ pre {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page .df {
|
.page .df {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## changelog
|
## changelog
|
||||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||||
|
|
||||||
|
### Saturday 4/04/2026 - v3.21.0
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### Gazook89
|
||||||
|
* [x] Allow custom {{openSans **:fas_table_list: SNIPPETS**}} to be inserted mid-line
|
||||||
|
|
||||||
|
##### abquintic
|
||||||
|
* [x] Move example snippet images out of imgur (for folks without imgur access)
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
* [x] Add auto-suggest to tag entry input box
|
||||||
|
* [x] Replace all example artwork with
|
||||||
|
* [x] Added tooltips to the {{openSans :fas_circle_info: **Properties**}} menu
|
||||||
|
* [x] Removed {{openSans **SYSTEMS**}} checkboxes from {{openSans :fas_circle_info: **Properties**}} menu; instead {{openSans **TAGS**}} should be used for this purpose
|
||||||
|
* [x] Replace all AI-generated art with public domain art
|
||||||
|
* [x] Major backend refactor to use Vite
|
||||||
|
|
||||||
|
##### A1Asriel (new contributor!)
|
||||||
|
* [x] Add fix for column breaks on Firefox
|
||||||
|
|
||||||
|
Fixes issues [#543](https://github.com/naturalcrit/homebrewery/issues/543), [#2473](https://github.com/naturalcrit/homebrewery/issues/2473), [#3712](https://github.com/naturalcrit/homebrewery/issues/3712)
|
||||||
|
|
||||||
|
##### G-Ambatte, abquintic, 5e-Cleric
|
||||||
|
* [x] Multiple other backend fixes and refactors
|
||||||
|
}}
|
||||||
|
|
||||||
### Friday 1/11/2026 - v3.20.1
|
### Friday 1/11/2026 - v3.20.1
|
||||||
|
|
||||||
{{taskList
|
{{taskList
|
||||||
@@ -2358,4 +2384,4 @@ Massive changelog incoming:
|
|||||||
|
|
||||||
* Added `phb.standalone.css` plus a build system for creating it
|
* Added `phb.standalone.css` plus a build system for creating it
|
||||||
* Added page numbers and footer text
|
* Added page numbers and footer text
|
||||||
* Page accent now flips each page
|
* Page accent now flips each page
|
||||||
@@ -13,7 +13,7 @@ const emojis = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const emojiCompletionList = (context)=>{
|
const emojiCompletionList = (context)=>{
|
||||||
const word = context.matchBefore(/:[^\s:]*/);
|
const word = context.matchBefore(/:[^\s:]+/);
|
||||||
if(!word) return null;
|
if(!word) return null;
|
||||||
|
|
||||||
const line = context.state.doc.lineAt(context.pos);
|
const line = context.state.doc.lineAt(context.pos);
|
||||||
@@ -49,8 +49,9 @@ const emojiCompletionList = (context)=>{
|
|||||||
//Info is the tooltip
|
//Info is the tooltip
|
||||||
|
|
||||||
return {
|
return {
|
||||||
from : word.from + 1,
|
from : word.from + 1,
|
||||||
options,
|
options,
|
||||||
|
filter : false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
const autoCloseCurlyBraces = function(CodeMirror, cm, typingClosingBrace) {
|
|
||||||
const ranges = cm.listSelections(), replacements = [];
|
|
||||||
for (let i = 0; i < ranges.length; i++) {
|
|
||||||
if(!ranges[i].empty()) return CodeMirror.Pass;
|
|
||||||
const pos = ranges[i].head, line = cm.getLine(pos.line), tok = cm.getTokenAt(pos);
|
|
||||||
if(!typingClosingBrace && (tok.type == 'string' || tok.string.charAt(0) != '{' || tok.start != pos.ch - 1))
|
|
||||||
return CodeMirror.Pass;
|
|
||||||
else if(typingClosingBrace) {
|
|
||||||
let hasUnclosedBraces = false, index = -1;
|
|
||||||
do {
|
|
||||||
index = line.indexOf('{{', index + 1);
|
|
||||||
if(index !== -1 && line.indexOf('}}', index + 1) === -1) {
|
|
||||||
hasUnclosedBraces = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} while (index !== -1);
|
|
||||||
if(!hasUnclosedBraces) return CodeMirror.Pass;
|
|
||||||
}
|
|
||||||
|
|
||||||
replacements[i] = typingClosingBrace ? {
|
|
||||||
text : '}}',
|
|
||||||
newPos : CodeMirror.Pos(pos.line, pos.ch + 2)
|
|
||||||
} : {
|
|
||||||
text : '{}}',
|
|
||||||
newPos : CodeMirror.Pos(pos.line, pos.ch + 1)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = ranges.length - 1; i >= 0; i--) {
|
|
||||||
const info = replacements[i];
|
|
||||||
cm.replaceRange(info.text, ranges[i].head, ranges[i].anchor, '+insert');
|
|
||||||
const sel = cm.listSelections().slice(0);
|
|
||||||
sel[i] = {
|
|
||||||
head : info.newPos,
|
|
||||||
anchor : info.newPos
|
|
||||||
};
|
|
||||||
cm.setSelections(sel);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
autoCloseCurlyBraces : function(CodeMirror, codeMirror) {
|
|
||||||
const map = { name: 'autoCloseCurlyBraces' };
|
|
||||||
map[`'{'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm); };
|
|
||||||
map[`'}'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm, true); };
|
|
||||||
codeMirror?.addKeyMap(map);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -11,22 +11,21 @@ import {
|
|||||||
scrollPastEnd,
|
scrollPastEnd,
|
||||||
Decoration,
|
Decoration,
|
||||||
ViewPlugin,
|
ViewPlugin,
|
||||||
WidgetType,
|
|
||||||
drawSelection,
|
drawSelection,
|
||||||
dropCursor,
|
dropCursor,
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { EditorState, Compartment } from '@codemirror/state';
|
import { EditorState, Compartment, StateEffect, StateField } from '@codemirror/state';
|
||||||
import { foldAll as foldAllCmd, unfoldAll as unfoldAllCmd, foldGutter, foldKeymap, syntaxHighlighting } from '@codemirror/language';
|
import { foldAll as foldAllCmd, unfoldAll as unfoldAllCmd, foldGutter, foldKeymap, syntaxHighlighting } from '@codemirror/language';
|
||||||
import { defaultKeymap, history, undo, redo, undoDepth, redoDepth } from '@codemirror/commands';
|
import { defaultKeymap, history, undo, redo, undoDepth, redoDepth } from '@codemirror/commands';
|
||||||
import { languages } from '@codemirror/language-data';
|
import { languages } from '@codemirror/language-data';
|
||||||
import { css, cssLanguage } from '@codemirror/lang-css';
|
import { css } from '@codemirror/lang-css';
|
||||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||||
import { html } from '@codemirror/lang-html';
|
import { html } from '@codemirror/lang-html';
|
||||||
import { autocompleteEmoji } from './autocompleteEmoji.js';
|
import { autocompleteEmoji } from './autocompleteEmoji.js';
|
||||||
import { searchKeymap, search } from '@codemirror/search';
|
import { searchKeymap, search } from '@codemirror/search';
|
||||||
import { closeBrackets } from '@codemirror/autocomplete';
|
import { closeBrackets } from '@codemirror/autocomplete';
|
||||||
|
|
||||||
const customClose = closeBrackets({ brackets: ['()', '[]', '{{}}'] });
|
const autoCloseBrackets = closeBrackets({ brackets: ['()', '[]', '{{}}'] });
|
||||||
|
|
||||||
import * as themesImport from '@uiw/codemirror-themes-all';
|
import * as themesImport from '@uiw/codemirror-themes-all';
|
||||||
import defaultCM5Theme from '@themes/codeMirror/default.js';
|
import defaultCM5Theme from '@themes/codeMirror/default.js';
|
||||||
@@ -36,11 +35,13 @@ const themes = { default: defaultCM5Theme, darkbrewery, ...themesImport };
|
|||||||
const themeCompartment = new Compartment();
|
const themeCompartment = new Compartment();
|
||||||
const highlightCompartment = new Compartment();
|
const highlightCompartment = new Compartment();
|
||||||
|
|
||||||
import customKeymap from './customKeyMap.js';
|
import { generalKeymap, markdownKeymap } from './customKeyMaps.js';
|
||||||
import pageFoldExtension from './customFolding.js';
|
import foldOnPages from './customFolding.js';
|
||||||
import { customHighlightStyle, tokenizeCustomMarkdown, tokenizeCustomCSS } from './customHighlight.js';
|
import { customHighlightStyle, tokenizeCustomMarkdown, tokenizeCustomCSS } from './customHighlight.js';
|
||||||
import { legacyCustomHighlightStyle, legacyTokenizeCustomMarkdown } from './legacyCustomHighlight.js';
|
import { legacyCustomHighlightStyle, legacyTokenizeCustomMarkdown } from './legacyCustomHighlight.js';
|
||||||
|
|
||||||
|
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
||||||
|
|
||||||
const createHighlightPlugin = (renderer, tab)=>{
|
const createHighlightPlugin = (renderer, tab)=>{
|
||||||
//this function takes the custom tokens created in the tokenize function in customhighlight files
|
//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
|
//takes the tokens defined by that function and assigns classes to them
|
||||||
@@ -84,7 +85,7 @@ const createHighlightPlugin = (renderer, tab)=>{
|
|||||||
}
|
}
|
||||||
if(tok.type === 'snippetLine' && tab === 'brewSnippets') {
|
if(tok.type === 'snippetLine' && tab === 'brewSnippets') {
|
||||||
snippetCount++;
|
snippetCount++;
|
||||||
decos.push(Decoration.line({ attributes: { 'data-page-number': pageCount } }).range(line.from));
|
decos.push(Decoration.line({ attributes: { 'data-page-number': snippetCount } }).range(line.from));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -97,17 +98,45 @@ const createHighlightPlugin = (renderer, tab)=>{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setProgrammaticCursorLine = StateEffect.define();
|
||||||
|
|
||||||
|
const programmaticCursorLineField = StateField.define({
|
||||||
|
create() {
|
||||||
|
return Decoration.none;
|
||||||
|
},
|
||||||
|
update(decorations, transitionState) {
|
||||||
|
//deco is the decoratiions object
|
||||||
|
//tr is the transition state object, tr.effects is an array of stateEffects
|
||||||
|
//seems to be the easiest way of setting a class programatically only when called
|
||||||
|
for (const effects of transitionState.effects) {
|
||||||
|
if(effects.is(setProgrammaticCursorLine)) {
|
||||||
|
const pos = effects.value;
|
||||||
|
if(pos == null) return Decoration.none;
|
||||||
|
const line = transitionState.state.doc.lineAt(pos);
|
||||||
|
|
||||||
|
return Decoration.set([
|
||||||
|
Decoration.line({
|
||||||
|
class : 'sourceMoveFlash'
|
||||||
|
}).range(line.from)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return decorations;
|
||||||
|
},
|
||||||
|
provide : (decorationSet)=>EditorView.decorations.from(decorationSet)
|
||||||
|
});
|
||||||
|
|
||||||
const CodeEditor = forwardRef(
|
const CodeEditor = forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
language = '',
|
||||||
|
tab = 'brewText',
|
||||||
|
view,
|
||||||
value = '',
|
value = '',
|
||||||
onChange = ()=>{},
|
onChange = ()=>{},
|
||||||
onCursorChange = ()=>{},
|
onCursorChange = ()=>{},
|
||||||
onViewChange = ()=>{},
|
onViewChange = ()=>{},
|
||||||
language = '',
|
|
||||||
tab = 'brewText',
|
|
||||||
editorTheme = 'default',
|
editorTheme = 'default',
|
||||||
view,
|
|
||||||
style,
|
style,
|
||||||
renderer,
|
renderer,
|
||||||
...props
|
...props
|
||||||
@@ -119,22 +148,44 @@ const CodeEditor = forwardRef(
|
|||||||
const docsRef = useRef({});
|
const docsRef = useRef({});
|
||||||
const prevTabRef = useRef(tab);
|
const prevTabRef = useRef(tab);
|
||||||
|
|
||||||
|
const pageMap = useRef([]);
|
||||||
|
|
||||||
|
const recomputePages = (doc)=>{
|
||||||
|
const pages = [0];
|
||||||
|
const text = doc.toString();
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (const line of text.split('\n')) {
|
||||||
|
if(PAGEBREAK_REGEX_V3.test(line)) {
|
||||||
|
pages.push(offset);
|
||||||
|
}
|
||||||
|
offset += line.length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pageMap.current = pages;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findPageFromPos = (pos)=>{
|
||||||
|
const pages = pageMap.current;
|
||||||
|
let page = 1;
|
||||||
|
|
||||||
|
for (let i = 1; i < pages.length; i++) {
|
||||||
|
if(pos >= pages[i]) page = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return page;
|
||||||
|
};
|
||||||
|
|
||||||
const createExtensions = ({ onChange, language, editorTheme })=>{
|
const createExtensions = ({ onChange, language, editorTheme })=>{
|
||||||
const setEventListeners = EditorView.updateListener.of((update)=>{
|
const setEventListeners = EditorView.updateListener.of((update)=>{
|
||||||
if(update.docChanged) {
|
if(update.docChanged) {
|
||||||
|
recomputePages(update.state.doc);
|
||||||
onChange(update.state.doc.toString());
|
onChange(update.state.doc.toString());
|
||||||
}
|
}
|
||||||
if(update.selectionSet) {
|
if(update.selectionSet) {
|
||||||
const pos = update.state.selection.main.head;
|
const pos = update.state.selection.main.head;
|
||||||
const line = update.state.doc.lineAt(pos).number;
|
const page = findPageFromPos(pos);
|
||||||
|
onCursorChange(page);
|
||||||
onCursorChange(line);
|
|
||||||
}
|
|
||||||
if(update.viewportChanged) {
|
|
||||||
const { from } = update.view.viewport;
|
|
||||||
const line = update.state.doc.lineAt(from).number;
|
|
||||||
|
|
||||||
onViewChange(line);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,34 +196,42 @@ const CodeEditor = forwardRef(
|
|||||||
const customHighlightPlugin = createHighlightPlugin(renderer, tab);
|
const customHighlightPlugin = createHighlightPlugin(renderer, tab);
|
||||||
|
|
||||||
const languageExtension = language === 'css' ? css() : [markdown({ base: markdownLanguage, codeLanguages: languages }), html({ autoCloseTags: true })];
|
const languageExtension = language === 'css' ? css() : [markdown({ base: markdownLanguage, codeLanguages: languages }), html({ autoCloseTags: true })];
|
||||||
const themeExtension = Array.isArray(themes[editorTheme]) ? themes[editorTheme] : themes[0];
|
const themeExtension = Array.isArray(themes[editorTheme]) ? themes[editorTheme] : themes[editorTheme] || themes['default'];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
history(), //allows for undo and redo
|
|
||||||
setEventListeners,
|
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
scrollPastEnd(),
|
setEventListeners,
|
||||||
languageExtension,
|
languageExtension,
|
||||||
|
autoCloseBrackets,
|
||||||
lineNumbers(),
|
lineNumbers(),
|
||||||
pageFoldExtension,
|
scrollPastEnd(),
|
||||||
|
search(),
|
||||||
|
history(), //allows for undo and redo
|
||||||
|
...(tab !== 'brewStyles' ? [autocompleteEmoji] : []),
|
||||||
|
|
||||||
|
//folding
|
||||||
|
foldOnPages,
|
||||||
foldGutter({
|
foldGutter({
|
||||||
openText : '▾',
|
openText : '▾',
|
||||||
closedText : '▸'
|
closedText : '▸'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
highlightActiveLine(),
|
//highlights
|
||||||
highlightActiveLineGutter(),
|
|
||||||
highlightCompartment.of([customHighlightPlugin, highlightExtension]),
|
highlightCompartment.of([customHighlightPlugin, highlightExtension]),
|
||||||
themeCompartment.of(themeExtension),
|
themeCompartment.of(themeExtension),
|
||||||
...(tab !== 'brewStyles' ? [autocompleteEmoji] : []),
|
highlightActiveLine(),
|
||||||
search(),
|
highlightActiveLineGutter(),
|
||||||
|
|
||||||
|
//keyboard shortcut
|
||||||
keymap.of([...defaultKeymap, foldKeymap, ...searchKeymap]),
|
keymap.of([...defaultKeymap, foldKeymap, ...searchKeymap]),
|
||||||
customKeymap,
|
generalKeymap,
|
||||||
|
...(tab !== 'brewStyles' ? [markdownKeymap] : []),
|
||||||
|
|
||||||
|
//multiple cursors and selections
|
||||||
drawSelection(),
|
drawSelection(),
|
||||||
EditorState.allowMultipleSelections.of(true),
|
EditorState.allowMultipleSelections.of(true),
|
||||||
customClose,
|
|
||||||
dropCursor(),
|
dropCursor(),
|
||||||
|
programmaticCursorLineField,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -184,14 +243,40 @@ const CodeEditor = forwardRef(
|
|||||||
extensions : createExtensions({ onChange, language, editorTheme }),
|
extensions : createExtensions({ onChange, language, editorTheme }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
recomputePages(state.doc);
|
||||||
|
|
||||||
viewRef.current = new EditorView({
|
viewRef.current = new EditorView({
|
||||||
state,
|
state,
|
||||||
parent : editorRef.current,
|
parent : editorRef.current,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const view = viewRef.current;
|
||||||
|
|
||||||
|
let ticking = false;
|
||||||
|
|
||||||
|
const handleScroll = ()=>{
|
||||||
|
if(ticking) return;
|
||||||
|
|
||||||
|
ticking = true;
|
||||||
|
requestAnimationFrame(()=>{
|
||||||
|
const top = view.scrollDOM.scrollTop;
|
||||||
|
const block = view.lineBlockAtHeight(top);
|
||||||
|
|
||||||
|
const page = findPageFromPos(block.from); // CHANGED
|
||||||
|
onViewChange(page);
|
||||||
|
|
||||||
|
ticking = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
view.scrollDOM.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
docsRef.current[tab] = state;
|
docsRef.current[tab] = state;
|
||||||
|
|
||||||
return ()=>viewRef.current?.destroy();
|
return ()=>{
|
||||||
|
view.scrollDOM.removeEventListener('scroll', handleScroll);
|
||||||
|
viewRef.current?.destroy();
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
@@ -215,6 +300,7 @@ const CodeEditor = forwardRef(
|
|||||||
view.setState(nextState);
|
view.setState(nextState);
|
||||||
prevTabRef.current = tab;
|
prevTabRef.current = tab;
|
||||||
}
|
}
|
||||||
|
view.focus();
|
||||||
}, [tab]);
|
}, [tab]);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
@@ -234,13 +320,15 @@ const CodeEditor = forwardRef(
|
|||||||
const view = viewRef.current;
|
const view = viewRef.current;
|
||||||
if(!view) return;
|
if(!view) return;
|
||||||
|
|
||||||
const themeExtension = Array.isArray(themes[editorTheme]) ? themes[editorTheme] : [];
|
const themeExtension = Array.isArray(themes[editorTheme])? themes[editorTheme]: themes[editorTheme] || themes['default'];
|
||||||
|
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
effects : themeCompartment.reconfigure(themeExtension),
|
effects : themeCompartment.reconfigure(themeExtension),
|
||||||
});
|
});
|
||||||
}, [editorTheme]);
|
}, [editorTheme]);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
|
//rebuild syntax highlight when changing tab or renderer
|
||||||
const view = viewRef.current;
|
const view = viewRef.current;
|
||||||
if(!view) return;
|
if(!view) return;
|
||||||
|
|
||||||
@@ -256,59 +344,36 @@ const CodeEditor = forwardRef(
|
|||||||
}, [renderer, tab]);
|
}, [renderer, tab]);
|
||||||
|
|
||||||
useImperativeHandle(ref, ()=>({
|
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)=>{
|
injectText : (text)=>{
|
||||||
const view = viewRef.current;
|
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.dispatch(
|
||||||
|
view.state.replaceSelection(text)
|
||||||
|
);
|
||||||
view.focus();
|
view.focus();
|
||||||
},
|
},
|
||||||
getCursorPosition : ()=>viewRef.current.state.selection.main.head,
|
getCursorPosition : ()=>viewRef.current.state.selection.main.head,
|
||||||
|
|
||||||
getScrollTop : ()=>viewRef.current.scrollDOM.scrollTop,
|
scrollToPage : (pageNumber, smooth = true)=>{
|
||||||
|
|
||||||
scrollToY : (y)=>{
|
|
||||||
viewRef.current.scrollDOM.scrollTo({ top: y });
|
|
||||||
},
|
|
||||||
|
|
||||||
getLineTop : (lineNumber)=>{
|
|
||||||
const view = viewRef.current;
|
const view = viewRef.current;
|
||||||
if(!view) return 0;
|
if(!view) return;
|
||||||
|
|
||||||
const line = view.state.doc.line(lineNumber);
|
const pos = pageMap.current[pageNumber - 1] ?? 0;
|
||||||
return view.coordsAtPos(line.from)?.top ?? 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
setCursorToLine : (lineNumber)=>{
|
|
||||||
const view = viewRef.current;
|
|
||||||
const line = view.state.doc.line(lineNumber);
|
|
||||||
|
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
selection : { anchor: line.from }
|
selection : { anchor: pos },
|
||||||
|
effects : [setProgrammaticCursorLine.of(pos), EditorView.scrollIntoView(pos, { y: 'start' })],
|
||||||
});
|
});
|
||||||
|
|
||||||
view.focus();
|
view.focus();
|
||||||
|
|
||||||
|
setTimeout(()=>{
|
||||||
|
view.dispatch({
|
||||||
|
effects : setProgrammaticCursorLine.of(null)
|
||||||
|
});
|
||||||
|
}, 400);
|
||||||
},
|
},
|
||||||
|
|
||||||
undo : ()=>undo(viewRef.current),
|
undo : ()=>undo(viewRef.current),
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Flash animation for source moves */
|
/* Flash animation for source moves */
|
||||||
.sourceMoveFlash .cm-line {
|
.cm-line.sourceMoveFlash {
|
||||||
animation-name: sourceMoveAnimation;
|
animation-name: sourceMoveAnimation;
|
||||||
animation-duration: 0.4s;
|
animation-duration: 0.4s;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { foldService, codeFolding } from '@codemirror/language';
|
import { foldService, codeFolding } from '@codemirror/language';
|
||||||
|
|
||||||
const pageFoldExtension = [
|
const foldOnPages = [
|
||||||
foldService.of((state, lineStart)=>{ //tells where to fold
|
foldService.of((state, lineStart)=>{ //tells where to fold
|
||||||
const doc = state.doc;
|
const doc = state.doc;
|
||||||
const matcher = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
const matcher = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
||||||
@@ -43,4 +43,4 @@ const pageFoldExtension = [
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
export default pageFoldExtension;
|
export default foldOnPages;
|
||||||
@@ -26,6 +26,8 @@ export function tokenizeCustomMarkdown(text) {
|
|||||||
const tokens = [];
|
const tokens = [];
|
||||||
const lines = text.split('\n');
|
const lines = text.split('\n');
|
||||||
|
|
||||||
|
//tokens without a `from` or `to` are interpreted by the custom plugin as line tokens
|
||||||
|
|
||||||
lines.forEach((lineText, lineNumber)=>{
|
lines.forEach((lineText, lineNumber)=>{
|
||||||
// --- Page / snippet lines ---
|
// --- Page / snippet lines ---
|
||||||
if(/^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m.test(lineText)) tokens.push({ line: lineNumber, type: customTags.pageLine });
|
if(/^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m.test(lineText)) tokens.push({ line: lineNumber, type: customTags.pageLine });
|
||||||
@@ -79,8 +81,8 @@ export function tokenizeCustomMarkdown(text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const singleLineRegex = /^([^:\n]*\S)(\s*)(::)([^\n]*)$/dmy;
|
// --- single line def list ---
|
||||||
|
const singleLineRegex = /^(?=.*[^:])(.+?)(\s*)(::)([^\n]*)$/dmy;
|
||||||
const match = singleLineRegex.exec(lineText);
|
const match = singleLineRegex.exec(lineText);
|
||||||
|
|
||||||
if(match) {
|
if(match) {
|
||||||
@@ -127,7 +129,7 @@ export function tokenizeCustomMarkdown(text) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// multiline def list
|
// --- multiline def list ---
|
||||||
if(!/^::/.test(lines[lineNumber]) && lineNumber + 1 < lines.length && /^::/.test(lines[lineNumber + 1])) {
|
if(!/^::/.test(lines[lineNumber]) && lineNumber + 1 < lines.length && /^::/.test(lines[lineNumber + 1])) {
|
||||||
const startLine = lineNumber;
|
const startLine = lineNumber;
|
||||||
const defs = [];
|
const defs = [];
|
||||||
@@ -187,8 +189,8 @@ export function tokenizeCustomMarkdown(text) {
|
|||||||
while ((match = injectionRegex.exec(lineText)) !== null) {
|
while ((match = injectionRegex.exec(lineText)) !== null) {
|
||||||
tokens.push({
|
tokens.push({
|
||||||
line : lineNumber,
|
line : lineNumber,
|
||||||
from : match.index +1,
|
from : match.index,
|
||||||
to : match.index + match[1].length +1,
|
to : match.index + match[1].length,
|
||||||
type : customTags.injection,
|
type : customTags.injection,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-27
@@ -1,6 +1,6 @@
|
|||||||
/* eslint max-lines: ["error", { "max": 300 }] */
|
/* eslint max-lines: ["error", { "max": 300 }] */
|
||||||
import { keymap } from '@codemirror/view';
|
import { keymap } from '@codemirror/view';
|
||||||
import { undo, redo } from '@codemirror/commands';
|
import { undo, redo, indentMore } from '@codemirror/commands';
|
||||||
import * as prettier from "prettier/standalone";
|
import * as prettier from "prettier/standalone";
|
||||||
import * as postcssPlugin from "prettier/plugins/postcss";
|
import * as postcssPlugin from "prettier/plugins/postcss";
|
||||||
|
|
||||||
@@ -21,27 +21,6 @@ async function formatCSS(view) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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 indentLess = (view)=>{
|
||||||
const { from, to } = view.state.selection.main;
|
const { from, to } = view.state.selection.main;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
@@ -227,8 +206,13 @@ const newPage = (view)=>{
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default keymap.of([
|
export const generalKeymap = keymap.of([
|
||||||
{ key: 'Tab', run: insertTabAtCursor },
|
{ key: 'Tab', run: indentMore },
|
||||||
|
{ key: 'Mod-z', run: undo }, //i think it may be unnecessary
|
||||||
|
{ key: 'Mod-Shift-z', run: redo },
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const markdownKeymap = keymap.of([
|
||||||
//{ key: 'Shift-Tab', run: indentMore },
|
//{ key: 'Shift-Tab', run: indentMore },
|
||||||
{ key: 'Shift-Tab', run: indentLess },
|
{ key: 'Shift-Tab', run: indentLess },
|
||||||
{ key: 'Mod-b', run: makeBold },
|
{ key: 'Mod-b', run: makeBold },
|
||||||
@@ -253,7 +237,5 @@ export default keymap.of([
|
|||||||
{ key: 'Shift-Mod-6', run: makeHeader(6) },
|
{ key: 'Shift-Mod-6', run: makeHeader(6) },
|
||||||
{ key: 'Shift-Mod-Enter', run: newColumn },
|
{ key: 'Shift-Mod-Enter', run: newColumn },
|
||||||
{ key: 'Mod-Enter', run: newPage },
|
{ key: 'Mod-Enter', run: newPage },
|
||||||
{ key: 'Mod-z', run: undo }, //i think it may be unnecessary
|
{ key: 'Mod-Shift-f', run: formatCSS },
|
||||||
{ key: 'Mod-Shift-z', run: redo },
|
|
||||||
{ key: 'alt-Shift-f', run: formatCSS },
|
|
||||||
]);
|
]);
|
||||||
@@ -33,7 +33,7 @@ const INITIAL_CONTENT = dedent`
|
|||||||
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
|
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
|
||||||
<link href="${brewRendererStylesUrl}" rel="stylesheet" />
|
<link href="${brewRendererStylesUrl}" rel="stylesheet" />
|
||||||
<link href="${headerNavStylesUrl}" rel="stylesheet" />
|
<link href="${headerNavStylesUrl}" rel="stylesheet" />
|
||||||
<base target=_blank>
|
<base target="_top">
|
||||||
</head><body style='overflow: hidden'><div></div></body></html>`;
|
</head><body style='overflow: hidden'><div></div></body></html>`;
|
||||||
|
|
||||||
|
|
||||||
@@ -135,6 +135,7 @@ const BrewRenderer = (props)=>{
|
|||||||
|
|
||||||
const mainRef = useRef(null);
|
const mainRef = useRef(null);
|
||||||
const pagesRef = useRef(null);
|
const pagesRef = useRef(null);
|
||||||
|
const urlRef = useRef('');
|
||||||
|
|
||||||
if(props.renderer == 'legacy') {
|
if(props.renderer == 'legacy') {
|
||||||
rawPages = props.text.split(PAGEBREAK_REGEX_LEGACY);
|
rawPages = props.text.split(PAGEBREAK_REGEX_LEGACY);
|
||||||
@@ -272,6 +273,8 @@ const BrewRenderer = (props)=>{
|
|||||||
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
|
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
|
||||||
scrollToHash(window.location.hash);
|
scrollToHash(window.location.hash);
|
||||||
|
|
||||||
|
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
|
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(); //Make sure page is renderable before showing
|
||||||
setState((prevState)=>({
|
setState((prevState)=>({
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ const HeaderNavItem = ({ link, text, depth, className })=>{
|
|||||||
if(!link || !text) return;
|
if(!link || !text) return;
|
||||||
|
|
||||||
return <li>
|
return <li>
|
||||||
<a href={`#${link}`} target='_self' className={`depth-${depth} ${className ?? ''}`}>
|
<a href={`#${link}`} className={`depth-${depth} ${className ?? ''}`}>
|
||||||
{trimString(text, depth)}
|
{trimString(text, depth)}
|
||||||
</a>
|
</a>
|
||||||
</li>;
|
</li>;
|
||||||
|
|||||||
@@ -11,7 +11,21 @@ import MetadataEditor from './metadataEditor/metadataEditor.jsx';
|
|||||||
|
|
||||||
const EDITOR_THEME_KEY = 'HB_editor_theme';
|
const EDITOR_THEME_KEY = 'HB_editor_theme';
|
||||||
|
|
||||||
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
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 PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
||||||
//const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
|
//const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
|
||||||
const DEFAULT_STYLE_TEXT = dedent`
|
const DEFAULT_STYLE_TEXT = dedent`
|
||||||
/*=======--- Example CSS styling ---=======*/
|
/*=======--- Example CSS styling ---=======*/
|
||||||
@@ -29,6 +43,7 @@ const DEFAULT_SNIPPET_TEXT = dedent`
|
|||||||
This snippet is accessible in the brew tab, and will be inherited if the brew is used as a theme.
|
This snippet is accessible in the brew tab, and will be inherited if the brew is used as a theme.
|
||||||
`;
|
`;
|
||||||
let isJumping = false;
|
let isJumping = false;
|
||||||
|
let jumpSource = null;
|
||||||
|
|
||||||
const Editor = createReactClass({
|
const Editor = createReactClass({
|
||||||
displayName : 'Editor',
|
displayName : 'Editor',
|
||||||
@@ -71,14 +86,15 @@ const Editor = createReactClass({
|
|||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
|
|
||||||
document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys);
|
const brewRenderer = document.getElementById('BrewRenderer');
|
||||||
|
brewRenderer.onload = ()=>brewRenderer.contentDocument?.addEventListener('keydown', this.handleControlKeys);
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
document.addEventListener('keydown', this.handleControlKeys);
|
||||||
|
|
||||||
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
|
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
|
||||||
if(editorTheme) {
|
if(editorTheme && EditorThemes.includes(editorTheme)) {
|
||||||
this.setState({
|
this.setState({ editorTheme });
|
||||||
editorTheme : editorTheme
|
} else {
|
||||||
});
|
this.setState({ editorTheme: 'default' });
|
||||||
}
|
}
|
||||||
const snippetBar = document.querySelector('.editor > .snippetBar');
|
const snippetBar = document.querySelector('.editor > .snippetBar');
|
||||||
if(!snippetBar) return;
|
if(!snippetBar) return;
|
||||||
@@ -126,22 +142,16 @@ const Editor = createReactClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateCurrentCursorPage : function(lineNumber) {
|
updateCurrentCursorPage : function(pageNumber) {
|
||||||
const lines = this.props.brew.text.split('\n').slice(0, lineNumber);
|
this.props.onCursorPageChange(pageNumber);
|
||||||
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(topLine) {
|
updateCurrentViewPage : function(pageNumber) {
|
||||||
const lines = this.props.brew.text.split('\n').slice(0, topLine);
|
this.props.onViewPageChange(pageNumber);
|
||||||
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);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handleInject : function(injectText){
|
handleInject : function(injectText){
|
||||||
this.codeEditor.current?.injectText(injectText, false);
|
this.codeEditor.current?.injectText(injectText);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleViewChange : function(newView){
|
handleViewChange : function(newView){
|
||||||
@@ -150,12 +160,12 @@ const Editor = createReactClass({
|
|||||||
this.setState({
|
this.setState({
|
||||||
view : newView
|
view : newView
|
||||||
}, ()=>{
|
}, ()=>{
|
||||||
this.codeEditor.current?.codeMirror?.focus();
|
this.codeEditor.current?.focus();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
brewJump : function(targetPage=this.props.currentEditorCursorPageNum, smooth=true){
|
brewJump : function(targetPage=this.props.currentEditorCursorPageNum, smooth=true){
|
||||||
if(!window || !this.isText() || isJumping)
|
if(!window || !this.isText() || isJumping || jumpSource === 'source')
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Get current brewRenderer scroll position and calculate target position
|
// Get current brewRenderer scroll position and calculate target position
|
||||||
@@ -168,11 +178,13 @@ const Editor = createReactClass({
|
|||||||
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
|
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
|
||||||
scrollingTimeout = setTimeout(()=>{
|
scrollingTimeout = setTimeout(()=>{
|
||||||
isJumping = false;
|
isJumping = false;
|
||||||
|
jumpSource = null;
|
||||||
brewRenderer.removeEventListener('scroll', checkIfScrollComplete);
|
brewRenderer.removeEventListener('scroll', checkIfScrollComplete);
|
||||||
}, 150); // If 150 ms pass without a brewRenderer scroll event, assume scrolling is done
|
}, 150); // If 150 ms pass without a brewRenderer scroll event, assume scrolling is done
|
||||||
};
|
};
|
||||||
|
|
||||||
isJumping = true;
|
isJumping = true;
|
||||||
|
jumpSource = 'brew';
|
||||||
checkIfScrollComplete();
|
checkIfScrollComplete();
|
||||||
brewRenderer.addEventListener('scroll', checkIfScrollComplete);
|
brewRenderer.addEventListener('scroll', checkIfScrollComplete);
|
||||||
|
|
||||||
@@ -196,46 +208,19 @@ const Editor = createReactClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
sourceJump : function(targetPage=this.props.currentBrewRendererPageNum, smooth=true){
|
sourceJump : function(targetPage=this.props.currentBrewRendererPageNum, smooth=true){
|
||||||
if(!this.isText() || isJumping)
|
if(!this.isText() || isJumping || jumpSource === 'brew')
|
||||||
return;
|
return;
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
const editor = this.codeEditor.current;
|
const editor = this.codeEditor.current;
|
||||||
|
if(!editor) return;
|
||||||
|
jumpSource = 'source';
|
||||||
|
|
||||||
let currentY = editor.getScrollTop();
|
editor.scrollToPage(targetPage);
|
||||||
const targetY = editor.getLineTop(targetLine);
|
setTimeout(()=>{
|
||||||
|
jumpSource = null;
|
||||||
let scrollingTimeout;
|
}, 200);
|
||||||
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;
|
|
||||||
}, 150); // If 150 ms pass without a scroll event, assume scrolling is done
|
|
||||||
};
|
|
||||||
|
|
||||||
isJumping = true;
|
|
||||||
checkIfScrollComplete();
|
|
||||||
|
|
||||||
if(smooth) {
|
|
||||||
//Scroll 1/10 of the way every 10ms until 1px off.
|
|
||||||
const incrementalScroll = setInterval(()=>{
|
|
||||||
currentY += (targetY - currentY) / 10;
|
|
||||||
editor.scrollToY(currentY);
|
|
||||||
|
|
||||||
if(Math.abs(targetY - currentY) < 1) {
|
|
||||||
editor.scrollToY(targetY);
|
|
||||||
editor.setCursorToLine(targetLine);
|
|
||||||
clearInterval(incrementalScroll);
|
|
||||||
}
|
|
||||||
}, 10);
|
|
||||||
} else {
|
|
||||||
editor.scrollToY(targetY);
|
|
||||||
editor.setCursorToLine(targetLine);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
//Called when there are changes to the editor's dimensions
|
//Called when there are changes to the editor's dimensions
|
||||||
update : function(){},
|
update : function(){},
|
||||||
|
|
||||||
@@ -261,12 +246,11 @@ const Editor = createReactClass({
|
|||||||
view={this.state.view}
|
view={this.state.view}
|
||||||
value={this.props.brew.text}
|
value={this.props.brew.text}
|
||||||
onChange={this.props.onBrewChange('text')}
|
onChange={this.props.onBrewChange('text')}
|
||||||
onCursorChange={(line)=>this.updateCurrentCursorPage(line)}
|
onCursorChange={(page)=>this.updateCurrentCursorPage(page)}
|
||||||
onViewChange={(line)=>this.updateCurrentViewPage(line)}
|
onViewChange={(page)=>this.updateCurrentViewPage(page)}
|
||||||
editorTheme={this.state.editorTheme}
|
editorTheme={this.state.editorTheme}
|
||||||
renderer={this.props.brew.renderer}
|
renderer={this.props.brew.renderer}
|
||||||
rerenderParent={this.rerenderParent}
|
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }}/>
|
||||||
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }} />
|
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
if(this.isStyle()){
|
if(this.isStyle()){
|
||||||
@@ -278,19 +262,16 @@ const Editor = createReactClass({
|
|||||||
view={this.state.view}
|
view={this.state.view}
|
||||||
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
||||||
onChange={this.props.onBrewChange('style')}
|
onChange={this.props.onBrewChange('style')}
|
||||||
enableFolding={true}
|
|
||||||
editorTheme={this.state.editorTheme}
|
editorTheme={this.state.editorTheme}
|
||||||
renderer={this.props.brew.renderer}
|
renderer={this.props.brew.renderer}
|
||||||
rerenderParent={this.rerenderParent}
|
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }}/>
|
||||||
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }} />
|
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
if(this.isMeta()){
|
if(this.isMeta()){
|
||||||
return <>
|
return <>
|
||||||
<CodeEditor key='codeEditor'
|
<CodeEditor key='codeEditor'
|
||||||
view={this.state.view}
|
view={this.state.view}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}/>
|
||||||
rerenderParent={this.rerenderParent} />
|
|
||||||
<MetadataEditor
|
<MetadataEditor
|
||||||
metadata={this.props.brew}
|
metadata={this.props.brew}
|
||||||
themeBundle={this.props.themeBundle}
|
themeBundle={this.props.themeBundle}
|
||||||
@@ -313,7 +294,7 @@ const Editor = createReactClass({
|
|||||||
editorTheme={this.state.editorTheme}
|
editorTheme={this.state.editorTheme}
|
||||||
renderer={this.props.brew.renderer}
|
renderer={this.props.brew.renderer}
|
||||||
rerenderParent={this.rerenderParent}
|
rerenderParent={this.rerenderParent}
|
||||||
style={{ height: `calc(100% - 25px)` }} />
|
style={{ height: `calc(100% - 25px)` }}/>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -330,13 +311,13 @@ const Editor = createReactClass({
|
|||||||
return this.codeEditor.current?.undo();
|
return this.codeEditor.current?.undo();
|
||||||
},
|
},
|
||||||
|
|
||||||
foldCode: function() {
|
foldCode : function() {
|
||||||
return this.codeEditor.current?.foldAll();
|
return this.codeEditor.current?.foldAll();
|
||||||
},
|
},
|
||||||
|
|
||||||
unfoldCode: function() {
|
unfoldCode : function() {
|
||||||
return this.codeEditor.current?.unfoldAll();
|
return this.codeEditor.current?.unfoldAll();
|
||||||
},
|
},
|
||||||
render : function(){
|
render : function(){
|
||||||
return (
|
return (
|
||||||
<div className='editor' ref={this.editor}>
|
<div className='editor' ref={this.editor}>
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ const ThemeSnippets = {
|
|||||||
V3_Blank : V3_Blank,
|
V3_Blank : V3_Blank,
|
||||||
};
|
};
|
||||||
|
|
||||||
//import EditorThemes from '../../../../build/homebrew/codeMirror/editorThemes.json';
|
|
||||||
import * as themesImport from '@uiw/codemirror-themes-all';
|
import * as themesImport from '@uiw/codemirror-themes-all';
|
||||||
import defaultCM5Theme from '@themes/codeMirror/default.js';
|
import defaultCM5Theme from '@themes/codeMirror/default.js';
|
||||||
import darkbrewery from '@themes/codeMirror/darkbrewery.js';
|
import darkbrewery from '@themes/codeMirror/darkbrewery.js';
|
||||||
|
|||||||
+4
-4
@@ -25,18 +25,18 @@
|
|||||||
|
|
||||||
let prefix = '';
|
let prefix = '';
|
||||||
|
|
||||||
if (url.includes('://homebrewery-stage.')) {
|
if (url && url?.includes('://homebrewery-stage.')) {
|
||||||
prefix = `Stage `;
|
prefix = `Stage `;
|
||||||
} else if (url.includes('://homebrewery-pr-')) {
|
} else if (url?.includes('://homebrewery-pr-')) {
|
||||||
const match = url.match(/pr-(\d+)/);
|
const match = url.match(/pr-(\d+)/);
|
||||||
if (match) prefix = `PR-${match[1]} `;
|
if (match) prefix = `PR-${match[1]} `;
|
||||||
} else if (url.includes('://localhost')) {
|
} else if (url?.includes('://localhost')) {
|
||||||
prefix = 'Local ';
|
prefix = 'Local ';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
document.title = `${prefix} - ${title} - The Homebrewery`;
|
document.title = `${prefix} - ${title} - The Homebrewery`;
|
||||||
} else {
|
} else if (prefix) {
|
||||||
document.title = `${prefix} - The Homebrewery`;
|
document.title = `${prefix} - The Homebrewery`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+455
-453
File diff suppressed because it is too large
Load Diff
+5
-6
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "homebrewery",
|
"name": "homebrewery",
|
||||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||||
"version": "3.20.1",
|
"version": "3.21.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": ">=10.8 <12",
|
"npm": ">=10.8 <12",
|
||||||
@@ -110,7 +110,6 @@
|
|||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"cm6-theme-basic-light": "^0.2.0",
|
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"core-js": "^3.49.0",
|
"core-js": "^3.49.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
@@ -126,7 +125,7 @@
|
|||||||
"js-yaml": "^4.1.1",
|
"js-yaml": "^4.1.1",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
"less": "^4.6.4",
|
"less": "^4.6.4",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.18.1",
|
||||||
"marked": "15.0.12",
|
"marked": "15.0.12",
|
||||||
"marked-alignment-paragraphs": "^1.0.0",
|
"marked-alignment-paragraphs": "^1.0.0",
|
||||||
"marked-definition-lists": "^1.0.1",
|
"marked-definition-lists": "^1.0.1",
|
||||||
@@ -142,12 +141,12 @@
|
|||||||
"mongoose": "^9.3.3",
|
"mongoose": "^9.3.3",
|
||||||
"nanoid": "5.1.7",
|
"nanoid": "5.1.7",
|
||||||
"nconf": "^0.13.0",
|
"nconf": "^0.13.0",
|
||||||
"node": "^25.8.2",
|
"node": "^25.9.0",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-frame-component": "^5.3.1",
|
"react-frame-component": "^5.3.2",
|
||||||
"react-router": "^7.13.2",
|
"react-router": "^7.14.0",
|
||||||
"sanitize-filename": "1.6.4",
|
"sanitize-filename": "1.6.4",
|
||||||
"superagent": "^10.2.1"
|
"superagent": "^10.2.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -392,7 +392,7 @@ const api = {
|
|||||||
|
|
||||||
if(brewFromServer?.hash !== brewFromClient?.hash) {
|
if(brewFromServer?.hash !== brewFromClient?.hash) {
|
||||||
console.log(`Hash mismatch on brew ${brewFromClient.editId}`);
|
console.log(`Hash mismatch on brew ${brewFromClient.editId}`);
|
||||||
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
||||||
res.setHeader('Content-Type', 'application/json');
|
res.setHeader('Content-Type', 'application/json');
|
||||||
return res.status(409).send(JSON.stringify({ message: `The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` }));
|
return res.status(409).send(JSON.stringify({ message: `The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` }));
|
||||||
}
|
}
|
||||||
@@ -405,7 +405,7 @@ const api = {
|
|||||||
throw ('Patches did not apply cleanly, text mismatch detected');
|
throw ('Patches did not apply cleanly, text mismatch detected');
|
||||||
// brew.text = applyPatches(patches, brewFromServer.text)[0];
|
// brew.text = applyPatches(patches, brewFromServer.text)[0];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
||||||
console.error('Failed to apply patches:', {
|
console.error('Failed to apply patches:', {
|
||||||
//patches : brewFromClient.patches,
|
//patches : brewFromClient.patches,
|
||||||
brewId : brewFromClient.editId || 'unknown',
|
brewId : brewFromClient.editId || 'unknown',
|
||||||
|
|||||||
+3
-3
@@ -70,9 +70,9 @@ renderer.link = function (token) {
|
|||||||
if(title) {
|
if(title) {
|
||||||
out += ` title="${escape(title)}"`;
|
out += ` title="${escape(title)}"`;
|
||||||
}
|
}
|
||||||
if(self) {
|
// if(self) {
|
||||||
out += ' target="_self"';
|
// out += ' target="_self"';
|
||||||
}
|
// }
|
||||||
out += `>${text}</a>`;
|
out += `>${text}</a>`;
|
||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ renderer.link = function (href, title, text) {
|
|||||||
if(title) {
|
if(title) {
|
||||||
out += ` title="${title}"`;
|
out += ` title="${title}"`;
|
||||||
}
|
}
|
||||||
if(self) {
|
// if(self) {
|
||||||
out += ' target="_self"';
|
// out += ' target="_self"';
|
||||||
}
|
// }
|
||||||
out += `>${text}</a>`;
|
out += `>${text}</a>`;
|
||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ test('Processes the markdown within an HTML block if its just a class wrapper',
|
|||||||
expect(rendered).toBe('<div> <p><em>Bold text</em></p>\n </div>');
|
expect(rendered).toBe('<div> <p><em>Bold text</em></p>\n </div>');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Check markdown is using the custom renderer; specifically that it adds target=_self attribute to internal links in HTML blocks', function() {
|
// TEST REMOVED AS IT IS NO LONGER REQUIRED
|
||||||
const source = '<div>[Has _self Attribute?](#p1)</div>';
|
//
|
||||||
const rendered = Markdown.render(source);
|
// test('Check markdown is using the custom renderer; specifically that it adds target=_self attribute to internal links in HTML blocks', function() {
|
||||||
expect(rendered).toBe('<div> <p><a href="#p1" target="_self">Has _self Attribute?</a></p>\n </div>');
|
// const source = '<div>[Has _self Attribute?](#p1)</div>';
|
||||||
});
|
// const rendered = Markdown.render(source);
|
||||||
|
// expect(rendered).toBe('<div> <p><a href="#p1" target="_self">Has _self Attribute?</a></p>\n </div>');
|
||||||
|
// });
|
||||||
|
|||||||
Reference in New Issue
Block a user