103 lines
2.9 KiB
TypeScript
103 lines
2.9 KiB
TypeScript
import type { MDXComponents } from "mdx/types";
|
|
import Link from "next/link";
|
|
import { CodeBlock } from "@/components/code-block";
|
|
|
|
function slugify(text: string): string {
|
|
return text
|
|
.toLowerCase()
|
|
.replace(/[^\w\s-]/g, "")
|
|
.replace(/\s+/g, "-")
|
|
.trim();
|
|
}
|
|
|
|
function extractText(children: React.ReactNode): string {
|
|
if (typeof children === "string") return children;
|
|
if (typeof children === "number") return String(children);
|
|
if (Array.isArray(children)) return children.map(extractText).join("");
|
|
if (children && typeof children === "object") {
|
|
const obj = children as unknown as Record<string, unknown>;
|
|
if ("props" in obj) {
|
|
const props = obj.props as { children?: React.ReactNode } | undefined;
|
|
return extractText(props?.children);
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
export function useMDXComponents(components: MDXComponents): MDXComponents {
|
|
return {
|
|
...components,
|
|
h1: ({ children }: { children?: React.ReactNode }) => {
|
|
const id = slugify(extractText(children));
|
|
return (
|
|
<h1 id={id} className="heading-anchor">
|
|
{children}
|
|
<a href={`#${id}`} aria-label="Link to this section">#</a>
|
|
</h1>
|
|
);
|
|
},
|
|
h2: ({ children }: { children?: React.ReactNode }) => {
|
|
const id = slugify(extractText(children));
|
|
return (
|
|
<h2 id={id} className="heading-anchor">
|
|
{children}
|
|
<a href={`#${id}`} aria-label="Link to this section">#</a>
|
|
</h2>
|
|
);
|
|
},
|
|
h3: ({ children }: { children?: React.ReactNode }) => {
|
|
const id = slugify(extractText(children));
|
|
return (
|
|
<h3 id={id} className="heading-anchor">
|
|
{children}
|
|
<a href={`#${id}`} aria-label="Link to this section">#</a>
|
|
</h3>
|
|
);
|
|
},
|
|
a: ({
|
|
href,
|
|
children,
|
|
}: {
|
|
href?: string;
|
|
children?: React.ReactNode;
|
|
}) => {
|
|
if (href?.startsWith("/")) {
|
|
return <Link href={href}>{children}</Link>;
|
|
}
|
|
return (
|
|
<a href={href} target="_blank" rel="noopener noreferrer">
|
|
{children}
|
|
</a>
|
|
);
|
|
},
|
|
code: ({
|
|
children,
|
|
className,
|
|
}: {
|
|
children?: React.ReactNode;
|
|
className?: string;
|
|
}) => {
|
|
if (className) {
|
|
return <code className={className}>{children}</code>;
|
|
}
|
|
return <code>{children}</code>;
|
|
},
|
|
pre: async ({ children }: { children?: React.ReactNode }) => {
|
|
const codeElement = children as React.ReactElement<{
|
|
className?: string;
|
|
children?: string;
|
|
}>;
|
|
const className = codeElement?.props?.className || "";
|
|
const lang = className.replace("language-", "") || "bash";
|
|
const code = codeElement?.props?.children || "";
|
|
|
|
return (
|
|
<CodeBlock
|
|
code={typeof code === "string" ? code : String(code)}
|
|
lang={lang}
|
|
/>
|
|
);
|
|
},
|
|
};
|
|
}
|