Hayden BleaselUpgrades (#1419)453f5bb8 months agoHistory
555 lines · 13.5 KB · TypeScript
RawDownload
"use client"; import type { ComponentProps, CSSProperties, HTMLAttributes } from "react";import type {  BundledLanguage,  BundledTheme,  HighlighterGeneric,  ThemedToken,} from "shiki"; import { Button } from "@/components/ui/button";import {  Select,  SelectContent,  SelectItem,  SelectTrigger,  SelectValue,} from "@/components/ui/select";import { cn } from "@/lib/utils";import { CheckIcon, CopyIcon } from "lucide-react";import {  createContext,  memo,  useCallback,  useContext,  useEffect,  useMemo,  useRef,  useState,} from "react";import { createHighlighter } from "shiki"; // Shiki uses bitflags for font styles: 1=italic, 2=bold, 4=underline// biome-ignore lint/suspicious/noBitwiseOperators: shiki bitflag check// eslint-disable-next-line no-bitwise -- shiki bitflag checkconst isItalic = (fontStyle: number | undefined) => fontStyle && fontStyle & 1;// biome-ignore lint/suspicious/noBitwiseOperators: shiki bitflag check// eslint-disable-next-line no-bitwise -- shiki bitflag check// oxlint-disable-next-line eslint(no-bitwise)const isBold = (fontStyle: number | undefined) => fontStyle && fontStyle & 2;const isUnderline = (fontStyle: number | undefined) =>  // biome-ignore lint/suspicious/noBitwiseOperators: shiki bitflag check  // oxlint-disable-next-line eslint(no-bitwise)  fontStyle && fontStyle & 4; // Transform tokens to include pre-computed keys to avoid noArrayIndexKey lintinterface KeyedToken {  token: ThemedToken;  key: string;}interface KeyedLine {  tokens: KeyedToken[];  key: string;} const addKeysToTokens = (lines: ThemedToken[][]): KeyedLine[] =>  lines.map((line, lineIdx) => ({    key: `line-${lineIdx}`,    tokens: line.map((token, tokenIdx) => ({      key: `line-${lineIdx}-${tokenIdx}`,      token,    })),  })); // Token rendering componentconst TokenSpan = ({ token }: { token: ThemedToken }) => (  <span    className="dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)]"    style={      {        backgroundColor: token.bgColor,        color: token.color,        fontStyle: isItalic(token.fontStyle) ? "italic" : undefined,        fontWeight: isBold(token.fontStyle) ? "bold" : undefined,        textDecoration: isUnderline(token.fontStyle) ? "underline" : undefined,        ...token.htmlStyle,      } as CSSProperties    }  >    {token.content}  </span>); // Line rendering componentconst LineSpan = ({  keyedLine,  showLineNumbers,}: {  keyedLine: KeyedLine;  showLineNumbers: boolean;}) => (  <span className={showLineNumbers ? LINE_NUMBER_CLASSES : "block"}>    {keyedLine.tokens.length === 0      ? "\n"      : keyedLine.tokens.map(({ token, key }) => (          <TokenSpan key={key} token={token} />        ))}  </span>); // Typestype CodeBlockProps = HTMLAttributes<HTMLDivElement> & {  code: string;  language: BundledLanguage;  showLineNumbers?: boolean;}; interface TokenizedCode {  tokens: ThemedToken[][];  fg: string;  bg: string;} interface CodeBlockContextType {  code: string;} // Contextconst CodeBlockContext = createContext<CodeBlockContextType>({  code: "",}); // Highlighter cache (singleton per language)const highlighterCache = new Map<  string,  Promise<HighlighterGeneric<BundledLanguage, BundledTheme>>>(); // Token cacheconst tokensCache = new Map<string, TokenizedCode>(); // Subscribers for async token updatesconst subscribers = new Map<string, Set<(result: TokenizedCode) => void>>(); const getTokensCacheKey = (code: string, language: BundledLanguage) => {  const start = code.slice(0, 100);  const end = code.length > 100 ? code.slice(-100) : "";  return `${language}:${code.length}:${start}:${end}`;}; const getHighlighter = (  language: BundledLanguage): Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> => {  const cached = highlighterCache.get(language);  if (cached) {    return cached;  }   const highlighterPromise = createHighlighter({    langs: [language],    themes: ["github-light", "github-dark"],  });   highlighterCache.set(language, highlighterPromise);  return highlighterPromise;}; // Create raw tokens for immediate display while highlighting loadsconst createRawTokens = (code: string): TokenizedCode => ({  bg: "transparent",  fg: "inherit",  tokens: code.split("\n").map((line) =>    line === ""      ? []      : [          {            color: "inherit",            content: line,          } as ThemedToken,        ]  ),}); // Synchronous highlight with callback for async resultsexport const highlightCode = (  code: string,  language: BundledLanguage,  // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks)  callback?: (result: TokenizedCode) => void): TokenizedCode | null => {  const tokensCacheKey = getTokensCacheKey(code, language);   // Return cached result if available  const cached = tokensCache.get(tokensCacheKey);  if (cached) {    return cached;  }   // Subscribe callback if provided  if (callback) {    if (!subscribers.has(tokensCacheKey)) {      subscribers.set(tokensCacheKey, new Set());    }    subscribers.get(tokensCacheKey)?.add(callback);  }   // Start highlighting in background - fire-and-forget async pattern  getHighlighter(language)    // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then)    .then((highlighter) => {      const availableLangs = highlighter.getLoadedLanguages();      const langToUse = availableLangs.includes(language) ? language : "text";       const result = highlighter.codeToTokens(code, {        lang: langToUse,        themes: {          dark: "github-dark",          light: "github-light",        },      });       const tokenized: TokenizedCode = {        bg: result.bg ?? "transparent",        fg: result.fg ?? "inherit",        tokens: result.tokens,      };       // Cache the result      tokensCache.set(tokensCacheKey, tokenized);       // Notify all subscribers      const subs = subscribers.get(tokensCacheKey);      if (subs) {        for (const sub of subs) {          sub(tokenized);        }        subscribers.delete(tokensCacheKey);      }    })    // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then), eslint-plugin-promise(prefer-await-to-callbacks)    .catch((error) => {      console.error("Failed to highlight code:", error);      subscribers.delete(tokensCacheKey);    });   return null;}; // Line number styles using CSS countersconst LINE_NUMBER_CLASSES = cn(  "block",  "before:content-[counter(line)]",  "before:inline-block",  "before:[counter-increment:line]",  "before:w-8",  "before:mr-4",  "before:text-right",  "before:text-muted-foreground/50",  "before:font-mono",  "before:select-none"); const CodeBlockBody = memo(  ({    tokenized,    showLineNumbers,    className,  }: {    tokenized: TokenizedCode;    showLineNumbers: boolean;    className?: string;  }) => {    const preStyle = useMemo(      () => ({        backgroundColor: tokenized.bg,        color: tokenized.fg,      }),      [tokenized.bg, tokenized.fg]    );     const keyedLines = useMemo(      () => addKeysToTokens(tokenized.tokens),      [tokenized.tokens]    );     return (      <pre        className={cn(          "dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)] m-0 p-4 text-sm",          className        )}        style={preStyle}      >        <code          className={cn(            "font-mono text-sm",            showLineNumbers && "[counter-increment:line_0] [counter-reset:line]"          )}        >          {keyedLines.map((keyedLine) => (            <LineSpan              key={keyedLine.key}              keyedLine={keyedLine}              showLineNumbers={showLineNumbers}            />          ))}        </code>      </pre>    );  },  (prevProps, nextProps) =>    prevProps.tokenized === nextProps.tokenized &&    prevProps.showLineNumbers === nextProps.showLineNumbers &&    prevProps.className === nextProps.className); CodeBlockBody.displayName = "CodeBlockBody"; export const CodeBlockContainer = ({  className,  language,  style,  ...props}: HTMLAttributes<HTMLDivElement> & { language: string }) => (  <div    className={cn(      "group relative w-full overflow-hidden rounded-md border bg-background text-foreground",      className    )}    data-language={language}    style={{      containIntrinsicSize: "auto 200px",      contentVisibility: "auto",      ...style,    }}    {...props}  />); export const CodeBlockHeader = ({  children,  className,  ...props}: HTMLAttributes<HTMLDivElement>) => (  <div    className={cn(      "flex items-center justify-between border-b bg-muted/80 px-3 py-2 text-muted-foreground text-xs",      className    )}    {...props}  >    {children}  </div>); export const CodeBlockTitle = ({  children,  className,  ...props}: HTMLAttributes<HTMLDivElement>) => (  <div className={cn("flex items-center gap-2", className)} {...props}>    {children}  </div>); export const CodeBlockFilename = ({  children,  className,  ...props}: HTMLAttributes<HTMLSpanElement>) => (  <span className={cn("font-mono", className)} {...props}>    {children}  </span>); export const CodeBlockActions = ({  children,  className,  ...props}: HTMLAttributes<HTMLDivElement>) => (  <div    className={cn("-my-1 -mr-1 flex items-center gap-2", className)}    {...props}  >    {children}  </div>); export const CodeBlockContent = ({  code,  language,  showLineNumbers = false,}: {  code: string;  language: BundledLanguage;  showLineNumbers?: boolean;}) => {  // Memoized raw tokens for immediate display  const rawTokens = useMemo(() => createRawTokens(code), [code]);   // Try to get cached result synchronously, otherwise use raw tokens  const [tokenized, setTokenized] = useState<TokenizedCode>(    () => highlightCode(code, language) ?? rawTokens  );   useEffect(() => {    let cancelled = false;     // Reset to raw tokens when code changes (shows current code, not stale tokens)    setTokenized(highlightCode(code, language) ?? rawTokens);     // Subscribe to async highlighting result    highlightCode(code, language, (result) => {      if (!cancelled) {        setTokenized(result);      }    });     return () => {      cancelled = true;    };  }, [code, language, rawTokens]);   return (    <div className="relative overflow-auto">      <CodeBlockBody showLineNumbers={showLineNumbers} tokenized={tokenized} />    </div>  );}; export const CodeBlock = ({  code,  language,  showLineNumbers = false,  className,  children,  ...props}: CodeBlockProps) => {  const contextValue = useMemo(() => ({ code }), [code]);   return (    <CodeBlockContext.Provider value={contextValue}>      <CodeBlockContainer className={className} language={language} {...props}>        {children}        <CodeBlockContent          code={code}          language={language}          showLineNumbers={showLineNumbers}        />      </CodeBlockContainer>    </CodeBlockContext.Provider>  );}; export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {  onCopy?: () => void;  onError?: (error: Error) => void;  timeout?: number;}; export const CodeBlockCopyButton = ({  onCopy,  onError,  timeout = 2000,  children,  className,  ...props}: CodeBlockCopyButtonProps) => {  const [isCopied, setIsCopied] = useState(false);  const timeoutRef = useRef<number>(0);  const { code } = useContext(CodeBlockContext);   const copyToClipboard = useCallback(async () => {    if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {      onError?.(new Error("Clipboard API not available"));      return;    }     try {      if (!isCopied) {        await navigator.clipboard.writeText(code);        setIsCopied(true);        onCopy?.();        timeoutRef.current = window.setTimeout(          () => setIsCopied(false),          timeout        );      }    } catch (error) {      onError?.(error as Error);    }  }, [code, onCopy, onError, timeout, isCopied]);   useEffect(    () => () => {      window.clearTimeout(timeoutRef.current);    },    []  );   const Icon = isCopied ? CheckIcon : CopyIcon;   return (    <Button      className={cn("shrink-0", className)}      onClick={copyToClipboard}      size="icon"      variant="ghost"      {...props}    >

Lines 1–500 of 555.