buster/apps/web/src/components/ui/report/elements/CodeBlockNode.tsx

253 lines
9.4 KiB
TypeScript
Raw Normal View History

2025-07-29 02:54:53 +08:00
'use client';
import * as React from 'react';
import { formatCodeBlock, isLangSupported } from '@platejs/code-block';
import { Check, Copy2, BracketsCurly } from '@/components/ui/icons';
import { type TCodeBlockElement, type TCodeSyntaxLeaf, NodeApi } from 'platejs';
import {
type PlateElementProps,
type PlateLeafProps,
PlateElement,
2025-07-29 05:27:22 +08:00
PlateLeaf,
2025-07-29 12:05:04 +08:00
useSelected
2025-07-29 02:54:53 +08:00
} from 'platejs/react';
import { useEditorRef, useElement, useReadOnly } from 'platejs/react';
import { Button } from '@/components/ui/buttons';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from '@/components/ui/command';
import { PopoverBase, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { cn } from '@/lib/utils';
/*
This is used for code blocks.
*/
export function CodeBlockElement(props: PlateElementProps<TCodeBlockElement>) {
const { editor, element } = props;
2025-07-29 04:58:12 +08:00
2025-07-29 02:54:53 +08:00
return (
<PlateElement
2025-07-29 05:27:22 +08:00
className={cn(
'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]'
)}
2025-07-29 02:54:53 +08:00
{...props}>
2025-07-29 12:05:04 +08:00
<div className={cn('bg-muted/50 relative rounded-md transition-all duration-100')}>
2025-07-29 02:54:53 +08:00
<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>
2025-07-29 04:58:12 +08:00
2025-07-29 02:54:53 +08:00
<div
className="absolute top-1 right-1 z-10 flex gap-0.5 select-none"
contentEditable={false}>
{isLangSupported(element.lang) && (
<Button
2025-07-29 12:05:04 +08:00
variant="ghost"
2025-07-29 02:54:53 +08:00
className="size-6 text-xs"
onClick={() => formatCodeBlock(editor, { element })}
title="Format code"
prefix={<BracketsCurly />}></Button>
)}
2025-07-29 04:58:12 +08:00
2025-07-29 02:54:53 +08:00
<CodeBlockCombobox />
2025-07-29 04:58:12 +08:00
<CopyButton value={() => NodeApi.string(element)} />
2025-07-29 02:54:53 +08:00
</div>
</div>
</PlateElement>
);
}
2025-07-29 04:58:12 +08:00
2025-07-29 02:54:53 +08:00
function CodeBlockCombobox() {
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('');
2025-07-29 04:58:12 +08:00
2025-07-29 02:54:53 +08:00
const items = React.useMemo(
() =>
languages.filter(
(language) =>
!searchValue || language.label.toLowerCase().includes(searchValue.toLowerCase())
),
[searchValue]
);
2025-07-29 04:58:12 +08:00
2025-07-29 02:54:53 +08:00
if (readOnly) return null;
2025-07-29 04:58:12 +08:00
2025-07-29 02:54:53 +08:00
return (
<PopoverBase open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
2025-07-29 12:05:04 +08:00
<Button variant="ghost" aria-expanded={open} role="combobox">
2025-07-29 02:54:53 +08:00
{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>
2025-07-29 12:05:04 +08:00
<CommandList className="overflow-y-auto">
2025-07-29 02:54:53 +08:00
<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);
}}>
<div className={cn(value === language.value ? 'opacity-100' : 'opacity-0')}>
<Check />
</div>
{language.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</PopoverBase>
);
}
2025-07-29 04:58:12 +08:00
2025-07-29 02:54:53 +08:00
function CopyButton({
value,
...props
}: { value: (() => string) | string } & Omit<React.ComponentProps<typeof Button>, 'value'>) {
const [hasCopied, setHasCopied] = React.useState(false);
2025-07-29 04:58:12 +08:00
2025-07-29 02:54:53 +08:00
React.useEffect(() => {
setTimeout(() => {
setHasCopied(false);
}, 2000);
}, [hasCopied]);
2025-07-29 04:58:12 +08:00
2025-07-29 02:54:53 +08:00
return (
<Button
2025-07-29 12:05:04 +08:00
variant="ghost"
2025-07-29 04:58:12 +08:00
prefix={hasCopied ? <Check /> : <Copy2 />}
2025-07-29 02:54:53 +08:00
onClick={() => {
void navigator.clipboard.writeText(typeof value === 'function' ? value() : value);
setHasCopied(true);
2025-07-29 04:58:12 +08:00
}}>
Copy
2025-07-29 02:54:53 +08:00
</Button>
);
}
export function CodeLineElement(props: PlateElementProps) {
return <PlateElement {...props} />;
}
export function CodeSyntaxLeaf(props: PlateLeafProps<TCodeSyntaxLeaf>) {
const tokenClassName = props.leaf.className as string;
2025-07-29 04:58:12 +08:00
2025-07-29 02:54:53 +08:00
return <PlateLeaf className={tokenClassName} {...props} />;
}
2025-07-29 04:58:12 +08:00
2025-07-29 02:54:53 +08:00
const languages: { label: string; value: string }[] = [
2025-07-29 12:05:04 +08:00
// { label: 'Auto', value: 'auto' },
2025-07-29 02:54:53 +08:00
{ label: 'Plain Text', value: 'plaintext' },
2025-07-29 09:12:51 +08:00
// { label: 'ABAP', value: 'abap' },
// { label: 'Agda', value: 'agda' },
// { label: 'Arduino', value: 'arduino' },
// { label: 'ASCII Art', value: 'ascii' },
// { label: 'Assembly', value: 'x86asm' },
2025-07-29 02:54:53 +08:00
{ label: 'Bash', value: 'bash' },
2025-07-29 09:12:51 +08:00
// { 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' },
2025-07-29 02:54:53 +08:00
{ label: 'JavaScript', value: 'javascript' },
{ label: 'JSON', value: 'json' },
2025-07-29 09:12:51 +08:00
// { 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' },
2025-07-29 02:54:53 +08:00
{ label: 'SQL', value: 'sql' },
2025-07-29 09:12:51 +08:00
// { label: 'Swift', value: 'swift' },
// { label: 'TOML', value: 'toml' },
2025-07-29 02:54:53 +08:00
{ label: 'TypeScript', value: 'typescript' },
2025-07-29 09:12:51 +08:00
// { 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' },
2025-07-29 02:54:53 +08:00
{ label: 'YAML', value: 'yaml' }
];