md-components.tsx (11.51 KB)
// These components are taken from streamdown.
// @see https://streamdown.ai/
// @see https://github.com/vercel/streamdown
// Thank you Vercel!! Very Cool.

import { type JSX, memo } from "react";
import type { Components } from "react-markdown";
import ShikiHighlighter from "react-shiki";
import { cn } from "../lib/utils";

type MarkdownPoint = { line?: number; column?: number };
type MarkdownPosition = { start?: MarkdownPoint; end?: MarkdownPoint };
type MarkdownNode = {
  position?: MarkdownPosition;
  properties?: { className?: string };
};

type WithNode<T> = T & {
  node?: MarkdownNode;
  children?: React.ReactNode;
  className?: string;
};

function sameNodePosition(prev?: MarkdownNode, next?: MarkdownNode): boolean {
  if (!(prev?.position || next?.position)) {
    return true;
  }
  if (!(prev?.position && next?.position)) {
    return false;
  }

  const prevStart = prev.position.start;
  const nextStart = next.position.start;
  const prevEnd = prev.position.end;
  const nextEnd = next.position.end;

  return (
    prevStart?.line === nextStart?.line &&
    prevStart?.column === nextStart?.column &&
    prevEnd?.line === nextEnd?.line &&
    prevEnd?.column === nextEnd?.column
  );
}

// Helper to compare className and node
function sameClassAndNode(
  prev: { className?: string; node?: MarkdownNode },
  next: { className?: string; node?: MarkdownNode }
) {
  return (
    prev.className === next.className && sameNodePosition(prev.node, next.node)
  );
}

type OlProps = WithNode<JSX.IntrinsicElements["ol"]>;
const MemoOl = memo<OlProps>(
  ({ children, className, node, ...props }: OlProps) => (
    <ol
      className={cn(
        "ml-4 list-outside list-decimal whitespace-normal",
        className
      )}
      {...props}
    >
      {children}
    </ol>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoOl.displayName = "MarkdownOl";

type UlProps = WithNode<JSX.IntrinsicElements["ul"]>;
const MemoUl = memo<UlProps>(
  ({ children, className, node, ...props }: UlProps) => (
    <ul
      className={cn("ml-4 list-outside list-disc whitespace-normal", className)}
      {...props}
    >
      {children}
    </ul>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoUl.displayName = "MarkdownUl";

type LiProps = WithNode<JSX.IntrinsicElements["li"]>;
const MemoLi = memo<LiProps>(
  ({ children, className, node, ...props }: LiProps) => (
    <li className={cn("py-1", className)} {...props}>
      {children}
    </li>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoLi.displayName = "MarkdownLi";

type HrProps = WithNode<JSX.IntrinsicElements["hr"]>;
const MemoHr = memo<HrProps>(
  ({ className, node, ...props }: HrProps) => (
    <hr className={cn("my-6 border-border", className)} {...props} />
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoHr.displayName = "MarkdownHr";

type StrongProps = WithNode<JSX.IntrinsicElements["strong"]>;
const MemoStrong = memo<StrongProps>(
  ({ children, className, node, ...props }: StrongProps) => (
    <strong className={cn("font-semibold", className)} {...props}>
      {children}
    </strong>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoStrong.displayName = "MarkdownStrong";

type EmProps = WithNode<JSX.IntrinsicElements["em"]>;
const MemoEm = memo<EmProps>(
  ({ children, className, node, ...props }: EmProps) => (
    <em className={cn("italic", className)} {...props}>
      {children}
    </em>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoEm.displayName = "MarkdownEm";

type AProps = WithNode<JSX.IntrinsicElements["a"]> & { href?: string };
const MemoA = memo<AProps>(
  ({ children, className, href, node, ...props }: AProps) => (
    <a
      className={cn(
        "wrap-anywhere font-medium text-primary underline",
        className
      )}
      href={href}
      rel="noreferrer"
      target="_blank"
      {...props}
    >
      {children}
    </a>
  ),
  (p, n) => sameClassAndNode(p, n) && p.href === n.href
);
MemoA.displayName = "MarkdownA";

type HeadingProps<TTag extends keyof JSX.IntrinsicElements> = WithNode<
  JSX.IntrinsicElements[TTag]
>;

const MemoH1 = memo<HeadingProps<"h1">>(
  ({ children, className, node, ...props }) => (
    <h1
      className={cn("mt-6 mb-2 font-semibold text-3xl", className)}
      {...props}
    >
      {children}
    </h1>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoH1.displayName = "MarkdownH1";

const MemoH2 = memo<HeadingProps<"h2">>(
  ({ children, className, node, ...props }) => (
    <h2
      className={cn("mt-6 mb-2 font-semibold text-2xl", className)}
      {...props}
    >
      {children}
    </h2>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoH2.displayName = "MarkdownH2";

const MemoH3 = memo<HeadingProps<"h3">>(
  ({ children, className, node, ...props }) => (
    <h3 className={cn("mt-6 mb-2 font-semibold text-xl", className)} {...props}>
      {children}
    </h3>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoH3.displayName = "MarkdownH3";

const MemoH4 = memo<HeadingProps<"h4">>(
  ({ children, className, node, ...props }) => (
    <h4 className={cn("mt-6 mb-2 font-semibold text-lg", className)} {...props}>
      {children}
    </h4>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoH4.displayName = "MarkdownH4";

const MemoH5 = memo<HeadingProps<"h5">>(
  ({ children, className, node, ...props }) => (
    <h5
      className={cn("mt-6 mb-2 font-semibold text-base", className)}
      {...props}
    >
      {children}
    </h5>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoH5.displayName = "MarkdownH5";

const MemoH6 = memo<HeadingProps<"h6">>(
  ({ children, className, node, ...props }) => (
    <h6 className={cn("mt-6 mb-2 font-semibold text-sm", className)} {...props}>
      {children}
    </h6>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoH6.displayName = "MarkdownH6";

type TableProps = WithNode<JSX.IntrinsicElements["table"]>;
const MemoTable = memo<TableProps>(
  ({ children, className, node, ...props }: TableProps) => (
    <div className="my-4">
      <div className="overflow-x-auto">
        <table
          className={cn(
            "w-full border-collapse border border-border",
            className
          )}
          {...props}
        >
          {children}
        </table>
      </div>
    </div>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoTable.displayName = "MarkdownTable";

type TheadProps = WithNode<JSX.IntrinsicElements["thead"]>;
const MemoThead = memo<TheadProps>(
  ({ children, className, node, ...props }: TheadProps) => (
    <thead className={cn("bg-muted/80", className)} {...props}>
      {children}
    </thead>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoThead.displayName = "MarkdownThead";

type TbodyProps = WithNode<JSX.IntrinsicElements["tbody"]>;
const MemoTbody = memo<TbodyProps>(
  ({ children, className, node, ...props }: TbodyProps) => (
    <tbody
      className={cn("divide-y divide-border bg-muted/40", className)}
      {...props}
    >
      {children}
    </tbody>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoTbody.displayName = "MarkdownTbody";

type TrProps = WithNode<JSX.IntrinsicElements["tr"]>;
const MemoTr = memo<TrProps>(
  ({ children, className, node, ...props }: TrProps) => (
    <tr className={cn("border-border border-b", className)} {...props}>
      {children}
    </tr>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoTr.displayName = "MarkdownTr";

type ThProps = WithNode<JSX.IntrinsicElements["th"]>;
const MemoTh = memo<ThProps>(
  ({ children, className, node, ...props }: ThProps) => (
    <th
      className={cn(
        "whitespace-nowrap px-4 py-2 text-left font-semibold text-sm",
        className
      )}
      {...props}
    >
      {children}
    </th>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoTh.displayName = "MarkdownTh";

type TdProps = WithNode<JSX.IntrinsicElements["td"]>;
const MemoTd = memo<TdProps>(
  ({ children, className, node, ...props }: TdProps) => (
    <td className={cn("px-4 py-2 text-sm", className)} {...props}>
      {children}
    </td>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoTd.displayName = "MarkdownTd";

type BlockquoteProps = WithNode<JSX.IntrinsicElements["blockquote"]>;
const MemoBlockquote = memo<BlockquoteProps>(
  ({ children, className, node, ...props }: BlockquoteProps) => (
    <blockquote
      className={cn(
        "my-4 border-muted-foreground/30 border-l-4 pl-4 text-muted-foreground italic",
        className
      )}
      {...props}
    >
      {children}
    </blockquote>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoBlockquote.displayName = "MarkdownBlockquote";

type SupProps = WithNode<JSX.IntrinsicElements["sup"]>;
const MemoSup = memo<SupProps>(
  ({ children, className, node, ...props }: SupProps) => (
    <sup className={cn("text-sm", className)} {...props}>
      {children}
    </sup>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoSup.displayName = "MarkdownSup";

type SubProps = WithNode<JSX.IntrinsicElements["sub"]>;
const MemoSub = memo<SubProps>(
  ({ children, className, node, ...props }: SubProps) => (
    <sub className={cn("text-sm", className)} {...props}>
      {children}
    </sub>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoSub.displayName = "MarkdownSub";

type ParagraphProps = WithNode<JSX.IntrinsicElements["p"]>;
const MemoParagraph = memo<ParagraphProps>(
  ({ children, className, node, ...props }: ParagraphProps) => (
    <p className={cn("my-2", className)} {...props}>
      {children}
    </p>
  ),
  (p, n) => sameClassAndNode(p, n)
);
MemoParagraph.displayName = "MarkdownParagraph";

type CodeProps = WithNode<JSX.IntrinsicElements["code"]>;
const MemoCode = memo<CodeProps>(
  ({ children, className, node, ...props }: CodeProps) => {
    const codeString = String(children);
    const inline = !codeString.includes("\n");

    if (inline) {
      return (
        <code
          className={cn(
            "rounded bg-muted px-1.5 py-0.5 font-mono text-sm",
            className
          )}
          {...props}
        >
          {children}
        </code>
      );
    }

    const cleanCode = codeString.replace(/\n$/, "");
    const match = /language-(\w+)/.exec(className || "");
    const language = match?.[1] || "text";

    return (
      <div className="my-4">
        <ShikiHighlighter
          className={cn("overflow-x-auto rounded-lg border", className)}
          language={language}
          showLanguage={false}
          theme="github-dark-default"
        >
          {cleanCode}
        </ShikiHighlighter>
      </div>
    );
  },
  (p, n) => sameClassAndNode(p, n)
);
MemoCode.displayName = "MarkdownCode";

type PreProps = WithNode<JSX.IntrinsicElements["pre"]>;
const MemoPre = memo<PreProps>(
  ({ children }: PreProps) => <>{children}</>,
  () => true
);
MemoPre.displayName = "MarkdownPre";

type ImgProps = WithNode<JSX.IntrinsicElements["img"]> & {
  src?: string;
  alt?: string;
};
const MemoImg = memo<ImgProps>(
  ({ src, alt, className, node, ...props }: ImgProps) => (
    // biome-ignore lint/correctness/useImageSize: Markdown images have dynamic dimensions
    <img
      alt={alt || ""}
      className={cn("my-4 h-auto max-w-full rounded-lg", className)}
      loading="lazy"
      src={src}
      {...props}
    />
  ),
  (p, n) => sameClassAndNode(p, n) && p.src === n.src && p.alt === n.alt
);
MemoImg.displayName = "MarkdownImg";

export const components: Components = {
  ol: MemoOl,
  ul: MemoUl,
  li: MemoLi,
  hr: MemoHr,
  strong: MemoStrong,
  em: MemoEm,
  a: MemoA,
  h1: MemoH1,
  h2: MemoH2,
  h3: MemoH3,
  h4: MemoH4,
  h5: MemoH5,
  h6: MemoH6,
  table: MemoTable,
  thead: MemoThead,
  tbody: MemoTbody,
  tr: MemoTr,
  th: MemoTh,
  td: MemoTd,
  blockquote: MemoBlockquote,
  sup: MemoSup,
  sub: MemoSub,
  p: MemoParagraph,
  code: MemoCode,
  pre: MemoPre,
  img: MemoImg,
};