Skip to content

Commit 2954933

Browse files
authored
Improve volto-code-block WP (#29)
* highlight js * improvments * ensure backwards compatibility * test * fixes
1 parent bb497a3 commit 2954933

File tree

19 files changed

+4683
-1544
lines changed

19 files changed

+4683
-1544
lines changed

packages/volto-code-block/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@
3030
},
3131
"dependencies": {
3232
"mermaid": "10.8.0",
33-
"prismjs": "1.29.0",
34-
"react-gist": "1.2.4"
33+
"react-syntax-highlighter": "^15.5.0",
34+
"react-gist": "1.2.4",
35+
"highlight.js": "^11.9.0"
3536
},
3637
"peerDependencies": {
3738
"react": "18.2.0",

packages/volto-code-block/src/components/Blocks/Code/Edit.jsx

Lines changed: 237 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,91 @@ import { SidebarPortal } from '@plone/volto/components';
44
import config from '@plone/volto/registry';
55
import CodeBlockData from './Data';
66
import Caption from '../../Caption/Caption.jsx';
7-
import Editor from '../../Editor/Editor.tsx';
8-
import { highlight } from 'prismjs/components/prism-core';
7+
import hljs from 'highlight.js/lib/core';
8+
import javascript from 'highlight.js/lib/languages/javascript';
9+
import python from 'highlight.js/lib/languages/python';
10+
import xml from 'highlight.js/lib/languages/xml';
11+
import css from 'highlight.js/lib/languages/css';
12+
import json from 'highlight.js/lib/languages/json';
13+
import bash from 'highlight.js/lib/languages/bash';
14+
import c from 'highlight.js/lib/languages/c';
15+
import cpp from 'highlight.js/lib/languages/cpp';
16+
import csharp from 'highlight.js/lib/languages/csharp';
17+
import diff from 'highlight.js/lib/languages/diff';
18+
import go from 'highlight.js/lib/languages/go';
19+
import java from 'highlight.js/lib/languages/java';
20+
import kotlin from 'highlight.js/lib/languages/kotlin';
21+
import less from 'highlight.js/lib/languages/less';
22+
import lua from 'highlight.js/lib/languages/lua';
23+
import makefile from 'highlight.js/lib/languages/makefile';
24+
import markdown from 'highlight.js/lib/languages/markdown';
25+
import objectivec from 'highlight.js/lib/languages/objectivec';
26+
import perl from 'highlight.js/lib/languages/perl';
27+
import php from 'highlight.js/lib/languages/php';
28+
import ruby from 'highlight.js/lib/languages/ruby';
29+
import rust from 'highlight.js/lib/languages/rust';
30+
import scss from 'highlight.js/lib/languages/scss';
31+
import sql from 'highlight.js/lib/languages/sql';
32+
import swift from 'highlight.js/lib/languages/swift';
33+
import typescript from 'highlight.js/lib/languages/typescript';
34+
import yaml from 'highlight.js/lib/languages/yaml';
35+
import dockerfile from 'highlight.js/lib/languages/dockerfile';
36+
import nginx from 'highlight.js/lib/languages/nginx';
37+
import powershell from 'highlight.js/lib/languages/powershell';
38+
import r from 'highlight.js/lib/languages/r';
39+
import 'highlight.js/styles/github.css';
40+
41+
// Register languages
42+
hljs.registerLanguage('javascript', javascript);
43+
hljs.registerLanguage('python', python);
44+
hljs.registerLanguage('xml', xml);
45+
hljs.registerLanguage('html', xml);
46+
hljs.registerLanguage('css', css);
47+
hljs.registerLanguage('json', json);
48+
hljs.registerLanguage('bash', bash);
49+
hljs.registerLanguage('shell', bash);
50+
hljs.registerLanguage('c', c);
51+
hljs.registerLanguage('cpp', cpp);
52+
hljs.registerLanguage('csharp', csharp);
53+
hljs.registerLanguage('diff', diff);
54+
hljs.registerLanguage('go', go);
55+
hljs.registerLanguage('java', java);
56+
hljs.registerLanguage('kotlin', kotlin);
57+
hljs.registerLanguage('less', less);
58+
hljs.registerLanguage('lua', lua);
59+
hljs.registerLanguage('makefile', makefile);
60+
hljs.registerLanguage('markdown', markdown);
61+
hljs.registerLanguage('objectivec', objectivec);
62+
hljs.registerLanguage('perl', perl);
63+
hljs.registerLanguage('php', php);
64+
hljs.registerLanguage('ruby', ruby);
65+
hljs.registerLanguage('rust', rust);
66+
hljs.registerLanguage('scss', scss);
67+
hljs.registerLanguage('sql', sql);
68+
hljs.registerLanguage('swift', swift);
69+
hljs.registerLanguage('typescript', typescript);
70+
hljs.registerLanguage('yaml', yaml);
71+
hljs.registerLanguage('dockerfile', dockerfile);
72+
hljs.registerLanguage('nginx', nginx);
73+
hljs.registerLanguage('powershell', powershell);
74+
hljs.registerLanguage('r', r);
75+
76+
// Register aliases
77+
hljs.registerLanguage('jsx', javascript);
78+
hljs.registerLanguage('tsx', typescript);
79+
hljs.registerLanguage('text', () => ({})); // Plain text
80+
hljs.registerLanguage('plain', () => ({})); // Plain text for backwards compatibility
81+
hljs.registerLanguage('mermaid', () => ({})); // Mermaid as plain text for backwards compatibility
82+
hljs.registerLanguage('docker', dockerfile);
83+
hljs.registerLanguage('batch', powershell);
84+
hljs.registerLanguage('fish', bash);
85+
hljs.registerLanguage('zsh', bash);
986

1087
const CodeBlockEdit = (props) => {
1188
const { data, selected, block, onChangeBlock } = props;
1289
const [code, setCode] = React.useState(data.code || '');
90+
const codeRef = React.useRef(null);
91+
const cursorPositionRef = React.useRef(0);
1392
const className = `code-block-wrapper edit ${data.style}`;
1493
const codeBlockConfig = config.blocks?.blocksConfig?.codeBlock;
1594
const defaultLanguage = codeBlockConfig?.defaultLanguage;
@@ -19,23 +98,168 @@ const CodeBlockEdit = (props) => {
1998
data.style = defaultStyle;
2099
}
21100
const allLanguages = config.settings.codeBlock.languages;
22-
const language = allLanguages[data.language].language;
101+
const language =
102+
data.language && allLanguages[data.language]
103+
? data.language
104+
: defaultLanguage || 'javascript';
105+
106+
// Create highlight function using highlight.js
107+
const highlightCode = (code) => {
108+
if (!code) return code;
109+
try {
110+
const result = hljs.highlight(code, { language: language });
111+
return result.value;
112+
} catch (err) {
113+
// Fallback to auto-detection or plain text
114+
try {
115+
const result = hljs.highlightAuto(code);
116+
return result.value;
117+
} catch (err2) {
118+
// Return code with basic escaping
119+
return code
120+
.replace(/&/g, '&')
121+
.replace(/</g, '&lt;')
122+
.replace(/>/g, '&gt;');
123+
}
124+
}
125+
};
126+
127+
const processCodeWithLineNumbers = (code) => {
128+
if (!data.showLineNumbers) {
129+
return highlightCode(code);
130+
}
131+
132+
const lines = code.split('\n');
133+
// Remove only the last empty line if it exists
134+
if (lines.length > 1 && lines[lines.length - 1] === '') {
135+
lines.pop();
136+
}
137+
const startNum = parseInt(data.lineNbr, 10) || 1;
138+
139+
// Process each line individually and add line numbers
140+
const processedLines = lines.map((line, index) => {
141+
try {
142+
const result = hljs.highlight(line, { language: language });
143+
return `<span class="code-line" data-line="${startNum + index}">${result.value}</span>`;
144+
} catch (err) {
145+
try {
146+
const result = hljs.highlightAuto(line);
147+
return `<span class="code-line" data-line="${startNum + index}">${result.value}</span>`;
148+
} catch (err2) {
149+
return `<span class="code-line" data-line="${startNum + index}">${line}</span>`;
150+
}
151+
}
152+
});
153+
154+
return processedLines.join('\n');
155+
};
156+
23157
const { caption_title, caption_description } = data;
24-
const handleChange = (code) => {
25-
setCode(code);
26-
onChangeBlock(block, { ...data, code: code });
158+
159+
// Save and restore cursor position
160+
const saveCursorPosition = () => {
161+
const selection = window.getSelection();
162+
if (selection.rangeCount > 0) {
163+
const range = selection.getRangeAt(0);
164+
const preCaretRange = range.cloneRange();
165+
preCaretRange.selectNodeContents(codeRef.current);
166+
preCaretRange.setEnd(range.endContainer, range.endOffset);
167+
return preCaretRange.toString().length;
168+
}
169+
return 0;
27170
};
28171

172+
const restoreCursorPosition = (position) => {
173+
const selection = window.getSelection();
174+
const range = document.createRange();
175+
let charCount = 0;
176+
let nodeStack = [codeRef.current];
177+
let node;
178+
let foundStart = false;
179+
180+
while (!foundStart && (node = nodeStack.pop())) {
181+
if (node.nodeType === Node.TEXT_NODE) {
182+
const nextCharCount = charCount + node.textContent.length;
183+
if (position >= charCount && position <= nextCharCount) {
184+
range.setStart(node, position - charCount);
185+
range.setEnd(node, position - charCount);
186+
foundStart = true;
187+
}
188+
charCount = nextCharCount;
189+
} else {
190+
for (let i = node.childNodes.length - 1; i >= 0; i--) {
191+
nodeStack.push(node.childNodes[i]);
192+
}
193+
}
194+
}
195+
196+
if (foundStart) {
197+
selection.removeAllRanges();
198+
selection.addRange(range);
199+
}
200+
};
201+
202+
const handleChange = (newCode) => {
203+
const cursorPosition = saveCursorPosition();
204+
cursorPositionRef.current = cursorPosition;
205+
setCode(newCode);
206+
onChangeBlock(block, { ...data, code: newCode });
207+
};
208+
209+
// Restore cursor position after re-render
210+
React.useLayoutEffect(() => {
211+
if (cursorPositionRef.current > 0) {
212+
restoreCursorPosition(cursorPositionRef.current);
213+
cursorPositionRef.current = 0;
214+
}
215+
}, [code]);
216+
217+
29218
return (
30219
<div className="block code">
31220
<div className={className}>
32-
<Editor
33-
value={code}
34-
onValueChange={(code) => handleChange(code)}
35-
highlight={(code) => highlight(code, language)}
36-
padding={10}
37-
preClassName={`code-block-wrapper ${data.style} language-${data.language}`}
38-
/>
221+
<div className={`code-editor-container ${data.showLineNumbers ? 'with-line-numbers' : ''}`}>
222+
<pre className={`code-editor-with-highlighting language-${language}`}>
223+
<code
224+
ref={codeRef}
225+
className={`language-${language}`}
226+
contentEditable
227+
suppressContentEditableWarning={true}
228+
onInput={(e) => handleChange(e.target.textContent)}
229+
onKeyDown={(e) => {
230+
if (e.key === 'Enter') {
231+
e.preventDefault();
232+
const selection = window.getSelection();
233+
if (selection.rangeCount > 0) {
234+
const range = selection.getRangeAt(0);
235+
const textNode = document.createTextNode('\n');
236+
range.insertNode(textNode);
237+
range.setStartAfter(textNode);
238+
range.setEndAfter(textNode);
239+
selection.removeAllRanges();
240+
selection.addRange(range);
241+
handleChange(e.target.textContent);
242+
}
243+
}
244+
}}
245+
style={{
246+
fontFamily:
247+
'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace',
248+
fontSize: '14px',
249+
lineHeight: '1.4',
250+
outline: 'none',
251+
whiteSpace: data.wrapLongLines ? 'pre-wrap' : 'pre',
252+
minHeight: '200px',
253+
display: 'block',
254+
padding: data.showLineNumbers ? '15px 15px 15px 60px' : '15px',
255+
margin: 0,
256+
border: 0,
257+
}}
258+
dangerouslySetInnerHTML={{ __html: data.showLineNumbers ? processCodeWithLineNumbers(code) : highlightCode(code) }}
259+
/>
260+
</pre>
261+
</div>
262+
39263
{caption_title && (
40264
<Caption title={caption_title} description={caption_description} />
41265
)}

packages/volto-code-block/src/components/Blocks/Mermaid/Edit.jsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import React from 'react';
22
import { withBlockExtensions } from '@plone/volto/helpers';
3-
import config from '@plone/volto/registry';
43
import Editor from '../../Editor/Editor.tsx';
5-
import { highlight } from 'prismjs/components/prism-core';
4+
import Prism from 'prismjs/components/prism-core';
5+
6+
// Register mermaid as a simple text language for Prism.js
7+
Prism.languages.mermaid = Prism.languages.text;
68

79
const MermaidBlockEdit = (props) => {
810
const { data, block, onChangeBlock } = props;
911
const [code, setCode] = React.useState(data.code || '');
1012
const className = `code-block-wrapper edit light`;
11-
const allLanguages = config.settings.codeBlock.languages;
12-
const language = allLanguages['mermaid'].language;
13+
const language = 'mermaid';
1314

1415
const handleChange = (code) => {
1516
setCode(code);
@@ -22,7 +23,7 @@ const MermaidBlockEdit = (props) => {
2223
<Editor
2324
value={code}
2425
onValueChange={(code) => handleChange(code)}
25-
highlight={(code) => highlight(code, language)}
26+
highlight={(code) => Prism.highlight(code, Prism.languages.mermaid, language)}
2627
padding={10}
2728
preClassName={`code-block-wrapper ${data.style} language-mermaid`}
2829
/>

0 commit comments

Comments
 (0)