mdx-components.tsx 4.9 KB

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