Skip to content
File

Blob: src/shared/editor/presentation/code-block.tsx

typescript87 lines
1import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
2import { Check, Copy } from "lucide-react";
3import { CODE_LANGUAGES, resolveLanguage } from "@/shared/editor/schema/code-block-model";
4import { bidAttribute, type BlockIdentityProps } from "./attrs";
5 
6export interface CodeBlockFrameProps extends BlockIdentityProps {
7 language: unknown;
8 controls?: ReactNode;
9 children?: ReactNode;
10}
11 
12export interface CodeBlockCopyButtonChromeProps extends ButtonHTMLAttributes<HTMLButtonElement> {
13 copied?: boolean;
14}
15 
16export function getCodeLanguageLabel(language: unknown): string {
17 const normalizedLanguage = resolveLanguage(typeof language === "string" ? language : null);
18 return CODE_LANGUAGES[normalizedLanguage]?.name ?? CODE_LANGUAGES.text.name;
19}
20 
21export function getCodeCopyButtonLabel(copied: boolean): string {
22 return copied ? "Code copied" : "Copy code";
23}
24 
25export const CodeBlockCopyButtonChrome = forwardRef<HTMLButtonElement, CodeBlockCopyButtonChromeProps>(
26 function CodeBlockCopyButtonChrome(
27 { copied = false, className, type = "button", title, "aria-label": ariaLabel, ...props },
28 ref,
29 ) {
30 const label = typeof ariaLabel === "string" ? ariaLabel : getCodeCopyButtonLabel(copied);
31 
32 return (
33 <button
34 {...props}
35 ref={ref}
36 type={type}
37 className={["tiptap-code-block-copy-btn", className].filter(Boolean).join(" ")}
38 aria-label={label}
39 title={title ?? label}
40 data-copied={copied ? "true" : undefined}
41 >
42 <span data-copy-icon-default="" aria-hidden="true">
43 <Copy className="h-3.5 w-3.5" />
44 </span>
45 <span data-copy-icon-copied="" aria-hidden="true">
46 <Check className="h-3.5 w-3.5" />
47 </span>
48 </button>
49 );
50 },
51);
52 
53export function CodeBlockFrame({ bid, language, controls, children }: CodeBlockFrameProps) {
54 return (
55 <div
56 className="tiptap-code-block-wrapper"
57 data-language={resolveLanguage(asOptionalString(language))}
58 {...bidAttribute(bid)}
59 >
60 {controls}
61 <pre className="tiptap-code-block-pre" spellCheck={false}>
62 {children}
63 </pre>
64 </div>
65 );
66}
67 
68export function CodeBlockContent({ children }: { children?: ReactNode }) {
69 return (
70 <code className="tiptap-code-block-content" style={{ whiteSpace: "inherit" }}>
71 {children}
72 </code>
73 );
74}
75 
76export function CodeBlockPresentation(props: CodeBlockFrameProps) {
77 return (
78 <CodeBlockFrame {...props}>
79 <CodeBlockContent>{props.children}</CodeBlockContent>
80 </CodeBlockFrame>
81 );
82}
83 
84function asOptionalString(value: unknown): string | null {
85 return typeof value === "string" ? value : null;
86}