Skip to content

Commit 29588fe

Browse files
committed
Refactor heading components to improve type safety and support ReactNode slugs
1 parent 21d23c9 commit 29588fe

File tree

1 file changed

+46
-24
lines changed

1 file changed

+46
-24
lines changed

src/mdx-components.tsx

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import type { MDXComponents } from 'mdx/types';
22
import Image, { type ImageProps } from 'next/image';
33
import Link from 'next/link';
44
import { HashIcon } from 'lucide-react';
5-
import type { AnchorHTMLAttributes, PropsWithChildren, ReactNode } from 'react';
5+
import React, {
6+
isValidElement,
7+
type AnchorHTMLAttributes,
8+
type PropsWithChildren,
9+
type ReactNode,
10+
} from 'react';
611
import {
712
Table,
813
TableBody,
@@ -31,9 +36,34 @@ function HeadingLink({
3136
);
3237
}
3338

39+
/**
40+
* Safely convert a ReactNode to a string.
41+
* Handles strings, numbers, booleans, arrays, null/undefined, and React elements by
42+
* recursively extracting their children text.
43+
*/
44+
function reactNodeToString(node: ReactNode): string {
45+
if (node === null || node === undefined) return '';
46+
if (
47+
typeof node === 'string' ||
48+
typeof node === 'number' ||
49+
typeof node === 'boolean'
50+
) {
51+
return String(node);
52+
}
53+
if (Array.isArray(node)) {
54+
return node.map(reactNodeToString).join('');
55+
}
56+
if (isValidElement(node)) {
57+
// If it's a React element, try to extract its children
58+
const element = node as React.ReactElement<{ children?: ReactNode }>;
59+
return reactNodeToString(element.props.children);
60+
}
61+
// Fallback to empty string for other types
62+
return '';
63+
}
64+
3465
export const customComponents = {
3566
h1: ({ children }: { children: ReactNode }) => {
36-
// @ts-expect-error I don't know what types I should define here, but this works at runtime
3767
const slug = slugify(children);
3868
return (
3969
<h1
@@ -50,8 +80,7 @@ export const customComponents = {
5080
export function useMDXComponents(components: MDXComponents): MDXComponents {
5181
return {
5282
h1: customComponents.h1,
53-
h2: ({ children }) => {
54-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
83+
h2: ({ children }: { children: ReactNode }) => {
5584
const slug = slugify(children);
5685
return (
5786
<h2
@@ -62,8 +91,7 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
6291
</h2>
6392
);
6493
},
65-
h3: ({ children }) => {
66-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
94+
h3: ({ children }: { children: ReactNode }) => {
6795
const slug = slugify(children);
6896
return (
6997
<h3
@@ -74,8 +102,7 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
74102
</h3>
75103
);
76104
},
77-
h4: ({ children }) => {
78-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
105+
h4: ({ children }: { children: ReactNode }) => {
79106
const slug = slugify(children);
80107
return (
81108
<h4
@@ -86,8 +113,7 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
86113
</h4>
87114
);
88115
},
89-
h5: ({ children }) => {
90-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
116+
h5: ({ children }: { children: ReactNode }) => {
91117
const slug = slugify(children);
92118
return (
93119
<h5
@@ -98,8 +124,7 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
98124
</h5>
99125
);
100126
},
101-
h6: ({ children }) => {
102-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
127+
h6: ({ children }: { children: ReactNode }) => {
103128
const slug = slugify(children);
104129
return (
105130
<h6
@@ -149,18 +174,15 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
149174
};
150175
}
151176

152-
function slugify(str: string): string {
153-
return (
154-
str
155-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-conversion
156-
.toString()
157-
.toLowerCase()
158-
.trim() // Remove whitespace from both ends of a string
159-
.replace(/\s+/g, '-') // Replace spaces with -
160-
.replace(/&/g, '-and-') // Replace & with 'and'
161-
.replace(/[^\w-]+/g, '') // Remove all non-word characters except for -
162-
.replace(/--+/g, '-')
163-
); // Replace multiple - with single -
177+
function slugify(node: ReactNode): string {
178+
const str = reactNodeToString(node);
179+
return str
180+
.toLowerCase()
181+
.trim() // Remove whitespace from both ends of a string
182+
.replace(/\s+/g, '-') // Replace spaces with -
183+
.replace(/&/g, '-and-') // Replace & with 'and'
184+
.replace(/[^\w-]+/g, '') // Remove all non-word characters except for -
185+
.replace(/--+/g, '-'); // Replace multiple - with single -
164186
}
165187

166188
function CustomLink(props: AnchorHTMLAttributes<HTMLAnchorElement>) {

0 commit comments

Comments
 (0)