mdx-components.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. import type { MDXComponents } from "mdx/types";
  2. import Link from "next/link";
  3. import { Code } from "@/components/code";
  4. import { GenerationModesDiagram } from "@/components/generation-modes-diagram";
  5. import { PackageInstall } from "@/components/package-install";
  6. function slugify(text: string): string {
  7. return text
  8. .toLowerCase()
  9. .replace(/[^\w\s-]/g, "")
  10. .replace(/\s+/g, "-")
  11. .trim();
  12. }
  13. function extractText(children: React.ReactNode): string {
  14. if (typeof children === "string") return children;
  15. if (typeof children === "number") return String(children);
  16. if (Array.isArray(children)) return children.map(extractText).join("");
  17. if (children && typeof children === "object") {
  18. const obj = children as unknown as Record<string, unknown>;
  19. if ("props" in obj) {
  20. const props = obj.props as { children?: React.ReactNode } | undefined;
  21. return extractText(props?.children);
  22. }
  23. }
  24. return "";
  25. }
  26. export function useMDXComponents(components: MDXComponents): MDXComponents {
  27. return {
  28. ...components,
  29. h1: ({ children }: { children?: React.ReactNode }) => (
  30. <h1 className="text-3xl font-bold mb-4">{children}</h1>
  31. ),
  32. h2: ({ children }: { children?: React.ReactNode }) => {
  33. const id = slugify(extractText(children));
  34. return (
  35. <h2 id={id} className="group text-xl font-semibold mt-12 mb-4">
  36. <a href={`#${id}`} className="no-underline hover:no-underline">
  37. {children}
  38. <span className="ml-2 text-muted-foreground/0 group-hover:text-muted-foreground transition-colors select-none">
  39. #
  40. </span>
  41. </a>
  42. </h2>
  43. );
  44. },
  45. h3: ({ children }: { children?: React.ReactNode }) => {
  46. const id = slugify(extractText(children));
  47. return (
  48. <h3 id={id} className="group text-lg font-medium mt-8 mb-3">
  49. <a href={`#${id}`} className="no-underline hover:no-underline">
  50. {children}
  51. <span className="ml-2 text-muted-foreground/0 group-hover:text-muted-foreground transition-colors select-none">
  52. #
  53. </span>
  54. </a>
  55. </h3>
  56. );
  57. },
  58. p: ({ children }: { children?: React.ReactNode }) => (
  59. <p className="text-sm text-muted-foreground mb-4 leading-relaxed">
  60. {children}
  61. </p>
  62. ),
  63. ul: ({ children }: { children?: React.ReactNode }) => (
  64. <ul className="list-disc list-inside text-sm text-muted-foreground space-y-1 mb-4">
  65. {children}
  66. </ul>
  67. ),
  68. ol: ({ children }: { children?: React.ReactNode }) => (
  69. <ol className="list-decimal list-inside space-y-2 text-sm text-muted-foreground mb-4">
  70. {children}
  71. </ol>
  72. ),
  73. li: ({ children }: { children?: React.ReactNode }) => <li>{children}</li>,
  74. a: ({ href, children }: { href?: string; children?: React.ReactNode }) => {
  75. if (href?.startsWith("/")) {
  76. return (
  77. <Link href={href} className="text-foreground hover:underline">
  78. {children}
  79. </Link>
  80. );
  81. }
  82. return (
  83. <a
  84. href={href}
  85. className="text-foreground hover:underline"
  86. target="_blank"
  87. rel="noopener noreferrer"
  88. >
  89. {children}
  90. </a>
  91. );
  92. },
  93. code: ({
  94. children,
  95. className,
  96. }: {
  97. children?: React.ReactNode;
  98. className?: string;
  99. }) => {
  100. // Fenced code blocks come through as <pre><code className="language-xxx">
  101. // Inline code has no className
  102. if (className) {
  103. // This is a fenced code block inside <pre> - handled by the pre component
  104. return <code className={className}>{children}</code>;
  105. }
  106. return (
  107. <code className="text-foreground bg-muted px-1.5 py-0.5 rounded text-sm">
  108. {children}
  109. </code>
  110. );
  111. },
  112. pre: async ({ children }: { children?: React.ReactNode }) => {
  113. // Extract lang and code from the <code> child
  114. const codeElement = children as React.ReactElement<{
  115. className?: string;
  116. children?: string;
  117. }>;
  118. const className = codeElement?.props?.className || "";
  119. const lang = className.replace("language-", "") || "typescript";
  120. const code = codeElement?.props?.children || "";
  121. return (
  122. <Code
  123. lang={lang as "json" | "tsx" | "typescript" | "bash" | "javascript"}
  124. >
  125. {typeof code === "string" ? code : String(code)}
  126. </Code>
  127. );
  128. },
  129. strong: ({ children }: { children?: React.ReactNode }) => (
  130. <strong className="text-foreground font-medium">{children}</strong>
  131. ),
  132. hr: () => <hr className="my-8 border-border" />,
  133. blockquote: ({ children }: { children?: React.ReactNode }) => (
  134. <blockquote className="border-l-2 border-border pl-4 my-4 text-sm text-muted-foreground italic">
  135. {children}
  136. </blockquote>
  137. ),
  138. table: ({ children }: { children?: React.ReactNode }) => (
  139. <div className="my-6 overflow-x-auto">
  140. <table className="mdx-table w-full text-sm border-collapse">
  141. {children}
  142. </table>
  143. </div>
  144. ),
  145. th: ({ children }: { children?: React.ReactNode }) => (
  146. <th className="border border-border px-4 py-2 text-left font-semibold bg-muted">
  147. {children}
  148. </th>
  149. ),
  150. td: ({ children }: { children?: React.ReactNode }) => (
  151. <td className="border border-border px-4 py-2 text-muted-foreground">
  152. {children}
  153. </td>
  154. ),
  155. em: ({ children }: { children?: React.ReactNode }) => <em>{children}</em>,
  156. // Custom components available in all MDX files
  157. GenerationModesDiagram,
  158. PackageInstall,
  159. };
  160. }