Files
website/components/mdx.tsx
Steffen Illium 0444067c2d
Some checks failed
Next.js App CI / docker (push) Failing after 3m19s
refined design
2025-09-14 22:49:23 +02:00

129 lines
4.2 KiB
TypeScript

"use client";
import Image, { ImageProps } from "next/image";
import Link from "next/link";
import React, { AnchorHTMLAttributes, 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';
type CustomLinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
href: string; // We ensure href is a required string
};
function CustomLink({ href, children, ...rest }: CustomLinkProps) {
if (href.startsWith("/")) {
return (
<Link href={href} {...rest}>
{children}
</Link>
);
}
if (href.startsWith("#")) {
// The same pattern applies to regular <a> tags.
return <a href={href} {...rest}>{children}</a>;
}
// We explicitly add href and children, and spread the rest.
return (
<a href={href} target="_blank" rel="noopener noreferrer" {...rest}>
{children}
</a>
);
}
function RoundedImage(props: ImageProps) {
<Image
{...props}
alt={props.alt}
className={`rounded-lg ${props.className || ''}`}
/>
}
// 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;
}
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]: unknown }) => {
// Extract plain text from children for reliable slug generation
const plainText = getPlainTextFromChildren(children);
const 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} />,
};