code bock nodes

This commit is contained in:
Nate Kelley 2025-07-28 14:58:12 -06:00
parent 9233af8b25
commit 4e31af9a9c
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
4 changed files with 212 additions and 25 deletions

View File

@ -54,7 +54,196 @@ const initialValue: Value = [
{ text: ' text for emphasis!' }
],
type: 'p'
},
{
children: [
{ text: 'Here is another paragraph with ' },
{ italic: true, text: 'italic text' },
{ text: ' and ' },
{ underline: true, text: 'underlined text' },
{ text: ' to demonstrate various formatting options.' }
],
type: 'p'
},
{
children: [
{ text: 'This paragraph contains ' },
{ code: true, text: 'inline code' },
{ text: ' and ' },
{ strikethrough: true, text: 'strikethrough text' },
{ text: ' along with ' },
{ highlight: true, text: 'highlighted text' },
{ text: '.' }
],
type: 'p'
},
{
children: [{ text: 'Code Block Example' }],
type: 'h4'
},
{
children: [
{ children: [{ text: 'function hello() {' }], type: 'code_line' },
{
children: [{ text: " console.info('Code blocks are supported!');" }],
type: 'code_line'
},
{ children: [{ text: '}' }], type: 'code_line' }
],
lang: 'javascript',
type: 'code_block'
},
{
children: [
{
text: `SELECT id, name
FROM users
WHERE active = true
ORDER BY created_at DESC
LIMIT 5;`
}
],
type: 'code_block',
lang: 'sql'
},
{
children: [
{
text: `function calculateTotal(items) {
let total = 0;
for (const item of items) {
total += item.price;
}
return total;
}
function calculateAverage(items) {
if (items.length === 0) return 0;
return calculateTotal(items) / items.length;
}`
}
],
type: 'code_block',
lang: 'javascript'
}
// {
// children: [{ text: 'Unordered List' }],
// type: 'h4'
// },
// {
// children: [{ text: 'Features of this editor:' }],
// type: 'p'
// },
// {
// children: [
// {
// children: [{ text: 'Rich text formatting (bold, italic, underline)' }],
// type: 'li'
// }
// ],
// type: 'ul'
// },
// {
// children: [
// {
// children: [{ text: 'Code blocks with syntax highlighting' }],
// type: 'li'
// }
// ],
// type: 'ul'
// },
// {
// children: [
// {
// children: [{ text: 'Multiple heading levels' }],
// type: 'li'
// }
// ],
// type: 'ul'
// },
// {
// children: [
// {
// children: [{ text: 'Blockquotes and callouts' }],
// type: 'li'
// }
// ],
// type: 'ul'
// },
// {
// children: [{ text: 'Ordered List' }],
// type: 'h4'
// },
// {
// children: [{ text: 'Steps to create a great report:' }],
// type: 'p'
// },
// {
// children: [
// {
// children: [{ text: 'Start with a clear title and introduction' }],
// type: 'li'
// }
// ],
// type: 'ol'
// },
// {
// children: [
// {
// children: [{ text: 'Organize content with headings and subheadings' }],
// type: 'li'
// }
// ],
// type: 'ol'
// },
// {
// children: [
// {
// children: [{ text: 'Use formatting to emphasize key points' }],
// type: 'li'
// }
// ],
// type: 'ol'
// },
// {
// children: [
// {
// children: [{ text: 'Include code examples when relevant' }],
// type: 'li'
// }
// ],
// type: 'ol'
// },
// {
// children: [
// {
// children: [{ text: 'Conclude with a summary or call to action' }],
// type: 'li'
// }
// ],
// type: 'ol'
// },
// {
// children: [{ text: 'Important Note' }],
// type: 'h4'
// },
// {
// children: [
// {
// text: '💡 This is an informational callout that helps draw attention to important information. You can use callouts to highlight tips, warnings, or key insights throughout your report.'
// }
// ],
// type: 'callout',
// variant: 'info'
// },
// {
// children: [
// {
// text: 'This is the final paragraph of our sample content. It demonstrates how all these different content types can work together to create a comprehensive and well-formatted document.'
// }
// ],
// type: 'p'
// }
];
export const Default: Story = {

View File

@ -1,6 +1,6 @@
import React, { useImperativeHandle } from 'react';
import type { Value, AnyPluginConfig } from 'platejs';
import { Plate, type TPlateEditor } from 'platejs/react';
import { Plate, useEditorMounted, type TPlateEditor } from 'platejs/react';
import { EditorContainer } from './EditorContainer';
import { EditorContent } from './EditorContent';
import { useEditor } from './useEditor';
@ -46,6 +46,8 @@ export const AppReport = React.memo(
// Optionally expose the editor instance to the parent via ref
useImperativeHandle(ref, () => ({ editor, onReset }), [editor]);
if (!editor) return null;
return (
<Plate editor={editor} readOnly={readOnly}>
{/*

View File

@ -29,37 +29,36 @@ This is used for code blocks.
*/
export function CodeBlockElement(props: PlateElementProps<TCodeBlockElement>) {
const { editor, element } = props;
return (
<PlateElement
className="py-1 **:[.hljs-addition]:bg-[#f0fff4] **:[.hljs-addition]:text-[#22863a] dark:**:[.hljs-addition]:bg-[#3c5743] dark:**:[.hljs-addition]:text-[#ceead5] **:[.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable]:text-[#005cc5] dark:**:[.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable]:text-[#6596cf] **:[.hljs-built\\\\_in,.hljs-symbol]:text-[#e36209] dark:**:[.hljs-built\\\\_in,.hljs-symbol]:text-[#c3854e] **:[.hljs-bullet]:text-[#735c0f] **:[.hljs-comment,.hljs-code,.hljs-formula]:text-[#6a737d] dark:**:[.hljs-comment,.hljs-code,.hljs-formula]:text-[#6a737d] **:[.hljs-deletion]:bg-[#ffeef0] **:[.hljs-deletion]:text-[#b31d28] dark:**:[.hljs-deletion]:bg-[#473235] dark:**:[.hljs-deletion]:text-[#e7c7cb] **:[.hljs-emphasis]:italic **:[.hljs-keyword,.hljs-doctag,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language\\\\_]:text-[#d73a49] dark:**:[.hljs-keyword,.hljs-doctag,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language\\\\_]:text-[#ee6960] **:[.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo]:text-[#22863a] dark:**:[.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo]:text-[#36a84f] **:[.hljs-regexp,.hljs-string,.hljs-meta_.hljs-string]:text-[#032f62] dark:**:[.hljs-regexp,.hljs-string,.hljs-meta_.hljs-string]:text-[#3593ff] **:[.hljs-section]:font-bold **:[.hljs-section]:text-[#005cc5] dark:**:[.hljs-section]:text-[#61a5f2] **:[.hljs-strong]:font-bold **:[.hljs-title,.hljs-title.class\\\\_,.hljs-title.class\\\\_.inherited\\\\_\\\\_,.hljs-title.function\\\\_]:text-[#6f42c1] dark:**:[.hljs-title,.hljs-title.class\\\\_,.hljs-title.class\\\\_.inherited\\\\_\\\\_,.hljs-title.function\\\\_]:text-[#a77bfa]"
className="py-1 **:[.hljs-addition]:bg-[#f0fff4] **:[.hljs-addition]:text-[#22863a] **:[.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable]:text-[#005cc5] **:[.hljs-built_in,.hljs-symbol]:text-[#e36209] **:[.hljs-bullet]:text-[#735c0f] **:[.hljs-comment,.hljs-code,.hljs-formula]:text-[#6a737d] **:[.hljs-deletion]:bg-[#ffeef0] **:[.hljs-deletion]:text-[#b31d28] **:[.hljs-emphasis]:italic **:[.hljs-keyword,.hljs-doctag,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_]:text-[#d73a49] **:[.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo]:text-[#22863a] **:[.hljs-regexp,.hljs-string,.hljs-meta_.hljs-string]:text-[#032f62] **:[.hljs-section]:font-bold **:[.hljs-section]:text-[#005cc5] **:[.hljs-strong]:font-bold **:[.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_]:text-[#6f42c1]"
{...props}>
<div className="bg-muted/50 relative rounded-md">
<pre className="overflow-x-auto p-8 pr-4 font-mono text-sm leading-[normal] [tab-size:2] print:break-inside-avoid">
<code>{props.children}</code>
</pre>
<div
className="absolute top-1 right-1 z-10 flex gap-0.5 select-none"
contentEditable={false}>
{isLangSupported(element.lang) && (
<Button
variant="ghost"
className="size-6 text-xs"
onClick={() => formatCodeBlock(editor, { element })}
title="Format code"
prefix={<BracketsCurly />}></Button>
)}
<CodeBlockCombobox />
<CopyButton
variant="ghost"
className="text-muted-foreground size-6 gap-1 text-xs"
prefix={<Copy2 />}
value={() => NodeApi.string(element)}
/>
<CopyButton value={() => NodeApi.string(element)} />
</div>
</div>
</PlateElement>
);
}
function CodeBlockCombobox() {
const [open, setOpen] = React.useState(false);
const readOnly = useReadOnly();
@ -67,6 +66,7 @@ function CodeBlockCombobox() {
const element = useElement<TCodeBlockElement>();
const value = element.lang || 'plaintext';
const [searchValue, setSearchValue] = React.useState('');
const items = React.useMemo(
() =>
languages.filter(
@ -75,16 +75,13 @@ function CodeBlockCombobox() {
),
[searchValue]
);
if (readOnly) return null;
return (
<PopoverBase open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
size="small"
variant="ghost"
className="text-muted-foreground h-6 justify-between gap-1 px-2 text-xs select-none"
aria-expanded={open}
role="combobox">
<Button aria-expanded={open} role="combobox">
{languages.find((language) => language.value === value)?.label ?? 'Plain Text'}
</Button>
</PopoverTrigger>
@ -122,43 +119,41 @@ function CodeBlockCombobox() {
</PopoverBase>
);
}
function CopyButton({
value,
...props
}: { value: (() => string) | string } & Omit<React.ComponentProps<typeof Button>, 'value'>) {
const [hasCopied, setHasCopied] = React.useState(false);
React.useEffect(() => {
setTimeout(() => {
setHasCopied(false);
}, 2000);
}, [hasCopied]);
return (
<Button
prefix={hasCopied ? <Check /> : <Copy2 />}
onClick={() => {
void navigator.clipboard.writeText(typeof value === 'function' ? value() : value);
setHasCopied(true);
}}
{...props}>
<span className="sr-only">Copy</span>
<div className="size-4">{hasCopied ? <Check /> : <Copy2 />}</div>
}}>
Copy
</Button>
);
}
/*
This is used for code lines.
*/
export function CodeLineElement(props: PlateElementProps) {
return <PlateElement {...props} />;
}
/*
This is used for code syntax.
*/
export function CodeSyntaxLeaf(props: PlateLeafProps<TCodeSyntaxLeaf>) {
const tokenClassName = props.leaf.className as string;
return <PlateLeaf className={tokenClassName} {...props} />;
}
const languages: { label: string; value: string }[] = [
{ label: 'Auto', value: 'auto' },
{ label: 'Plain Text', value: 'plaintext' },

View File

@ -2,6 +2,7 @@
import { CodeBlockPlugin, CodeLinePlugin, CodeSyntaxPlugin } from '@platejs/code-block/react';
import { all, createLowlight } from 'lowlight';
import { CodeBlockElement, CodeLineElement, CodeSyntaxLeaf } from '../elements/CodeBlockNode';
const lowlight = createLowlight(all);