mdx-components.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  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="text-xl font-semibold mt-12 mb-4">
  36. {children}
  37. </h2>
  38. );
  39. },
  40. h3: ({ children }: { children?: React.ReactNode }) => {
  41. const id = slugify(extractText(children));
  42. return (
  43. <h3 id={id} className="text-lg font-medium mt-8 mb-3">
  44. {children}
  45. </h3>
  46. );
  47. },
  48. p: ({ children }: { children?: React.ReactNode }) => (
  49. <p className="text-sm text-muted-foreground mb-4 leading-relaxed">
  50. {children}
  51. </p>
  52. ),
  53. ul: ({ children }: { children?: React.ReactNode }) => (
  54. <ul className="list-disc list-inside text-sm text-muted-foreground space-y-1 mb-4">
  55. {children}
  56. </ul>
  57. ),
  58. ol: ({ children }: { children?: React.ReactNode }) => (
  59. <ol className="list-decimal list-inside space-y-2 text-sm text-muted-foreground mb-4">
  60. {children}
  61. </ol>
  62. ),
  63. li: ({ children }: { children?: React.ReactNode }) => <li>{children}</li>,
  64. a: ({ href, children }: { href?: string; children?: React.ReactNode }) => {
  65. if (href?.startsWith("/")) {
  66. return (
  67. <Link href={href} className="text-foreground hover:underline">
  68. {children}
  69. </Link>
  70. );
  71. }
  72. return (
  73. <a
  74. href={href}
  75. className="text-foreground hover:underline"
  76. target="_blank"
  77. rel="noopener noreferrer"
  78. >
  79. {children}
  80. </a>
  81. );
  82. },
  83. code: ({
  84. children,
  85. className,
  86. }: {
  87. children?: React.ReactNode;
  88. className?: string;
  89. }) => {
  90. // Fenced code blocks come through as <pre><code className="language-xxx">
  91. // Inline code has no className
  92. if (className) {
  93. // This is a fenced code block inside <pre> - handled by the pre component
  94. return <code className={className}>{children}</code>;
  95. }
  96. return (
  97. <code className="text-foreground bg-muted px-1.5 py-0.5 rounded text-sm">
  98. {children}
  99. </code>
  100. );
  101. },
  102. pre: async ({ children }: { children?: React.ReactNode }) => {
  103. // Extract lang and code from the <code> child
  104. const codeElement = children as React.ReactElement<{
  105. className?: string;
  106. children?: string;
  107. }>;
  108. const className = codeElement?.props?.className || "";
  109. const lang = className.replace("language-", "") || "typescript";
  110. const code = codeElement?.props?.children || "";
  111. return (
  112. <Code
  113. lang={lang as "json" | "tsx" | "typescript" | "bash" | "javascript"}
  114. >
  115. {typeof code === "string" ? code : String(code)}
  116. </Code>
  117. );
  118. },
  119. strong: ({ children }: { children?: React.ReactNode }) => (
  120. <strong className="text-foreground font-medium">{children}</strong>
  121. ),
  122. hr: () => <hr className="my-8 border-border" />,
  123. blockquote: ({ children }: { children?: React.ReactNode }) => (
  124. <blockquote className="border-l-2 border-border pl-4 my-4 text-sm text-muted-foreground italic">
  125. {children}
  126. </blockquote>
  127. ),
  128. table: ({ children }: { children?: React.ReactNode }) => (
  129. <div className="my-6 overflow-x-auto">
  130. <table className="mdx-table w-full text-sm border-collapse">
  131. {children}
  132. </table>
  133. </div>
  134. ),
  135. th: ({ children }: { children?: React.ReactNode }) => (
  136. <th className="border border-border px-4 py-2 text-left font-semibold bg-muted">
  137. {children}
  138. </th>
  139. ),
  140. td: ({ children }: { children?: React.ReactNode }) => (
  141. <td className="border border-border px-4 py-2 text-muted-foreground">
  142. {children}
  143. </td>
  144. ),
  145. em: ({ children }: { children?: React.ReactNode }) => <em>{children}</em>,
  146. // Custom components available in all MDX files
  147. GenerationModesDiagram,
  148. PackageInstall,
  149. };
  150. }