Files
website/components/mdx.tsx
2025-09-12 23:20:36 +02:00

120 lines
4.0 KiB
TypeScript

"use client";
import Image from "next/image";
import Link from "next/link";
import React, { Children, isValidElement } from "react";
import { InfoBox } from "./infobox";
import { Cite } from "./cite";
import { FloatingImage } from "./floating-image";
import { CenteredImage } from "./centered-image";
import { FaGithub, FaPython, FaBook, FaFileAlt } from 'react-icons/fa';
function CustomLink(props: any) {
let href = props.href;
if (href.startsWith("/")) {
return (
<Link href={href} {...props}>
{props.children}
</Link>
);
}
if (href.startsWith("#")) {
return <a {...props} />;
}
return <a target="_blank" rel="noopener noreferrer" {...props} />;
}
function RoundedImage(props: any) {
return <Image alt={props.alt} className="rounded-lg" {...props} />;
}
// Helper to extract plain text content from React children for slugification.
function getPlainTextFromChildren(nodes: React.ReactNode): string {
return Children.toArray(nodes)
.map(node => {
if (typeof node === 'string') {
return node;
}
// Recursively extract text from valid React elements
if (isValidElement(node) && node.props && node.props.children) {
return getPlainTextFromChildren(node.props.children);
}
return ''; // Ignore other types of nodes (null, undefined, numbers etc.)
})
.join('');
}
// Robust slugification function
function slugify(str: string) {
return str
.toString()
.toLowerCase()
.trim() // Remove whitespace from both ends of a string
.replace(/&/g, " and ") // Replace & with ' and ', using spaces to prevent immediate double hyphens
.replace(/[^\w\s-]/g, "") // Remove all non-word characters except spaces and hyphens
.replace(/[\s_-]+/g, "-") // Replace spaces, underscores, or multiple hyphens with a single hyphen
.replace(/^-+|-+$/g, ""); // Remove any leading/trailing hyphens that might remain
}
function createHeading(level: number) {
const Heading = ({ children, ...props }: { children?: React.ReactNode, [key: string]: any }) => {
// Extract plain text from children for reliable slug generation
const plainText = getPlainTextFromChildren(children);
let slug = slugify(plainText);
// Process children to handle HTML entities for text nodes and ensure unique keys
const processedChildren = Children.map(children, (child, index) => {
if (typeof child === 'string') {
// If it's a string, use dangerouslySetInnerHTML to prevent escaping of entities
// Ensure a key is provided for children in a map
return <span key={`text-content-${slug}-${index}`} dangerouslySetInnerHTML={{ __html: child }} />;
}
// If it's a React element, clone it to add a key if it doesn't have one
if (isValidElement(child)) {
return React.cloneElement(child, { key: child.key || `element-content-${slug}-${index}` });
}
return child; // Return other types as-is (numbers, null, undefined)
});
return React.createElement(
`h${level}`,
{ id: slug, ...props },
[
React.createElement("a", {
href: `#${slug}`,
key: `link-${slug}`,
className: "anchor",
}), ...processedChildren
]
);
};
Heading.displayName = `Heading${level}`;
return Heading;
}
export const mdxComponents = {
h1: createHeading(1),
h2: createHeading(2),
h3: createHeading(3),
h4: createHeading(4),
h5: createHeading(5),
h6: createHeading(6),
Image: RoundedImage,
a: CustomLink,
InfoBox,
Cite,
FloatingImage: FloatingImage,
CenteredImage: CenteredImage,
FaGithub: FaGithub,
FaPython: FaPython,
FaBook: FaBook,
FaFileAlt: FaFileAlt,
ul: (props: React.HTMLAttributes<HTMLUListElement>) => <ul className="list-disc pl-6" {...props} />,
ol: (props: React.HTMLAttributes<HTMLOListElement>) => <ol className="list-decimal pl-6" {...props} />,
li: (props: React.HTMLAttributes<HTMLLIElement>) => <li className="mb-2" {...props} />,
p: (props: React.HTMLAttributes<HTMLParagraphElement>) => <p className="mb-4" {...props} />,
};