0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-06-22 04:58:40 +00:00

Merge branch 'master' of https://github.com/naturalcrit/homebrewery into add-image-preview

This commit is contained in:
Víctor Losada Hernández
2026-05-10 19:10:23 +02:00
11 changed files with 623 additions and 524 deletions
+36 -6
View File
@@ -17,8 +17,7 @@ import {
crosshairCursor, crosshairCursor,
} from '@codemirror/view'; } from '@codemirror/view';
import { EditorState, Compartment, StateEffect, StateField } 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, foldEffect, foldState, syntaxHighlighting } from '@codemirror/language';
import { foldEffect } 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 } from '@codemirror/lang-css'; import { css } from '@codemirror/lang-css';
@@ -231,11 +230,14 @@ const CodeEditor = forwardRef(
const editorRef = useRef(null); const editorRef = useRef(null);
const viewRef = useRef(null); const viewRef = useRef(null);
const docsRef = useRef({}); const docsRef = useRef({});
const tabRef = useRef(tab);
const prevTabRef = useRef(tab); const prevTabRef = useRef(tab);
const scrollRef = useRef({});
const foldsRef = useRef({});
const pageMap = useRef([]); const pageMap = useRef([]);
const recomputePages = (doc)=>{ const recomputePages = (doc)=>{
if(tab !== 'brewText') return;
const pages = [0]; const pages = [0];
const text = doc.toString(); const text = doc.toString();
let offset = 0; let offset = 0;
@@ -261,6 +263,14 @@ const CodeEditor = forwardRef(
return page; 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 createExtensions = ({ onChange, language, editorTheme })=>{
const setEventListeners = EditorView.updateListener.of((update)=>{ const setEventListeners = EditorView.updateListener.of((update)=>{
if(update.docChanged) { if(update.docChanged) {
@@ -347,11 +357,10 @@ const CodeEditor = forwardRef(
ticking = true; ticking = true;
requestAnimationFrame(()=>{ requestAnimationFrame(()=>{
const top = view.scrollDOM.scrollTop; const top = view.scrollDOM.scrollTop;
scrollRef.current[tabRef.current] = top;
const block = view.lineBlockAtHeight(top); const block = view.lineBlockAtHeight(top);
const page = findPageFromPos(block.from);
const page = findPageFromPos(block.from); // CHANGED
onViewChange(page); onViewChange(page);
ticking = false; ticking = false;
}); });
}; };
@@ -366,12 +375,23 @@ const CodeEditor = forwardRef(
}; };
}, []); }, []);
const restoreFolds = (view, folds)=>{
if(!folds?.length) return;
view.dispatch({
effects : folds.map((f)=>foldEffect.of(f))
});
};
useEffect(()=>{ useEffect(()=>{
const view = viewRef.current; const view = viewRef.current;
if(!view) return; if(!view) return;
tabRef.current = tab;
const prevTab = prevTabRef.current; const prevTab = prevTabRef.current;
foldsRef.current[prevTab] = getFoldRanges(view.state);
if(prevTab !== tab) { if(prevTab !== tab) {
docsRef.current[prevTab] = view.state; docsRef.current[prevTab] = view.state;
@@ -385,6 +405,16 @@ const CodeEditor = forwardRef(
} }
view.setState(nextState); view.setState(nextState);
restoreFolds(view, foldsRef.current[tab]);
const savedScroll = scrollRef.current[tab];
if(savedScroll != null) {
requestAnimationFrame(()=>{
view.scrollDOM.scrollTop = savedScroll;
});
}
prevTabRef.current = tab; prevTabRef.current = tab;
} }
view.focus(); view.focus();
@@ -100,6 +100,10 @@
vertical-align : sub; vertical-align : sub;
color : rgb(123, 123, 15); color : rgb(123, 123, 15);
} }
.cm-strikethrough {
text-decoration: line-through;
}
.cm-definitionList { .cm-definitionList {
.cm-definitionTerm { color : rgb(96, 117, 143); } .cm-definitionTerm { color : rgb(96, 117, 143); }
.cm-definitionColon:not(:has(.cm-comment)) { .cm-definitionColon:not(:has(.cm-comment)) {
@@ -16,6 +16,7 @@ const customTags = {
definitionTerm : 'definitionTerm', // .cm-definitionTerm definitionTerm : 'definitionTerm', // .cm-definitionTerm
definitionDesc : 'definitionDesc', // .cm-definitionDesc definitionDesc : 'definitionDesc', // .cm-definitionDesc
definitionColon : 'definitionColon', // .cm-definitionColon definitionColon : 'definitionColon', // .cm-definitionColon
strikethrough : 'strikethrough', // .cm-strikethrough
//CSS //CSS
@@ -81,6 +82,23 @@ export function tokenizeCustomMarkdown(text) {
} }
} }
// --- Strikethrough ---
if(/\~/.test(lineText)) {
const strikethroughRegex = /~(?!\s)(.+?)(?<!\s)~/g;
let match = strikethroughRegex.exec(lineText);
let type = customTags.strikethrough;
if(match) {
tokens.push({
line : lineNumber,
type,
from : match.index,
to : match.index + match[0].length,
});
}
}
// --- single line def list --- // --- single line def list ---
const singleLineRegex = /^(?=.*[^:])(.+?)(\s*)(::)([^\n]*)$/dmy; const singleLineRegex = /^(?=.*[^:])(.+?)(\s*)(::)([^\n]*)$/dmy;
const match = singleLineRegex.exec(lineText); const match = singleLineRegex.exec(lineText);
@@ -190,7 +208,7 @@ export function tokenizeCustomMarkdown(text) {
tokens.push({ tokens.push({
line : lineNumber, line : lineNumber,
from : match.indices[1][0], from : match.indices[1][0],
to : match.indices[1][0] + match[1].length, to : match.indices[1][1],
type : customTags.injection, type : customTags.injection,
}); });
} }
@@ -3,6 +3,17 @@ import { keymap } from '@codemirror/view';
import { undo, redo, indentMore, deleteLine } from '@codemirror/commands'; import { undo, redo, indentMore, deleteLine } from '@codemirror/commands';
import { Prec } from '@codemirror/state'; 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 indentLess = (view)=>{
const { from, to } = view.state.selection.main; const { from, to } = view.state.selection.main;
const lines = []; const lines = [];
@@ -162,7 +173,7 @@ const newPage = (view)=>{
}; };
export const generalKeymap = Prec.high(keymap.of([ 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-z', run: undo }, //i think it may be unnecessary
{ key: 'Mod-Shift-z', run: redo }, { key: 'Mod-Shift-z', run: redo },
{ key: 'Mod-y', run: redo }, { key: 'Mod-y', run: redo },
@@ -170,7 +170,7 @@ const Snippetbar = createReactClass({
this.props.updateEditorTheme(e.target.value); this.props.updateEditorTheme(e.target.value);
this.setState({ this.setState({
showThemeSelector : false, themeSelector : false,
}); });
}, },
+8 -10
View File
@@ -90,7 +90,7 @@ const EditPage = (props)=>{
const handleControlKeys = (e)=>{ const handleControlKeys = (e)=>{
if(!(e.ctrlKey || e.metaKey)) return; 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(e.keyCode === 80) printCurrentBrew();
if([83, 80].includes(e.keyCode)) { if([83, 80].includes(e.keyCode)) {
e.stopPropagation(); e.stopPropagation();
@@ -118,13 +118,9 @@ const EditPage = (props)=>{
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current); const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
setUnsavedChanges(hasChange); setUnsavedChanges(hasChange);
if(autoSaveEnabled) trySave(false, hasChange); if(autoSaveEnabled) trySave(false, hasChange, saveGoogle);
}, [currentBrew]); }, [currentBrew]);
useEffect(()=>{
trySave(true);
}, [saveGoogle]);
const handleSplitMove = ()=>{ const handleSplitMove = ()=>{
editorRef.current?.update(); editorRef.current?.update();
}; };
@@ -183,11 +179,13 @@ const EditPage = (props)=>{
}; };
const toggleGoogleStorage = ()=>{ const toggleGoogleStorage = ()=>{
const newSaveGoogle = !saveGoogle;
setSaveGoogle((prev)=>!prev); setSaveGoogle((prev)=>!prev);
setError(null); setError(null);
trySave(true, true, newSaveGoogle);
}; };
const trySave = (immediate = false, hasChanges = true)=>{ const trySave = (immediate = false, hasChanges = true, saveToGoogle = false)=>{
clearTimeout(saveTimeout.current); clearTimeout(saveTimeout.current);
if(isSaving) return; if(isSaving) return;
if(!hasChanges && !immediate) return; if(!hasChanges && !immediate) return;
@@ -196,7 +194,7 @@ const EditPage = (props)=>{
saveTimeout.current = setTimeout(async ()=>{ saveTimeout.current = setTimeout(async ()=>{
setIsSaving(true); setIsSaving(true);
setError(null); setError(null);
await save(currentBrew, saveGoogle) await save(currentBrew, saveToGoogle)
.catch((err)=>{ .catch((err)=>{
setError(err); setError(err);
}); });
@@ -216,7 +214,7 @@ const EditPage = (props)=>{
const brewToSave = { const brewToSave = {
...brew, ...brew,
text : brew.text.normalize('NFC'), 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')))), patches : stringifyPatches(makePatches(encodeURI(lastSavedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))),
hash : await md5(lastSavedBrew.current.text.normalize('NFC')), hash : await md5(lastSavedBrew.current.text.normalize('NFC')),
textBin : undefined, textBin : undefined,
@@ -314,7 +312,7 @@ const EditPage = (props)=>{
// #3 - Unsaved changes exist, click to save, show SAVE NOW // #3 - Unsaved changes exist, click to save, show SAVE NOW
if(unsavedChanges) 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 // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
if(autoSaveEnabled) if(autoSaveEnabled)
+1 -1
View File
@@ -156,7 +156,7 @@ const NewPage = (props)=>{
const updatedBrew = { ...currentBrew }; const updatedBrew = { ...currentBrew };
splitTextStyleAndMetadata(updatedBrew); 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; updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1;
const res = await request const res = await request
+500 -488
View File
File diff suppressed because it is too large Load Diff
+13 -13
View File
@@ -88,10 +88,10 @@
"dependencies": { "dependencies": {
"@babel/core": "^7.29.0", "@babel/core": "^7.29.0",
"@babel/plugin-transform-runtime": "^7.29.0", "@babel/plugin-transform-runtime": "^7.29.0",
"@babel/preset-env": "^7.29.2", "@babel/preset-env": "^7.29.5",
"@babel/preset-react": "^7.28.5", "@babel/preset-react": "^7.28.5",
"@babel/runtime": "^7.29.2", "@babel/runtime": "^7.29.2",
"@codemirror/autocomplete": "^6.20.1", "@codemirror/autocomplete": "^6.20.2",
"@codemirror/commands": "^6.10.3", "@codemirror/commands": "^6.10.3",
"@codemirror/highlight": "^0.19.8", "@codemirror/highlight": "^0.19.8",
"@codemirror/lang-css": "^6.3.1", "@codemirror/lang-css": "^6.3.1",
@@ -101,7 +101,7 @@
"@codemirror/language-data": "^6.5.2", "@codemirror/language-data": "^6.5.2",
"@codemirror/search": "^6.6.0", "@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.6.0", "@codemirror/state": "^6.6.0",
"@codemirror/view": "^6.40.0", "@codemirror/view": "^6.42.1",
"@dmsnell/diff-match-patch": "^1.1.0", "@dmsnell/diff-match-patch": "^1.1.0",
"@googleapis/drive": "^20.1.0", "@googleapis/drive": "^20.1.0",
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "^1.2.3",
@@ -117,9 +117,9 @@
"dedent": "^1.7.1", "dedent": "^1.7.1",
"express": "^5.1.0", "express": "^5.1.0",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-static-gzip": "3.0.0", "express-static-gzip": "3.0.1",
"fflate": "^0.8.2", "fflate": "^0.8.2",
"fs-extra": "^11.3.3", "fs-extra": "^11.3.5",
"hash-wasm": "^4.12.0", "hash-wasm": "^4.12.0",
"idb-keyval": "^6.2.2", "idb-keyval": "^6.2.2",
"js-yaml": "^4.1.1", "js-yaml": "^4.1.1",
@@ -138,31 +138,31 @@
"marked-variables": "^1.0.5", "marked-variables": "^1.0.5",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1", "moment": "^2.30.1",
"mongoose": "^9.3.3", "mongoose": "^9.6.2",
"nanoid": "5.1.7", "nanoid": "5.1.11",
"nconf": "^0.13.0", "nconf": "^0.13.0",
"node": "^25.9.0", "node": "^25.9.0",
"react": "^19.2.4", "react": "^19.2.6",
"react-dom": "^19.2.4", "react-dom": "^19.2.6",
"react-frame-component": "^5.3.2", "react-frame-component": "^5.3.2",
"react-router": "^7.14.0", "react-router": "^7.15.0",
"sanitize-filename": "1.6.4", "sanitize-filename": "1.6.4",
"superagent": "^10.2.1" "superagent": "^10.2.1"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/stylelint-plugin": "^5.0.1", "@stylistic/stylelint-plugin": "^5.0.1",
"babel-jest": "^30.3.0", "babel-jest": "^30.4.1",
"babel-plugin-transform-import-meta": "^2.3.3", "babel-plugin-transform-import-meta": "^2.3.3",
"eslint": "9.7", "eslint": "9.7",
"eslint-plugin-jest": "^29.15.1", "eslint-plugin-jest": "^29.15.1",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"globals": "^16.4.0", "globals": "^16.4.0",
"jest": "^30.3.0", "jest": "^30.4.2",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",
"jsdom": "^28.1.0", "jsdom": "^28.1.0",
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",
"stylelint": "^17.6.0", "stylelint": "^17.11.0",
"stylelint-config-recess-order": "^7.7.0", "stylelint-config-recess-order": "^7.7.0",
"stylelint-config-recommended": "^18.0.0", "stylelint-config-recommended": "^18.0.0",
"supertest": "^7.1.4", "supertest": "^7.1.4",
+1 -1
View File
@@ -593,7 +593,7 @@ export default async function createApp(vite) {
html = html.replace( html = html.replace(
'<head>', '<head>',
`<head>\n<script id="props" >window.__INITIAL_PROPS__ = ${JSON.stringify(props)}</script>\n${ogMetaTags}` ()=>{ return `<head>\n<script id="props" >window.__INITIAL_PROPS__ = ${JSON.stringify(props)}</script>\n${ogMetaTags}`; }
); );
return html; return html;
+28 -2
View File
@@ -160,9 +160,35 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label)=>{
// Char-level diff // Char-level diff
for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) { for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) {
if(clientText[i] !== serverText[i]) { if(clientText[i] !== serverText[i]) {
const getMismatchContext = (text, index, name, size = 10)=>{
const lower = Math.max(index - size, 0);
const upper = Math.min(index + size, text.length);
const slice = `${JSON.stringify(text.slice(lower, index)).slice(1, -1)}\u001B[31m${JSON.stringify(text[i]).slice(1, -1)}\u001B[0m${JSON.stringify(text.slice(index+1, upper)).slice(1, -1)}`;
const lineNo = text.slice(0, index).split('\n').length;
const code = `U+${text.charCodeAt(i).toString(16).toUpperCase()}`;
return {
name,
lineNo,
code,
lower,
upper,
slice
};
};
const boundSize = 10;
const clientContext = getMismatchContext(clientText, i, 'Client', boundSize);
const serverContext = getMismatchContext(serverText, i, 'Server', boundSize);
const logContext = (context)=>{
console.log(` ${context.name} - line ${context.lineNo} : (${context.code})\t${context.slice}`);
};
console.log(`Char mismatch at index ${i}:`); console.log(`Char mismatch at index ${i}:`);
console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`); logContext(clientContext);
console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`); logContext(serverContext);
break; break;
} }
} }