返回 presentation-ai
code-block-node.tsx
根目录 / src / components / plate / ui / code-block-node.tsx
1 "use client";
2
3 import { formatCodeBlock, isLangSupported } from "@platejs/code-block";
4 import { BracesIcon, Check, CheckIcon, CopyIcon } from "lucide-react";
5 import { NodeApi, type TCodeBlockElement, type TCodeSyntaxLeaf } from "platejs";
6 import {
7 PlateElement,
8 PlateLeaf,
9 useEditorRef,
10 useElement,
11 useReadOnly,
12 type PlateElementProps,
13 type PlateLeafProps,
14 } from "platejs/react";
15 import * as React from "react";
16
17 import { Button } from "@/components/plate/ui/button";
18 import {
19 Command,
20 CommandEmpty,
21 CommandGroup,
22 CommandInput,
23 CommandItem,
24 CommandList,
25 } from "@/components/plate/ui/command";
26 import {
27 Popover,
28 PopoverContent,
29 PopoverTrigger,
30 } from "@/components/plate/ui/popover";
31 import { cn } from "@/lib/utils";
32
33 export function CodeBlockElement(props: PlateElementProps<TCodeBlockElement>) {
34 const { editor, element } = props;
35
36 return (
37 <PlateElement {...props}>
38 <style>
39 {`
40 .hljs-addition { background: #f0fff4; color: #22863a; }
41 .hljs-attr, .hljs-attribute, .hljs-literal, .hljs-meta, .hljs-number, .hljs-operator, .hljs-selector-attr, .hljs-selector-class, .hljs-selector-id, .hljs-variable { color: #005cc5; }
42 .hljs-built_in, .hljs-symbol { color: #e36209; }
43 .hljs-bullet { color: #735c0f; }
44 .hljs-comment, .hljs-code, .hljs-formula { color: #6a737d; }
45 .hljs-deletion { background: #ffeef0; color: #b31d28; }
46 .hljs-emphasis { font-style: italic; }
47 .hljs-keyword, .hljs-doctag, .hljs-template-tag, .hljs-template-variable, .hljs-type, .hljs-variable.language_ { color: #d73a49; }
48 .hljs-name, .hljs-quote, .hljs-selector-tag, .hljs-selector-pseudo { color: #22863a; }
49 .hljs-regexp, .hljs-string, .hljs-meta_.hljs-string { color: #3593ff; }
50 .hljs-section { font-weight: bold; color: #005cc5; }
51 .hljs-strong { font-weight: bold; }
52 .hljs-title, .hljs-title.class_, .hljs-title.class_.inherited__, .hljs-title.function_ { color: #6f42c1; }
53 `}
54 </style>
55 <div className="relative rounded-md bg-muted/50">
56 <pre className="overflow-x-auto p-8 pr-4 font-mono text-sm leading-[normal] [tab-size:2] print:break-inside-avoid">
57 <code>{props.children}</code>
58 </pre>
59
60 <div
61 className="absolute top-1 right-1 z-10 flex gap-0.5 select-none"
62 contentEditable={false}
63 >
64 {isLangSupported(element.lang) && (
65 <Button
66 size="icon"
67 variant="ghost"
68 className="size-6 text-xs"
69 onClick={() => formatCodeBlock(editor, { element })}
70 title="Format code"
71 >
72 <BracesIcon className="size-3.5! text-muted-foreground" />
73 </Button>
74 )}
75
76 <CodeBlockCombobox />
77
78 <CopyButton
79 size="icon"
80 variant="ghost"
81 className="size-6 gap-1 text-xs text-muted-foreground"
82 value={() => NodeApi.string(element)}
83 />
84 </div>
85 </div>
86 </PlateElement>
87 );
88 }
89
90 function CodeBlockCombobox() {
91 const [open, setOpen] = React.useState(false);
92 const commandListId = React.useId();
93 const readOnly = useReadOnly();
94 const editor = useEditorRef();
95 const element = useElement<TCodeBlockElement>();
96 const value = element.lang || "plaintext";
97 const [searchValue, setSearchValue] = React.useState("");
98
99 const items = React.useMemo(
100 () =>
101 languages.filter(
102 (language) =>
103 !searchValue ||
104 language.label.toLowerCase().includes(searchValue.toLowerCase()),
105 ),
106 [searchValue],
107 );
108
109 if (readOnly) return null;
110
111 return (
112 <Popover open={open} onOpenChange={setOpen}>
113 <PopoverTrigger asChild>
114 <Button
115 size="sm"
116 variant="ghost"
117 className="h-6 justify-between gap-1 px-2 text-xs text-muted-foreground select-none"
118 aria-expanded={open}
119 aria-controls={commandListId}
120 role="combobox"
121 >
122 {languages.find((language) => language.value === value)?.label ??
123 "Plain Text"}
124 </Button>
125 </PopoverTrigger>
126 <PopoverContent
127 className="w-50 p-0"
128 onCloseAutoFocus={() => setSearchValue("")}
129 >
130 <Command shouldFilter={false}>
131 <CommandInput
132 className="h-9"
133 value={searchValue}
134 onValueChange={(value) => setSearchValue(value)}
135 placeholder="Search language..."
136 />
137 <CommandEmpty>No language found.</CommandEmpty>
138
139 <CommandList id={commandListId} className="h-86 overflow-y-auto">
140 <CommandGroup>
141 {items.map((language) => (
142 <CommandItem
143 key={language.label}
144 className="cursor-pointer"
145 value={language.value}
146 onSelect={(value) => {
147 editor.tf.setNodes(
148 { lang: value } as Partial<TCodeBlockElement>,
149 { at: element },
150 );
151 setSearchValue(value);
152 setOpen(false);
153 }}
154 >
155 <Check
156 className={cn(
157 value === language.value ? "opacity-100" : "opacity-0",
158 )}
159 />
160 {language.label}
161 </CommandItem>
162 ))}
163 </CommandGroup>
164 </CommandList>
165 </Command>
166 </PopoverContent>
167 </Popover>
168 );
169 }
170
171 function CopyButton({
172 value,
173 ...props
174 }: { value: (() => string) | string } & Omit<
175 React.ComponentProps<typeof Button>,
176 "value"
177 >) {
178 const [hasCopied, setHasCopied] = React.useState(false);
179
180 React.useEffect(() => {
181 if (!hasCopied) {
182 return;
183 }
184
185 const timeoutId = window.setTimeout(() => {
186 setHasCopied(false);
187 }, 2000);
188
189 return () => {
190 window.clearTimeout(timeoutId);
191 };
192 }, [hasCopied]);
193
194 return (
195 <Button
196 onClick={() => {
197 void navigator.clipboard.writeText(
198 typeof value === "function" ? value() : value,
199 );
200 setHasCopied(true);
201 }}
202 {...props}
203 >
204 <span className="sr-only">Copy</span>
205 {hasCopied ? (
206 <CheckIcon className="size-3!" />
207 ) : (
208 <CopyIcon className="size-3!" />
209 )}
210 </Button>
211 );
212 }
213
214 export function CodeLineElement(props: PlateElementProps) {
215 return <PlateElement {...props} />;
216 }
217
218 export function CodeSyntaxLeaf(props: PlateLeafProps<TCodeSyntaxLeaf>) {
219 const tokenClassName = props.leaf.className as string;
220
221 return <PlateLeaf className={tokenClassName} {...props} />;
222 }
223
224 const languages: { label: string; value: string }[] = [
225 { label: "Auto", value: "auto" },
226 { label: "Plain Text", value: "plaintext" },
227 { label: "ABAP", value: "abap" },
228 { label: "Agda", value: "agda" },
229 { label: "Arduino", value: "arduino" },
230 { label: "ASCII Art", value: "ascii" },
231 { label: "Assembly", value: "x86asm" },
232 { label: "Bash", value: "bash" },
233 { label: "BASIC", value: "basic" },
234 { label: "BNF", value: "bnf" },
235 { label: "C", value: "c" },
236 { label: "C#", value: "csharp" },
237 { label: "C++", value: "cpp" },
238 { label: "Clojure", value: "clojure" },
239 { label: "CoffeeScript", value: "coffeescript" },
240 { label: "Coq", value: "coq" },
241 { label: "CSS", value: "css" },
242 { label: "Dart", value: "dart" },
243 { label: "Dhall", value: "dhall" },
244 { label: "Diff", value: "diff" },
245 { label: "Docker", value: "dockerfile" },
246 { label: "EBNF", value: "ebnf" },
247 { label: "Elixir", value: "elixir" },
248 { label: "Elm", value: "elm" },
249 { label: "Erlang", value: "erlang" },
250 { label: "F#", value: "fsharp" },
251 { label: "Flow", value: "flow" },
252 { label: "Fortran", value: "fortran" },
253 { label: "Gherkin", value: "gherkin" },
254 { label: "GLSL", value: "glsl" },
255 { label: "Go", value: "go" },
256 { label: "GraphQL", value: "graphql" },
257 { label: "Groovy", value: "groovy" },
258 { label: "Haskell", value: "haskell" },
259 { label: "HCL", value: "hcl" },
260 { label: "HTML", value: "html" },
261 { label: "Idris", value: "idris" },
262 { label: "Java", value: "java" },
263 { label: "JavaScript", value: "javascript" },
264 { label: "JSON", value: "json" },
265 { label: "Julia", value: "julia" },
266 { label: "Kotlin", value: "kotlin" },
267 { label: "LaTeX", value: "latex" },
268 { label: "Less", value: "less" },
269 { label: "Lisp", value: "lisp" },
270 { label: "LiveScript", value: "livescript" },
271 { label: "LLVM IR", value: "llvm" },
272 { label: "Lua", value: "lua" },
273 { label: "Makefile", value: "makefile" },
274 { label: "Markdown", value: "markdown" },
275 { label: "Markup", value: "markup" },
276 { label: "MATLAB", value: "matlab" },
277 { label: "Mathematica", value: "mathematica" },
278 { label: "Mermaid", value: "mermaid" },
279 { label: "Nix", value: "nix" },
280 { label: "Notion Formula", value: "notion" },
281 { label: "Objective-C", value: "objectivec" },
282 { label: "OCaml", value: "ocaml" },
283 { label: "Pascal", value: "pascal" },
284 { label: "Perl", value: "perl" },
285 { label: "PHP", value: "php" },
286 { label: "PowerShell", value: "powershell" },
287 { label: "Prolog", value: "prolog" },
288 { label: "Protocol Buffers", value: "protobuf" },
289 { label: "PureScript", value: "purescript" },
290 { label: "Python", value: "python" },
291 { label: "R", value: "r" },
292 { label: "Racket", value: "racket" },
293 { label: "Reason", value: "reasonml" },
294 { label: "Ruby", value: "ruby" },
295 { label: "Rust", value: "rust" },
296 { label: "Sass", value: "scss" },
297 { label: "Scala", value: "scala" },
298 { label: "Scheme", value: "scheme" },
299 { label: "SCSS", value: "scss" },
300 { label: "Shell", value: "shell" },
301 { label: "Smalltalk", value: "smalltalk" },
302 { label: "Solidity", value: "solidity" },
303 { label: "SQL", value: "sql" },
304 { label: "Swift", value: "swift" },
305 { label: "TOML", value: "toml" },
306 { label: "TypeScript", value: "typescript" },
307 { label: "VB.Net", value: "vbnet" },
308 { label: "Verilog", value: "verilog" },
309 { label: "VHDL", value: "vhdl" },
310 { label: "Visual Basic", value: "vbnet" },
311 { label: "WebAssembly", value: "wasm" },
312 { label: "XML", value: "xml" },
313 { label: "YAML", value: "yaml" },
314 ];
315
315 lines Plain Text