Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/volto-code-block/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@
},
"dependencies": {
"mermaid": "10.8.0",
"prismjs": "1.29.0",
"react-gist": "1.2.4"
"react-syntax-highlighter": "^15.5.0",
"react-gist": "1.2.4",
"highlight.js": "^11.9.0"
},
"peerDependencies": {
"react": "18.2.0",
Expand Down
250 changes: 237 additions & 13 deletions packages/volto-code-block/src/components/Blocks/Code/Edit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,91 @@ import { SidebarPortal } from '@plone/volto/components';
import config from '@plone/volto/registry';
import CodeBlockData from './Data';
import Caption from '../../Caption/Caption.jsx';
import Editor from '../../Editor/Editor.tsx';
import { highlight } from 'prismjs/components/prism-core';
import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';
import python from 'highlight.js/lib/languages/python';
import xml from 'highlight.js/lib/languages/xml';
import css from 'highlight.js/lib/languages/css';
import json from 'highlight.js/lib/languages/json';
import bash from 'highlight.js/lib/languages/bash';
import c from 'highlight.js/lib/languages/c';
import cpp from 'highlight.js/lib/languages/cpp';
import csharp from 'highlight.js/lib/languages/csharp';
import diff from 'highlight.js/lib/languages/diff';
import go from 'highlight.js/lib/languages/go';
import java from 'highlight.js/lib/languages/java';
import kotlin from 'highlight.js/lib/languages/kotlin';
import less from 'highlight.js/lib/languages/less';
import lua from 'highlight.js/lib/languages/lua';
import makefile from 'highlight.js/lib/languages/makefile';
import markdown from 'highlight.js/lib/languages/markdown';
import objectivec from 'highlight.js/lib/languages/objectivec';
import perl from 'highlight.js/lib/languages/perl';
import php from 'highlight.js/lib/languages/php';
import ruby from 'highlight.js/lib/languages/ruby';
import rust from 'highlight.js/lib/languages/rust';
import scss from 'highlight.js/lib/languages/scss';
import sql from 'highlight.js/lib/languages/sql';
import swift from 'highlight.js/lib/languages/swift';
import typescript from 'highlight.js/lib/languages/typescript';
import yaml from 'highlight.js/lib/languages/yaml';
import dockerfile from 'highlight.js/lib/languages/dockerfile';
import nginx from 'highlight.js/lib/languages/nginx';
import powershell from 'highlight.js/lib/languages/powershell';
import r from 'highlight.js/lib/languages/r';
import 'highlight.js/styles/github.css';

// Register languages
hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('python', python);
hljs.registerLanguage('xml', xml);
hljs.registerLanguage('html', xml);
hljs.registerLanguage('css', css);
hljs.registerLanguage('json', json);
hljs.registerLanguage('bash', bash);
hljs.registerLanguage('shell', bash);
hljs.registerLanguage('c', c);
hljs.registerLanguage('cpp', cpp);
hljs.registerLanguage('csharp', csharp);
hljs.registerLanguage('diff', diff);
hljs.registerLanguage('go', go);
hljs.registerLanguage('java', java);
hljs.registerLanguage('kotlin', kotlin);
hljs.registerLanguage('less', less);
hljs.registerLanguage('lua', lua);
hljs.registerLanguage('makefile', makefile);
hljs.registerLanguage('markdown', markdown);
hljs.registerLanguage('objectivec', objectivec);
hljs.registerLanguage('perl', perl);
hljs.registerLanguage('php', php);
hljs.registerLanguage('ruby', ruby);
hljs.registerLanguage('rust', rust);
hljs.registerLanguage('scss', scss);
hljs.registerLanguage('sql', sql);
hljs.registerLanguage('swift', swift);
hljs.registerLanguage('typescript', typescript);
hljs.registerLanguage('yaml', yaml);
hljs.registerLanguage('dockerfile', dockerfile);
hljs.registerLanguage('nginx', nginx);
hljs.registerLanguage('powershell', powershell);
hljs.registerLanguage('r', r);

// Register aliases
hljs.registerLanguage('jsx', javascript);
hljs.registerLanguage('tsx', typescript);
hljs.registerLanguage('text', () => ({})); // Plain text
hljs.registerLanguage('plain', () => ({})); // Plain text for backwards compatibility
hljs.registerLanguage('mermaid', () => ({})); // Mermaid as plain text for backwards compatibility
hljs.registerLanguage('docker', dockerfile);
hljs.registerLanguage('batch', powershell);
hljs.registerLanguage('fish', bash);
hljs.registerLanguage('zsh', bash);

const CodeBlockEdit = (props) => {
const { data, selected, block, onChangeBlock } = props;
const [code, setCode] = React.useState(data.code || '');
const codeRef = React.useRef(null);
const cursorPositionRef = React.useRef(0);
const className = `code-block-wrapper edit ${data.style}`;
const codeBlockConfig = config.blocks?.blocksConfig?.codeBlock;
const defaultLanguage = codeBlockConfig?.defaultLanguage;
Expand All @@ -19,23 +98,168 @@ const CodeBlockEdit = (props) => {
data.style = defaultStyle;
}
const allLanguages = config.settings.codeBlock.languages;
const language = allLanguages[data.language].language;
const language =
data.language && allLanguages[data.language]
? data.language
: defaultLanguage || 'javascript';

// Create highlight function using highlight.js
const highlightCode = (code) => {
if (!code) return code;
try {
const result = hljs.highlight(code, { language: language });
return result.value;
} catch (err) {
// Fallback to auto-detection or plain text
try {
const result = hljs.highlightAuto(code);
return result.value;
} catch (err2) {
// Return code with basic escaping
return code
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
}
};

const processCodeWithLineNumbers = (code) => {
if (!data.showLineNumbers) {
return highlightCode(code);
}

const lines = code.split('\n');
// Remove only the last empty line if it exists
if (lines.length > 1 && lines[lines.length - 1] === '') {
lines.pop();
}
const startNum = parseInt(data.lineNbr, 10) || 1;

// Process each line individually and add line numbers
const processedLines = lines.map((line, index) => {
try {
const result = hljs.highlight(line, { language: language });
return `<span class="code-line" data-line="${startNum + index}">${result.value}</span>`;
} catch (err) {
try {
const result = hljs.highlightAuto(line);
return `<span class="code-line" data-line="${startNum + index}">${result.value}</span>`;
} catch (err2) {
return `<span class="code-line" data-line="${startNum + index}">${line}</span>`;
}
}
});

return processedLines.join('\n');
};

const { caption_title, caption_description } = data;
const handleChange = (code) => {
setCode(code);
onChangeBlock(block, { ...data, code: code });

// Save and restore cursor position
const saveCursorPosition = () => {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(codeRef.current);
preCaretRange.setEnd(range.endContainer, range.endOffset);
return preCaretRange.toString().length;
}
return 0;
};

const restoreCursorPosition = (position) => {
const selection = window.getSelection();
const range = document.createRange();
let charCount = 0;
let nodeStack = [codeRef.current];
let node;
let foundStart = false;

while (!foundStart && (node = nodeStack.pop())) {
if (node.nodeType === Node.TEXT_NODE) {
const nextCharCount = charCount + node.textContent.length;
if (position >= charCount && position <= nextCharCount) {
range.setStart(node, position - charCount);
range.setEnd(node, position - charCount);
foundStart = true;
}
charCount = nextCharCount;
} else {
for (let i = node.childNodes.length - 1; i >= 0; i--) {
nodeStack.push(node.childNodes[i]);
}
}
}

if (foundStart) {
selection.removeAllRanges();
selection.addRange(range);
}
};

const handleChange = (newCode) => {
const cursorPosition = saveCursorPosition();
cursorPositionRef.current = cursorPosition;
setCode(newCode);
onChangeBlock(block, { ...data, code: newCode });
};

// Restore cursor position after re-render
React.useLayoutEffect(() => {
if (cursorPositionRef.current > 0) {
restoreCursorPosition(cursorPositionRef.current);
cursorPositionRef.current = 0;
}
}, [code]);


return (
<div className="block code">
<div className={className}>
<Editor
value={code}
onValueChange={(code) => handleChange(code)}
highlight={(code) => highlight(code, language)}
padding={10}
preClassName={`code-block-wrapper ${data.style} language-${data.language}`}
/>
<div className={`code-editor-container ${data.showLineNumbers ? 'with-line-numbers' : ''}`}>
<pre className={`code-editor-with-highlighting language-${language}`}>
<code
ref={codeRef}
className={`language-${language}`}
contentEditable
suppressContentEditableWarning={true}
onInput={(e) => handleChange(e.target.textContent)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const textNode = document.createTextNode('\n');
range.insertNode(textNode);
range.setStartAfter(textNode);
range.setEndAfter(textNode);
selection.removeAllRanges();
selection.addRange(range);
handleChange(e.target.textContent);
}
}
}}
style={{
fontFamily:
'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace',
fontSize: '14px',
lineHeight: '1.4',
outline: 'none',
whiteSpace: data.wrapLongLines ? 'pre-wrap' : 'pre',
minHeight: '200px',
display: 'block',
padding: data.showLineNumbers ? '15px 15px 15px 60px' : '15px',
margin: 0,
border: 0,
}}
dangerouslySetInnerHTML={{ __html: data.showLineNumbers ? processCodeWithLineNumbers(code) : highlightCode(code) }}
/>
</pre>
</div>

{caption_title && (
<Caption title={caption_title} description={caption_description} />
)}
Expand Down
11 changes: 6 additions & 5 deletions packages/volto-code-block/src/components/Blocks/Mermaid/Edit.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import React from 'react';
import { withBlockExtensions } from '@plone/volto/helpers';
import config from '@plone/volto/registry';
import Editor from '../../Editor/Editor.tsx';
import { highlight } from 'prismjs/components/prism-core';
import Prism from 'prismjs/components/prism-core';

// Register mermaid as a simple text language for Prism.js
Prism.languages.mermaid = Prism.languages.text;

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

const handleChange = (code) => {
setCode(code);
Expand All @@ -22,7 +23,7 @@ const MermaidBlockEdit = (props) => {
<Editor
value={code}
onValueChange={(code) => handleChange(code)}
highlight={(code) => highlight(code, language)}
highlight={(code) => Prism.highlight(code, Prism.languages.mermaid, language)}
padding={10}
preClassName={`code-block-wrapper ${data.style} language-mermaid`}
/>
Expand Down
Loading
Loading