File size: 3,606 Bytes
50fe3c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
"use client";

import { Button } from "@/components/ui/button";
import {
  CheckmarkCircle01Icon,
  CopyIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import type { ReactNode } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import type { BundledLanguage } from "shiki";

import { CodeBlockContent } from "./code-block";

const SUPPORTED = new Set<string>([
  "ts", "tsx", "js", "jsx", "json", "md", "markdown",
  "sh", "bash", "zsh", "shell", "rs", "rust", "py", "python",
  "css", "html", "yaml", "yml", "toml", "go", "java", "c", "cpp",
  "ruby", "swift", "kotlin", "sql", "diff", "text", "txt", "plaintext",
]);

const ALIASES: Record<string, BundledLanguage> = {
  shell: "bash" as BundledLanguage,
  zsh: "bash" as BundledLanguage,
  markdown: "md" as BundledLanguage,
  python: "py" as BundledLanguage,
  rust: "rs" as BundledLanguage,
  txt: "text" as BundledLanguage,
  plaintext: "text" as BundledLanguage,
};

function resolveLanguage(raw: string | null): BundledLanguage {
  if (!raw) return "text" as BundledLanguage;
  const norm = raw.toLowerCase();
  if (!SUPPORTED.has(norm)) return "text" as BundledLanguage;
  return (ALIASES[norm] ?? norm) as BundledLanguage;
}

/**
 * Streamdown `components.code` override: handles BOTH inline and fenced.
 * Detection: fenced blocks come with a `language-*` className.
 */
export function MarkdownCode({
  className,
  children,
  ...rest
}: {
  className?: string;
  children?: ReactNode;
}) {
  const match = className?.match(/language-(\w+)/);
  if (!match) {
    return (
      <code
        className="rounded bg-muted/70 px-1.5 py-0.5 font-mono text-[11px] text-foreground"
        {...rest}
      >
        {children}
      </code>
    );
  }

  const lang = resolveLanguage(match[1]);
  const code = String(children ?? "").replace(/\n$/, "");

  return <MarkdownCodeBlock code={code} language={lang} rawLang={match[1]} />;
}

export function MarkdownCodeBlock({
  code,
  language,
  rawLang,
}: {
  code: string;
  language: BundledLanguage;
  rawLang: string;
}) {
  return (
    <div className="not-prose my-2 overflow-hidden rounded-lg border border-border/50 bg-muted/30">
      <div className="flex items-center justify-between gap-2 border-b border-border/40 bg-muted/20 px-3 py-1">
        <span className="font-mono text-[10px] uppercase tracking-wide text-muted-foreground">
          {rawLang || "text"}
        </span>
        <CopyButton text={code} />
      </div>
      <div className="overflow-x-auto [&_pre]:!bg-transparent [&_pre]:!p-3 [&_pre]:text-[11.5px] [&_pre]:leading-relaxed">
        <CodeBlockContent code={code} language={language} />
      </div>
    </div>
  );
}

function CopyButton({ text }: { text: string }) {
  const [copied, setCopied] = useState(false);
  const tRef = useRef<number>(0);

  const onCopy = useCallback(async () => {
    if (!navigator?.clipboard?.writeText) return;
    try {
      await navigator.clipboard.writeText(text);
      setCopied(true);
      tRef.current = window.setTimeout(() => setCopied(false), 1500);
    } catch {
      /* swallow */
    }
  }, [text]);

  useEffect(() => () => window.clearTimeout(tRef.current), []);

  return (
    <Button
      type="button"
      size="icon"
      variant="ghost"
      onClick={onCopy}
      className="size-5 shrink-0 text-muted-foreground hover:text-foreground"
      aria-label="Copy code"
    >
      <HugeiconsIcon
        icon={copied ? CheckmarkCircle01Icon : CopyIcon}
        size={11}
        strokeWidth={1.75}
      />
    </Button>
  );
}