diff --git a/package-lock.json b/package-lock.json
index 0d2530c..1ed4753 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,9 +14,11 @@
"@lexical/rich-text": "^0.37.0",
"@lexical/selection": "^0.37.0",
"@lexical/utils": "^0.37.0",
+ "@types/prismjs": "^1.26.5",
"i18next": "^25.5.3",
"i18next-browser-languagedetector": "^8.2.0",
"lexical": "^0.37.0",
+ "prismjs": "^1.30.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-i18next": "^16.0.0",
@@ -1342,6 +1344,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/prismjs": {
+ "version": "1.26.5",
+ "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
+ "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
+ "license": "MIT"
+ },
"node_modules/@types/react": {
"version": "19.1.16",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.16.tgz",
diff --git a/package.json b/package.json
index 7ca73d1..4fab152 100644
--- a/package.json
+++ b/package.json
@@ -16,9 +16,11 @@
"@lexical/rich-text": "^0.37.0",
"@lexical/selection": "^0.37.0",
"@lexical/utils": "^0.37.0",
+ "@types/prismjs": "^1.26.5",
"i18next": "^25.5.3",
"i18next-browser-languagedetector": "^8.2.0",
"lexical": "^0.37.0",
+ "prismjs": "^1.30.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-i18next": "^16.0.0",
diff --git a/src/editor/RichTextEditor.tsx b/src/editor/RichTextEditor.tsx
index da7bcf5..089d268 100644
--- a/src/editor/RichTextEditor.tsx
+++ b/src/editor/RichTextEditor.tsx
@@ -4,11 +4,12 @@ import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
-import { CodeNode } from '@lexical/code';
+import { CodeNode, CodeHighlightNode } from '@lexical/code';
import { ListItemNode, ListNode } from '@lexical/list';
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
import ToolbarPlugin from './plugins/ToolbarPlugin';
+import CodeHighlightPlugin from './plugins/CodeHighlightPlugin';
import editorTheme from './themes/EditorTheme';
import './styles/editor.css';
@@ -22,6 +23,7 @@ const editorConfig = {
HeadingNode,
QuoteNode,
CodeNode,
+ CodeHighlightNode,
ListNode,
ListItemNode,
],
@@ -44,6 +46,7 @@ export default function RichTextEditor() {
/>
+
diff --git a/src/editor/plugins/CodeHighlightPlugin.tsx b/src/editor/plugins/CodeHighlightPlugin.tsx
new file mode 100644
index 0000000..862d094
--- /dev/null
+++ b/src/editor/plugins/CodeHighlightPlugin.tsx
@@ -0,0 +1,13 @@
+import { registerCodeHighlighting } from '@lexical/code';
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
+import { useEffect } from 'react';
+
+export default function CodeHighlightPlugin() {
+ const [editor] = useLexicalComposerContext();
+
+ useEffect(() => {
+ return registerCodeHighlighting(editor);
+ }, [editor]);
+
+ return null;
+}
diff --git a/src/editor/plugins/ToolbarPlugin.tsx b/src/editor/plugins/ToolbarPlugin.tsx
index 0fa420d..1ef5960 100644
--- a/src/editor/plugins/ToolbarPlugin.tsx
+++ b/src/editor/plugins/ToolbarPlugin.tsx
@@ -10,9 +10,12 @@ import {
COMMAND_PRIORITY_CRITICAL,
$getSelection,
$isRangeSelection,
+ $createParagraphNode,
} from 'lexical';
import type { TextFormatType } from 'lexical';
import { $getSelectionStyleValueForProperty, $patchStyleText } from '@lexical/selection';
+import { $setBlocksType } from '@lexical/selection';
+import { $createCodeNode, $isCodeNode } from '@lexical/code';
import DropdownColorPicker from '../ui/DropdownColorPicker';
import '../styles/toolbar.css';
@@ -27,6 +30,7 @@ export default function ToolbarPlugin() {
const [isUnderline, setIsUnderline] = useState(false);
const [isStrikethrough, setIsStrikethrough] = useState(false);
const [isCode, setIsCode] = useState(false);
+ const [isCodeBlock, setIsCodeBlock] = useState(false);
const [fontColor, setFontColor] = useState('#000');
const [bgColor, setBgColor] = useState('#fff');
const [fontSize, setFontSize] = useState('15px');
@@ -41,6 +45,14 @@ export default function ToolbarPlugin() {
setIsStrikethrough(selection.hasFormat('strikethrough'));
setIsCode(selection.hasFormat('code'));
+ // Check if we're in a code block
+ const anchorNode = selection.anchor.getNode();
+ const element =
+ anchorNode.getKey() === 'root'
+ ? anchorNode
+ : anchorNode.getTopLevelElementOrThrow();
+ setIsCodeBlock($isCodeNode(element));
+
// Update color
setFontColor(
$getSelectionStyleValueForProperty(selection, 'color', '#000')
@@ -136,6 +148,19 @@ export default function ToolbarPlugin() {
[editor]
);
+ const formatCodeBlock = useCallback(() => {
+ editor.update(() => {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ if (isCodeBlock) {
+ $setBlocksType(selection, () => $createParagraphNode());
+ } else {
+ $setBlocksType(selection, () => $createCodeNode());
+ }
+ }
+ });
+ }, [editor, isCodeBlock]);
+
return (
+
');
}
+i.format.code-block {
+ background-image: url('data:image/svg+xml;utf8,');
+}
+
i.icon.font-color {
background-image: url('data:image/svg+xml;utf8,');
display: inline-block;
diff --git a/src/editor/themes/EditorTheme.ts b/src/editor/themes/EditorTheme.ts
index 7812103..30403e5 100644
--- a/src/editor/themes/EditorTheme.ts
+++ b/src/editor/themes/EditorTheme.ts
@@ -27,7 +27,39 @@ const theme: EditorThemeClasses = {
code: 'editor-text-code',
},
code: 'editor-code',
- codeHighlight: {},
+ codeHighlight: {
+ atrule: 'editor-token-attr',
+ attr: 'editor-token-attr',
+ boolean: 'editor-token-property',
+ builtin: 'editor-token-selector',
+ cdata: 'editor-token-comment',
+ char: 'editor-token-selector',
+ class: 'editor-token-function',
+ 'class-name': 'editor-token-function',
+ comment: 'editor-token-comment',
+ constant: 'editor-token-property',
+ deleted: 'editor-token-deleted',
+ doctype: 'editor-token-comment',
+ entity: 'editor-token-operator',
+ function: 'editor-token-function',
+ important: 'editor-token-variable',
+ inserted: 'editor-token-inserted',
+ keyword: 'editor-token-attr',
+ namespace: 'editor-token-variable',
+ number: 'editor-token-property',
+ operator: 'editor-token-operator',
+ prolog: 'editor-token-comment',
+ property: 'editor-token-property',
+ punctuation: 'editor-token-punctuation',
+ regex: 'editor-token-variable',
+ selector: 'editor-token-selector',
+ string: 'editor-token-selector',
+ symbol: 'editor-token-property',
+ tag: 'editor-token-property',
+ unchanged: 'editor-token-unchanged',
+ url: 'editor-token-operator',
+ variable: 'editor-token-variable',
+ },
};
export default theme;