129 lines
4.2 KiB
TypeScript
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} />,
|
|
}; |