120 lines
4.0 KiB
TypeScript
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} />,
|
|
}; |