From cb4bc16c69784d2b5c9386d9035cd4b708f86ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Fri, 24 Apr 2026 10:17:28 +0200 Subject: [PATCH 01/19] move codemirror extensions into their own folder --- client/components/codeEditor/codeEditor.jsx | 10 +++++----- .../codeEditor/{ => extensions}/autocompleteEmoji.js | 0 .../codeEditor/{ => extensions}/customFolding.js | 0 .../codeEditor/{ => extensions}/customHighlight.js | 0 .../codeEditor/{ => extensions}/customKeyMaps.js | 0 .../{ => extensions}/legacyCustomHighlight.js | 0 6 files changed, 5 insertions(+), 5 deletions(-) rename client/components/codeEditor/{ => extensions}/autocompleteEmoji.js (100%) rename client/components/codeEditor/{ => extensions}/customFolding.js (100%) rename client/components/codeEditor/{ => extensions}/customHighlight.js (100%) rename client/components/codeEditor/{ => extensions}/customKeyMaps.js (100%) rename client/components/codeEditor/{ => extensions}/legacyCustomHighlight.js (100%) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index 5f46fc3e4..3495570ff 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -24,7 +24,7 @@ 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'; @@ -40,10 +40,10 @@ 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 { customHighlightStyle, tokenizeCustomMarkdown, tokenizeCustomCSS } from './extensions/customHighlight.js'; +import { legacyCustomHighlightStyle, legacyTokenizeCustomMarkdown } from './extensions/legacyCustomHighlight.js'; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; diff --git a/client/components/codeEditor/autocompleteEmoji.js b/client/components/codeEditor/extensions/autocompleteEmoji.js similarity index 100% rename from client/components/codeEditor/autocompleteEmoji.js rename to client/components/codeEditor/extensions/autocompleteEmoji.js diff --git a/client/components/codeEditor/customFolding.js b/client/components/codeEditor/extensions/customFolding.js similarity index 100% rename from client/components/codeEditor/customFolding.js rename to client/components/codeEditor/extensions/customFolding.js diff --git a/client/components/codeEditor/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js similarity index 100% rename from client/components/codeEditor/customHighlight.js rename to client/components/codeEditor/extensions/customHighlight.js diff --git a/client/components/codeEditor/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js similarity index 100% rename from client/components/codeEditor/customKeyMaps.js rename to client/components/codeEditor/extensions/customKeyMaps.js diff --git a/client/components/codeEditor/legacyCustomHighlight.js b/client/components/codeEditor/extensions/legacyCustomHighlight.js similarity index 100% rename from client/components/codeEditor/legacyCustomHighlight.js rename to client/components/codeEditor/extensions/legacyCustomHighlight.js From 0587266e22c07645a8066b4a00467d8700472170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Fri, 24 Apr 2026 12:34:16 +0200 Subject: [PATCH 02/19] repair injection highlight and add image previews --- client/components/codeEditor/codeEditor.jsx | 102 ++++++++++++++++-- client/components/codeEditor/codeEditor.less | 38 +++++++ .../codeEditor/extensions/customHighlight.js | 8 +- 3 files changed, 134 insertions(+), 14 deletions(-) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index 3495570ff..d677dc65e 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -38,15 +38,30 @@ const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery }; const themeCompartment = new Compartment(); const highlightCompartment = new Compartment(); -console.log(themes); - import { generalKeymap, markdownKeymap } from './extensions/customKeyMaps.js'; import foldOnPages from './extensions/customFolding.js'; import { customHighlightStyle, tokenizeCustomMarkdown, tokenizeCustomCSS } from './extensions/customHighlight.js'; import { legacyCustomHighlightStyle, legacyTokenizeCustomMarkdown } from './extensions/legacyCustomHighlight.js'; +import { syntaxTree } from '@codemirror/language'; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; +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; +} 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 @@ -76,21 +91,84 @@ const createHighlightPlugin = (renderer, tab)=>{ let pageCount = 1; let snippetCount = 0; + const tree = syntaxTree(view.state); +tree.iterate({ + enter: (node) => { + if (node.name === "Image") { + const url = getUrl(node, view.state.doc); + + if (!url) return; + + decos.push( + Decoration.mark({ + class: "cm-image", + attributes: { + "style": `--preview-img:url(${url});` + } + }).range(node.from, node.to) + ); + } + } +}); + + tokens.forEach((tok)=>{ const line = view.state.doc.line(tok.line + 1); + // INLINE TOKENS (marks) 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)); + const from = line.from + tok.from; + const to = line.from + tok.to; + + const attrs = {}; + console.log(tok); + // attach URL only for links + if(tok.type === 'Image' && tok.url) { + + attrs['data-url'] = tok.url; } + + decos.push( + Decoration.mark({ + class : `cm-${tok.type}`, + ...(Object.keys(attrs).length + ? { attributes: attrs } + : {}) + }).range(from, to) + ); + } + + // LINE TOKENS + else { + decos.push( + Decoration.line({ + class : `cm-${tok.type}` + }).range(line.from) + ); + + if(tok.type === 'pageLine' && tab === 'brewText') { + pageCount++; + if(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.push( + Decoration.line({ + attributes : { + 'data-page-number' : snippetCount + } + }).range(line.from) + ); } } }); @@ -99,7 +177,9 @@ const createHighlightPlugin = (renderer, tab)=>{ return Decoration.set(decos); } }, - { decorations: (v)=>v.decorations } + { + decorations : (v)=>v.decorations + } ); }; diff --git a/client/components/codeEditor/codeEditor.less b/client/components/codeEditor/codeEditor.less index 84ed8058d..3594742fd 100644 --- a/client/components/codeEditor/codeEditor.less +++ b/client/components/codeEditor/codeEditor.less @@ -157,6 +157,44 @@ outline : 1px inset #00000055 !important; } + .cm-image[style] { + position: relative; + + &::before, &::after { + content:""; + width:100px; + height:100px; + position:absolute; + bottom:0; + left:0; + translate:0 100%; + display:block; + z-index:1000; + pointer-events: none; + opacity:0; + transition:0.2s opacity; + } + + &::before{ + background-color: #fff; + border-radius:10px; + border:3px solid grey; + } + + &::after { + background-image: var(--preview-img); + background-size:80%; + background-repeat:no-repeat; + background-position:center; + filter:drop-shadow(0 0 5px #0008); + } + + &:hover::before, + &:hover::after { + opacity:1; + } + } + /* Tab character visualization (optional) */ //.cm-tab { // background: url(...) no-repeat right; diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index 8797704cc..92761a78b 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -182,13 +182,15 @@ export function tokenizeCustomMarkdown(text) { } 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) { + console.log(match.indices[1][0]); tokens.push({ line : lineNumber, - from : match.index, - to : match.index + match[1].length, + from : match.indices[1][0], + to : match.indices[1][0] + match[1].length, type : customTags.injection, }); } From fac3ef15fae817928f517a9dcec595439851d40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Fri, 24 Apr 2026 12:42:14 +0200 Subject: [PATCH 03/19] format --- client/components/codeEditor/codeEditor.jsx | 34 ++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index d677dc65e..dbe042d73 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -51,9 +51,9 @@ function getUrl(node, doc) { const cursor = node.node.cursor(); - if (cursor.firstChild()) { + if(cursor.firstChild()) { do { - if (cursor.name === "URL") { + if(cursor.name === 'URL') { url = doc.sliceString(cursor.from, cursor.to); break; } @@ -92,24 +92,24 @@ const createHighlightPlugin = (renderer, tab)=>{ let snippetCount = 0; const tree = syntaxTree(view.state); -tree.iterate({ - enter: (node) => { - if (node.name === "Image") { - const url = getUrl(node, view.state.doc); + tree.iterate({ + enter : (node)=>{ + if(node.name === 'Image') { + const url = getUrl(node, view.state.doc); - if (!url) return; + if(!url) return; - decos.push( - Decoration.mark({ - class: "cm-image", - attributes: { - "style": `--preview-img:url(${url});` + decos.push( + Decoration.mark({ + class : 'cm-image', + attributes : { + 'style' : `--preview-img:url(${url});` + } + }).range(node.from, node.to) + ); + } } - }).range(node.from, node.to) - ); - } - } -}); + }); tokens.forEach((tok)=>{ From 8872adfd95ed1767dc369f35f7b8aa831f9835ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Fri, 24 Apr 2026 13:18:18 +0200 Subject: [PATCH 04/19] make sure tall images fit --- client/components/codeEditor/codeEditor.less | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/components/codeEditor/codeEditor.less b/client/components/codeEditor/codeEditor.less index 3594742fd..074d1e265 100644 --- a/client/components/codeEditor/codeEditor.less +++ b/client/components/codeEditor/codeEditor.less @@ -162,8 +162,6 @@ &::before, &::after { content:""; - width:100px; - height:100px; position:absolute; bottom:0; left:0; @@ -176,14 +174,20 @@ } &::before{ + width:200px; + height:200px; background-color: #fff; border-radius:10px; border:3px solid grey; } &::after { + width:190px; + height:190px; + //padding of 5px + 3px border: 8px + translate:8px calc(100% + 8px); background-image: var(--preview-img); - background-size:80%; + background-size:contain; background-repeat:no-repeat; background-position:center; filter:drop-shadow(0 0 5px #0008); From 793adafe3216a55d8ba5674d74ba2d0b69392982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 10 May 2026 19:35:09 +0200 Subject: [PATCH 05/19] fix work on first load for images inside other nodes --- client/components/codeEditor/codeEditor.jsx | 16 ++++++++++++---- .../codeEditor/extensions/customHighlight.js | 1 - 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index ef28bf509..90ab1fde0 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -17,7 +17,17 @@ import { crosshairCursor, } from '@codemirror/view'; import { EditorState, Compartment, StateEffect, StateField } from '@codemirror/state'; -import { foldAll as foldAllCmd, unfoldAll as unfoldAllCmd, foldGutter, foldKeymap, foldEffect, foldState, syntaxHighlighting } from '@codemirror/language'; +import { + foldAll as foldAllCmd, + unfoldAll as unfoldAllCmd, + foldGutter, + foldKeymap, + foldEffect, + foldState, + syntaxHighlighting, + syntaxTree, + ensureSyntaxTree +} 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'; @@ -41,7 +51,6 @@ import { generalKeymap, markdownKeymap } from './extensions/customKeyMaps.js'; import foldOnPages from './extensions/customFolding.js'; import { customHighlightStyle, tokenizeCustomMarkdown, tokenizeCustomCSS } from './extensions/customHighlight.js'; import { legacyCustomHighlightStyle, legacyTokenizeCustomMarkdown } from './extensions/legacyCustomHighlight.js'; -import { syntaxTree } from '@codemirror/language'; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; @@ -90,7 +99,7 @@ const createHighlightPlugin = (renderer, tab)=>{ let pageCount = 1; let snippetCount = 0; - const tree = syntaxTree(view.state); + const tree = ensureSyntaxTree(view.state, view.state.doc.length, 50) || syntaxTree(view.state); tree.iterate({ enter : (node)=>{ if(node.name === 'Image') { @@ -120,7 +129,6 @@ const createHighlightPlugin = (renderer, tab)=>{ const to = line.from + tok.to; const attrs = {}; - console.log(tok); // attach URL only for links if(tok.type === 'Image' && tok.url) { diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index 39103fdac..ad562e8f1 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -204,7 +204,6 @@ export function tokenizeCustomMarkdown(text) { let match; while ((match = injectionRegex.exec(lineText)) !== null) { - console.log(match.indices[1][0]); tokens.push({ line : lineNumber, from : match.indices[1][0], From 70d9614fb18325718b302e3008a603e5d0f4cad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 10 May 2026 19:39:26 +0200 Subject: [PATCH 06/19] lint --- client/components/codeEditor/codeEditor.jsx | 32 ++++++--------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index 90ab1fde0..c70e1109b 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -17,13 +17,13 @@ import { crosshairCursor, } from '@codemirror/view'; import { EditorState, Compartment, StateEffect, StateField } from '@codemirror/state'; -import { - foldAll as foldAllCmd, +import { + foldAll as foldAllCmd, unfoldAll as unfoldAllCmd, - foldGutter, - foldKeymap, - foldEffect, - foldState, + foldGutter, + foldKeymap, + foldEffect, + foldState, syntaxHighlighting, syntaxTree, ensureSyntaxTree @@ -152,30 +152,14 @@ const createHighlightPlugin = (renderer, tab)=>{ class : `cm-${tok.type}` }).range(line.from) ); - if(tok.type === 'pageLine' && tab === 'brewText') { pageCount++; if(line.from === 0) pageCount--; - - decos.push( - Decoration.line({ - attributes : { - 'data-page-number' : pageCount - } - }).range(line.from) - ); + 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.push(Decoration.line({ attributes: { 'data-page-number': snippetCount } }).range(line.from)); } } }); From aac384390b74428e1b9f0ebf81ad7fe30baa1cd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 10 May 2026 19:41:11 +0200 Subject: [PATCH 07/19] lint --- client/components/codeEditor/codeEditor.jsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index c70e1109b..ae91361cb 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -123,13 +123,11 @@ const createHighlightPlugin = (renderer, tab)=>{ tokens.forEach((tok)=>{ const line = view.state.doc.line(tok.line + 1); - // INLINE TOKENS (marks) if(tok.from != null && tok.to != null && tok.from < tok.to) { const from = line.from + tok.from; const to = line.from + tok.to; const attrs = {}; - // attach URL only for links if(tok.type === 'Image' && tok.url) { attrs['data-url'] = tok.url; @@ -143,10 +141,7 @@ const createHighlightPlugin = (renderer, tab)=>{ : {}) }).range(from, to) ); - } - - // LINE TOKENS - else { + } else { decos.push( Decoration.line({ class : `cm-${tok.type}` @@ -168,9 +163,7 @@ const createHighlightPlugin = (renderer, tab)=>{ return Decoration.set(decos); } }, - { - decorations : (v)=>v.decorations - } + { decorations: (v)=>v.decorations } ); }; From b39502d806a2caf337196806575558fc57256aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 10 May 2026 19:42:02 +0200 Subject: [PATCH 08/19] lint more --- client/components/codeEditor/codeEditor.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index ae91361cb..ba60bf125 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -119,7 +119,6 @@ const createHighlightPlugin = (renderer, tab)=>{ } }); - tokens.forEach((tok)=>{ const line = view.state.doc.line(tok.line + 1); @@ -450,7 +449,6 @@ const CodeEditor = forwardRef( injectText : (text)=>{ const view = viewRef.current; - view.dispatch( view.state.replaceSelection(text) ); From 2971fe0679c482a789c01e771a543bdab68b3797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 13 May 2026 11:47:00 +0200 Subject: [PATCH 09/19] add transition delay --- client/components/codeEditor/codeEditor.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/codeEditor/codeEditor.less b/client/components/codeEditor/codeEditor.less index da31e4c4d..16d99bc30 100644 --- a/client/components/codeEditor/codeEditor.less +++ b/client/components/codeEditor/codeEditor.less @@ -174,7 +174,7 @@ z-index:1000; pointer-events: none; opacity:0; - transition:0.2s opacity; + transition:0.2s opacity 0.5s; } &::before{ From 0f7aa0ad49aed5b2cd50110a2196d26b23868707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 13 May 2026 11:49:41 +0200 Subject: [PATCH 10/19] change tok for token --- client/components/codeEditor/codeEditor.jsx | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index ba60bf125..254d2977d 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -119,22 +119,22 @@ const createHighlightPlugin = (renderer, tab)=>{ } }); - tokens.forEach((tok)=>{ - const line = view.state.doc.line(tok.line + 1); + tokens.forEach((token)=>{ + const line = view.state.doc.line(token.line + 1); - if(tok.from != null && tok.to != null && tok.from < tok.to) { - const from = line.from + tok.from; - const to = line.from + tok.to; + 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(tok.type === 'Image' && tok.url) { + if(token.type === 'Image' && token.url) { - attrs['data-url'] = tok.url; + attrs['data-url'] = token.url; } decos.push( Decoration.mark({ - class : `cm-${tok.type}`, + class : `cm-${token.type}`, ...(Object.keys(attrs).length ? { attributes: attrs } : {}) @@ -143,15 +143,15 @@ const createHighlightPlugin = (renderer, tab)=>{ } else { decos.push( Decoration.line({ - class : `cm-${tok.type}` + class : `cm-${token.type}` }).range(line.from) ); - if(tok.type === 'pageLine' && tab === 'brewText') { + 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(tok.type === 'snippetLine' && tab === 'brewSnippets') { + if(token.type === 'snippetLine' && tab === 'brewSnippets') { snippetCount++; decos.push(Decoration.line({ attributes: { 'data-page-number': snippetCount } }).range(line.from)); } From df4c59cbd6b6a605eb1dcee77269d571e81ee98d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 13 May 2026 12:06:21 +0200 Subject: [PATCH 11/19] move highlight plugin into its own file --- client/components/codeEditor/codeEditor.jsx | 130 +---------------- .../codeEditor/extensions/customHighlight.js | 132 +++++++++++++++++- 2 files changed, 130 insertions(+), 132 deletions(-) diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index 254d2977d..c5cf669bd 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -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,7 +10,6 @@ import { highlightActiveLine, scrollPastEnd, Decoration, - ViewPlugin, drawSelection, dropCursor, rectangularSelection, @@ -18,15 +17,12 @@ import { } from '@codemirror/view'; import { EditorState, Compartment, StateEffect, StateField } from '@codemirror/state'; import { - foldAll as foldAllCmd, unfoldAll as unfoldAllCmd, foldGutter, foldKeymap, foldEffect, foldState, syntaxHighlighting, - syntaxTree, - ensureSyntaxTree } from '@codemirror/language'; import { defaultKeymap, history, undo, redo, undoDepth, redoDepth } from '@codemirror/commands'; import { languages } from '@codemirror/language-data'; @@ -49,123 +45,11 @@ const highlightCompartment = new Compartment(); import { generalKeymap, markdownKeymap } from './extensions/customKeyMaps.js'; import foldOnPages from './extensions/customFolding.js'; -import { customHighlightStyle, tokenizeCustomMarkdown, tokenizeCustomCSS } from './extensions/customHighlight.js'; -import { legacyCustomHighlightStyle, legacyTokenizeCustomMarkdown } from './extensions/legacyCustomHighlight.js'; +import { customHighlightPlugin, customHighlightStyle } from './extensions/customHighlight.js'; +import { legacyCustomHighlightStyle } from './extensions/legacyCustomHighlight.js'; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; -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; -} -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; - - 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); - - if(!url) return; - - decos.push( - Decoration.mark({ - class : 'cm-image', - attributes : { - 'style' : `--preview-img:url(${url});` - } - }).range(node.from, node.to) - ); - } - } - }); - - 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 } - ); -}; - const setProgrammaticCursorLine = StateEffect.define(); const programmaticCursorLineField = StateField.define({ @@ -272,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']; @@ -296,7 +178,7 @@ const CodeEditor = forwardRef( }), //highlights - highlightCompartment.of([customHighlightPlugin, highlightExtension]), + highlightCompartment.of([customHighlightPlugin(renderer, tab), highlightExtension]), themeCompartment.of(themeExtension), highlightActiveLine(), highlightActiveLineGutter(), @@ -437,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]); diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index ad562e8f1..afccea2d6 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -1,5 +1,15 @@ +/* eslint max-lines: ["error", { "max": 450 }] */ import { HighlightStyle } from '@codemirror/language'; import { tags } from '@lezer/highlight'; +import { legacyTokenizeCustomMarkdown } from './legacyCustomHighlight'; +import { + Decoration, + ViewPlugin, +} from '@codemirror/view'; +import { + syntaxTree, + ensureSyntaxTree +} from '@codemirror/language'; // Making the tokens const customTags = { @@ -23,7 +33,7 @@ const customTags = { variable : 'variable', }; -export function tokenizeCustomMarkdown(text) { +function tokenizeCustomMarkdown(text) { const tokens = []; const lines = text.split('\n'); @@ -86,8 +96,8 @@ export function tokenizeCustomMarkdown(text) { if(/\~/.test(lineText)) { const strikethroughRegex = /~(?!\s)(.+?)(?{ + if(node.name === 'Image') { + const url = getUrl(node, view.state.doc); + + if(!url) return; + + decos.push( + Decoration.mark({ + class : 'cm-image', + attributes : { + 'style' : `--preview-img:url(${url});` + } + }).range(node.from, node.to) + ); + } + } + }); + + 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 } + ); +}; \ No newline at end of file From c66310fbe7401cc1de7e0a0fcb2d4b8074830c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 13 May 2026 12:09:43 +0200 Subject: [PATCH 12/19] linting --- client/components/codeEditor/extensions/customHighlight.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index afccea2d6..63251e49e 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -316,7 +316,6 @@ export const customHighlightStyle = HighlightStyle.define([ ]); - function getUrl(node, doc) { let url = null; @@ -333,6 +332,7 @@ function getUrl(node, doc) { return 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 From 097b941580dad433f1c0c181b3087c7072aa7574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 13 May 2026 12:45:46 +0200 Subject: [PATCH 13/19] move to use html img element --- client/components/codeEditor/codeEditor.less | 40 ++++++------------ .../codeEditor/extensions/customHighlight.js | 40 ++++++++++++++++-- client/icons/Broken-image-icon-in-Chrome.png | Bin 0 -> 9227 bytes 3 files changed, 48 insertions(+), 32 deletions(-) create mode 100644 client/icons/Broken-image-icon-in-Chrome.png diff --git a/client/components/codeEditor/codeEditor.less b/client/components/codeEditor/codeEditor.less index 16d99bc30..fbb2cacaa 100644 --- a/client/components/codeEditor/codeEditor.less +++ b/client/components/codeEditor/codeEditor.less @@ -161,44 +161,28 @@ outline : 1px inset #00000055 !important; } - .cm-image[style] { - position: relative; - - &::before, &::after { - content:""; + .cm-image { + position:relative; + + .cm-preview { + object-fit: contain; position:absolute; bottom:0; left:0; - translate:0 100%; - display:block; - z-index:1000; - pointer-events: none; - opacity:0; - transition:0.2s opacity 0.5s; - } - - &::before{ + height:200px; width:200px; height:200px; 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; } - &::after { - width:190px; - height:190px; - //padding of 5px + 3px border: 8px - translate:8px calc(100% + 8px); - background-image: var(--preview-img); - background-size:contain; - background-repeat:no-repeat; - background-position:center; - filter:drop-shadow(0 0 5px #0008); - } - - &:hover::before, - &:hover::after { + &:hover .cm-preview { opacity:1; } } diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index 63251e49e..2a63bb398 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -333,6 +333,33 @@ function getUrl(node, doc) { 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-icon-in-Chrome.png'; + }; + + 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 @@ -368,16 +395,21 @@ export function customHighlightPlugin(renderer, tab) { if(node.name === 'Image') { const url = getUrl(node, view.state.doc); + console.log(node.node.lastChild.from); + if(!url) return; decos.push( Decoration.mark({ - class : 'cm-image', - attributes : { - 'style' : `--preview-img:url(${url});` - } + class : 'cm-image' }).range(node.from, node.to) ); + decos.push( + Decoration.widget({ + widget : new ImageWidget(url), + side : 1 + }).range(node.node.lastChild.from) + ); } } }); diff --git a/client/icons/Broken-image-icon-in-Chrome.png b/client/icons/Broken-image-icon-in-Chrome.png new file mode 100644 index 0000000000000000000000000000000000000000..6c4d4fd4058dafcff3b47f056aca571b739b6163 GIT binary patch literal 9227 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 F@*masY7PJZ literal 0 HcmV?d00001 From 3c4bafd1ad7ab139e3d962445faad5fa93875d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 13 May 2026 12:46:10 +0200 Subject: [PATCH 14/19] eslint changes --- client/components/codeEditor/extensions/customHighlight.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index 2a63bb398..79a4a18c9 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -1,4 +1,4 @@ -/* eslint max-lines: ["error", { "max": 450 }] */ +/* eslint max-lines: ["error", { "max": 500 }] */ import { HighlightStyle } from '@codemirror/language'; import { tags } from '@lezer/highlight'; import { legacyTokenizeCustomMarkdown } from './legacyCustomHighlight'; From d630882f18f03c21fd4be0ba20c7a125c963a168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 13 May 2026 12:51:45 +0200 Subject: [PATCH 15/19] add padding --- client/components/codeEditor/codeEditor.less | 1 + 1 file changed, 1 insertion(+) diff --git a/client/components/codeEditor/codeEditor.less b/client/components/codeEditor/codeEditor.less index fbb2cacaa..f3658f290 100644 --- a/client/components/codeEditor/codeEditor.less +++ b/client/components/codeEditor/codeEditor.less @@ -172,6 +172,7 @@ height:200px; width:200px; height:200px; + padding:5px; background-color: #fff; border-radius:10px; border:3px solid grey; From 0bee33efc0ff85c3e231a67a6b158c13dd5dc5c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 13 May 2026 20:58:40 +0200 Subject: [PATCH 16/19] file removal --- client/icons/Broken-image-icon-in-Chrome.png | Bin 9227 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 client/icons/Broken-image-icon-in-Chrome.png diff --git a/client/icons/Broken-image-icon-in-Chrome.png b/client/icons/Broken-image-icon-in-Chrome.png deleted file mode 100644 index 6c4d4fd4058dafcff3b47f056aca571b739b6163..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9227 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 F@*masY7PJZ From 16948f451fe86ec6dd4d266c204b9538d80f2010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Thu, 14 May 2026 11:08:09 +0200 Subject: [PATCH 17/19] add a comment --- client/components/codeEditor/extensions/customHighlight.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index 79a4a18c9..410bfe0d1 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -395,7 +395,10 @@ export function customHighlightPlugin(renderer, tab) { if(node.name === 'Image') { const url = getUrl(node, view.state.doc); - console.log(node.node.lastChild.from); + 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; @@ -408,7 +411,7 @@ export function customHighlightPlugin(renderer, tab) { Decoration.widget({ widget : new ImageWidget(url), side : 1 - }).range(node.node.lastChild.from) + }).range(widgetPosition) ); } } From 074af5e8e0dba553ba3e30c75cb828f7acea5fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sat, 16 May 2026 16:28:53 +0200 Subject: [PATCH 18/19] more solid block tokenizing --- client/components/codeEditor/codeEditor.less | 3 ++- .../codeEditor/extensions/customHighlight.js | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/client/components/codeEditor/codeEditor.less b/client/components/codeEditor/codeEditor.less index f3658f290..ca77f29a8 100644 --- a/client/components/codeEditor/codeEditor.less +++ b/client/components/codeEditor/codeEditor.less @@ -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; } diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index 410bfe0d1..9f08c2e1a 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -250,7 +250,16 @@ 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) { + console.log('closing', closingMatch); + console.log(closingMatch.indices[1][0], closingMatch.indices[1][1]) + 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 }); + } + } }); From 7a0348536cb8afdd3655a798b3300c612f74ca41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sat, 23 May 2026 09:18:04 +0200 Subject: [PATCH 19/19] fix image request going crazy --- .../codeEditor/extensions/customHighlight.js | 4 +--- client/icons/broken-image.jpg | Bin 0 -> 14669 bytes 2 files changed, 1 insertion(+), 3 deletions(-) create mode 100644 client/icons/broken-image.jpg diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index 9f08c2e1a..33c68da7a 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -253,8 +253,6 @@ function tokenizeCustomMarkdown(text) { const closingMatch = lineText.match(/ *(}})/d); if(closingMatch) { - console.log('closing', closingMatch); - console.log(closingMatch.indices[1][0], closingMatch.indices[1][1]) 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 }); @@ -358,7 +356,7 @@ class ImageWidget extends WidgetType { img.onerror = ()=>{ - img.src = 'client/icons/Broken-image-icon-in-Chrome.png'; + img.src = 'client/icons/broken-image.jpg'; }; return img; diff --git a/client/icons/broken-image.jpg b/client/icons/broken-image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..52fa33195d2f902715a918e7d9610049cf670487 GIT binary patch literal 14669 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