SurfSense/surfsense_web/components/ui/code-block-node.tsx

266 lines
9.8 KiB
TypeScript
Raw Normal View History

2026-02-17 12:47:39 +05:30
"use client";
2026-02-17 12:47:39 +05:30
import { formatCodeBlock, isLangSupported } from "@platejs/code-block";
import { BracesIcon, Check, CheckIcon, CopyIcon } from "lucide-react";
2026-02-20 22:44:56 -08:00
import { NodeApi, type TCodeBlockElement, type TCodeSyntaxLeaf } from "platejs";
import {
2026-02-17 12:47:39 +05:30
PlateElement,
2026-02-20 22:44:56 -08:00
type PlateElementProps,
2026-02-17 12:47:39 +05:30
PlateLeaf,
2026-02-20 22:44:56 -08:00
type PlateLeafProps,
useEditorRef,
useElement,
useReadOnly,
2026-02-17 12:47:39 +05:30
} from "platejs/react";
2026-02-20 22:44:56 -08:00
import * as React from "react";
2026-02-17 12:47:39 +05:30
import { Button } from "@/components/ui/button";
import {
2026-02-17 12:47:39 +05:30
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
export function CodeBlockElement(props: PlateElementProps<TCodeBlockElement>) {
2026-02-17 12:47:39 +05:30
const { editor, element } = props;
2026-02-17 12:47:39 +05:30
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]"
{...props}
>
<div className="relative rounded-md bg-muted/50">
<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>
2026-02-17 12:47:39 +05:30
<div
className="absolute top-1 right-1 z-10 flex select-none gap-0.5"
contentEditable={false}
>
{isLangSupported(element.lang) && (
<Button
size="icon"
variant="ghost"
className="size-6 text-xs"
onClick={() => formatCodeBlock(editor, { element })}
title="Format code"
>
<BracesIcon className="!size-3.5 text-muted-foreground" />
</Button>
)}
2026-02-17 12:47:39 +05:30
<CodeBlockCombobox />
2026-02-17 12:47:39 +05:30
<CopyButton
size="icon"
variant="ghost"
className="size-6 gap-1 text-muted-foreground text-xs"
value={() => NodeApi.string(element)}
/>
</div>
</div>
</PlateElement>
);
}
function CodeBlockCombobox() {
2026-02-17 12:47:39 +05:30
const [open, setOpen] = React.useState(false);
const readOnly = useReadOnly();
const editor = useEditorRef();
const element = useElement<TCodeBlockElement>();
const value = element.lang || "plaintext";
const [searchValue, setSearchValue] = React.useState("");
2026-02-17 12:47:39 +05:30
const items = React.useMemo(
() =>
languages.filter(
(language) =>
!searchValue || language.label.toLowerCase().includes(searchValue.toLowerCase())
),
[searchValue]
);
2026-02-17 12:47:39 +05:30
if (readOnly) return null;
2026-02-17 12:47:39 +05:30
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
size="sm"
variant="ghost"
className="h-6 select-none justify-between gap-1 px-2 text-muted-foreground text-xs"
aria-expanded={open}
role="combobox"
>
{languages.find((language) => language.value === value)?.label ?? "Plain Text"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" onCloseAutoFocus={() => setSearchValue("")}>
<Command shouldFilter={false}>
<CommandInput
className="h-9"
value={searchValue}
onValueChange={(value) => setSearchValue(value)}
placeholder="Search language..."
/>
<CommandEmpty>No language found.</CommandEmpty>
2026-02-17 12:47:39 +05:30
<CommandList className="h-[344px] overflow-y-auto">
<CommandGroup>
{items.map((language) => (
<CommandItem
key={language.label}
className="cursor-pointer"
value={language.value}
onSelect={(value) => {
editor.tf.setNodes<TCodeBlockElement>({ lang: value }, { at: element });
setSearchValue(value);
setOpen(false);
}}
>
<Check className={cn(value === language.value ? "opacity-100" : "opacity-0")} />
{language.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
function CopyButton({
2026-02-17 12:47:39 +05:30
value,
...props
}: { value: (() => string) | string } & Omit<React.ComponentProps<typeof Button>, "value">) {
const [hasCopied, setHasCopied] = React.useState(false);
2026-02-17 12:47:39 +05:30
React.useEffect(() => {
setTimeout(() => {
setHasCopied(false);
}, 2000);
}, [hasCopied]);
2026-02-17 12:47:39 +05:30
return (
<Button
onClick={() => {
void navigator.clipboard.writeText(typeof value === "function" ? value() : value);
setHasCopied(true);
}}
{...props}
>
<span className="sr-only">Copy</span>
{hasCopied ? <CheckIcon className="!size-3" /> : <CopyIcon className="!size-3" />}
</Button>
);
}
export function CodeLineElement(props: PlateElementProps) {
2026-02-17 12:47:39 +05:30
return <PlateElement {...props} />;
}
export function CodeSyntaxLeaf(props: PlateLeafProps<TCodeSyntaxLeaf>) {
2026-02-17 12:47:39 +05:30
const tokenClassName = props.leaf.className as string;
2026-02-17 12:47:39 +05:30
return <PlateLeaf className={tokenClassName} {...props} />;
}
const languages: { label: string; value: string }[] = [
2026-02-17 12:47:39 +05:30
{ label: "Auto", value: "auto" },
{ label: "Plain Text", value: "plaintext" },
{ label: "ABAP", value: "abap" },
{ label: "Agda", value: "agda" },
{ label: "Arduino", value: "arduino" },
{ label: "ASCII Art", value: "ascii" },
{ label: "Assembly", value: "x86asm" },
{ label: "Bash", value: "bash" },
{ label: "BASIC", value: "basic" },
{ label: "BNF", value: "bnf" },
{ label: "C", value: "c" },
{ label: "C#", value: "csharp" },
{ label: "C++", value: "cpp" },
{ label: "Clojure", value: "clojure" },
{ label: "CoffeeScript", value: "coffeescript" },
{ label: "Coq", value: "coq" },
{ label: "CSS", value: "css" },
{ label: "Dart", value: "dart" },
{ label: "Dhall", value: "dhall" },
{ label: "Diff", value: "diff" },
{ label: "Docker", value: "dockerfile" },
{ label: "EBNF", value: "ebnf" },
{ label: "Elixir", value: "elixir" },
{ label: "Elm", value: "elm" },
{ label: "Erlang", value: "erlang" },
{ label: "F#", value: "fsharp" },
{ label: "Flow", value: "flow" },
{ label: "Fortran", value: "fortran" },
{ label: "Gherkin", value: "gherkin" },
{ label: "GLSL", value: "glsl" },
{ label: "Go", value: "go" },
{ label: "GraphQL", value: "graphql" },
{ label: "Groovy", value: "groovy" },
{ label: "Haskell", value: "haskell" },
{ label: "HCL", value: "hcl" },
{ label: "HTML", value: "html" },
{ label: "Idris", value: "idris" },
{ label: "Java", value: "java" },
{ label: "JavaScript", value: "javascript" },
{ label: "JSON", value: "json" },
{ label: "Julia", value: "julia" },
{ label: "Kotlin", value: "kotlin" },
{ label: "LaTeX", value: "latex" },
{ label: "Less", value: "less" },
{ label: "Lisp", value: "lisp" },
{ label: "LiveScript", value: "livescript" },
{ label: "LLVM IR", value: "llvm" },
{ label: "Lua", value: "lua" },
{ label: "Makefile", value: "makefile" },
{ label: "Markdown", value: "markdown" },
{ label: "Markup", value: "markup" },
{ label: "MATLAB", value: "matlab" },
{ label: "Mathematica", value: "mathematica" },
{ label: "Mermaid", value: "mermaid" },
{ label: "Nix", value: "nix" },
{ label: "Notion Formula", value: "notion" },
{ label: "Objective-C", value: "objectivec" },
{ label: "OCaml", value: "ocaml" },
{ label: "Pascal", value: "pascal" },
{ label: "Perl", value: "perl" },
{ label: "PHP", value: "php" },
{ label: "PowerShell", value: "powershell" },
{ label: "Prolog", value: "prolog" },
{ label: "Protocol Buffers", value: "protobuf" },
{ label: "PureScript", value: "purescript" },
{ label: "Python", value: "python" },
{ label: "R", value: "r" },
{ label: "Racket", value: "racket" },
{ label: "Reason", value: "reasonml" },
{ label: "Ruby", value: "ruby" },
{ label: "Rust", value: "rust" },
{ label: "Sass", value: "scss" },
{ label: "Scala", value: "scala" },
{ label: "Scheme", value: "scheme" },
{ label: "SCSS", value: "scss" },
{ label: "Shell", value: "shell" },
{ label: "Smalltalk", value: "smalltalk" },
{ label: "Solidity", value: "solidity" },
{ label: "SQL", value: "sql" },
{ label: "Swift", value: "swift" },
{ label: "TOML", value: "toml" },
{ label: "TypeScript", value: "typescript" },
{ label: "VB.Net", value: "vbnet" },
{ label: "Verilog", value: "verilog" },
{ label: "VHDL", value: "vhdl" },
{ label: "Visual Basic", value: "vbnet" },
{ label: "WebAssembly", value: "wasm" },
{ label: "XML", value: "xml" },
{ label: "YAML", value: "yaml" },
];