Skip to content

Commit c4840c9

Browse files
committed
support MDX components for TOC
1 parent 01b2bdd commit c4840c9

File tree

8 files changed

+72
-34
lines changed

8 files changed

+72
-34
lines changed

src/components/Layout/Toc.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function Toc({headings}: {headings: Toc}) {
5151
'block hover:text-link dark:hover:text-link-dark leading-normal py-2'
5252
)}
5353
href={h.url}>
54-
{h.text}
54+
{h.node}
5555
</a>
5656
</li>
5757
);

src/components/MDX/InlineCode.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1+
'use client';
2+
13
/*
24
* Copyright (c) Facebook, Inc. and its affiliates.
35
*/
46

57
import cn from 'classnames';
6-
import type {HTMLAttributes} from 'react';
8+
import {useContext, type HTMLAttributes} from 'react';
9+
import {LinkContext} from './Link';
710

811
interface InlineCodeProps {
9-
isLink?: boolean;
1012
meta?: string;
1113
}
12-
function InlineCode({
13-
isLink,
14-
...props
15-
}: HTMLAttributes<HTMLElement> & InlineCodeProps) {
14+
function InlineCode({...props}: HTMLAttributes<HTMLElement> & InlineCodeProps) {
15+
const isLink = useContext(LinkContext);
1616
return (
1717
<code
1818
dir="ltr" // This is needed to prevent the code from inheriting the RTL direction of <html> in case of RTL languages to avoid like `()console.log` to be rendered as `console.log()`

src/components/MDX/InlineToc.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22

3-
import Link from 'next/link';
3+
// import Link from 'next/link';
4+
import Link from './Link';
45
import {useContext, useMemo} from 'react';
56
import {Toc, TocContext, TocItem} from './TocContext';
67
import {UL, LI} from './Primitives';
@@ -50,7 +51,7 @@ function InlineTocItem({items}: {items: Array<NestedTocNode>}) {
5051
<UL>
5152
{items.map((node) => (
5253
<LI key={node.item.url}>
53-
<Link href={node.item.url}>{node.item.text}</Link>
54+
<Link href={node.item.url}>{node.item.node}</Link>
5455
{node.children.length > 0 && <InlineTocItem items={node.children} />}
5556
</LI>
5657
))}

src/components/MDX/Link.tsx

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1+
'use client';
2+
13
/*
24
* Copyright (c) Facebook, Inc. and its affiliates.
35
*/
46

5-
import {Children, cloneElement} from 'react';
7+
import {createContext} from 'react';
68
import NextLink from 'next/link';
79
import cn from 'classnames';
810

911
import {ExternalLink} from 'components/ExternalLink';
1012

13+
export const LinkContext = createContext(false);
14+
1115
function Link({
1216
href,
1317
className,
@@ -16,36 +20,29 @@ function Link({
1620
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
1721
const classes =
1822
'inline text-link dark:text-link-dark border-b border-link border-opacity-0 hover:border-opacity-100 duration-100 ease-in transition leading-normal';
19-
const modifiedChildren = Children.toArray(children).map((child: any) => {
20-
if (child.props?.['data-mdx-name'] === 'inlineCode') {
21-
return cloneElement(child, {
22-
isLink: true,
23-
});
24-
}
25-
return child;
26-
});
2723

2824
if (!href) {
2925
// eslint-disable-next-line jsx-a11y/anchor-has-content
3026
return <a href={href} className={className} {...props} />;
3127
}
28+
3229
return (
33-
<>
30+
<LinkContext.Provider value={true}>
3431
{href.startsWith('https://') ? (
3532
<ExternalLink href={href} className={cn(classes, className)} {...props}>
36-
{modifiedChildren}
33+
{children}
3734
</ExternalLink>
3835
) : href.startsWith('#') ? (
3936
// eslint-disable-next-line jsx-a11y/anchor-has-content
4037
<a className={cn(classes, className)} href={href} {...props}>
41-
{modifiedChildren}
38+
{children}
4239
</a>
4340
) : (
4441
<NextLink href={href} className={cn(classes, className)} {...props}>
45-
{modifiedChildren}
42+
{children}
4643
</NextLink>
4744
)}
48-
</>
45+
</LinkContext.Provider>
4946
);
5047
}
5148

src/components/MDX/MDXComponents.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,11 @@ function annotateMDXComponents(
242242
}, {} as Record<string, React.ElementType>);
243243
}
244244

245+
export const MDXComponentsToc = annotateMDXComponents({
246+
a: Link,
247+
code: InlineCode,
248+
});
249+
245250
export const MDXComponents = annotateMDXComponents({
246251
p: P,
247252
strong: Strong,

src/components/MDX/TocContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {ReactNode} from 'react';
77

88
export type TocItem = {
99
url: string;
10-
text: ReactNode;
10+
node: ReactNode;
1111
depth: number;
1212
};
1313
export type Toc = Array<TocItem>;

src/utils/generateMDX.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as runtime from 'react/jsx-runtime';
66
import {remarkPlugins} from '../../plugins/markdownToHtml';
77
import remarkGfm from 'remark-gfm';
88
import remarkFrontmatter from 'remark-frontmatter';
9-
import {MDXComponents} from '../components/MDX/MDXComponents';
9+
import {MDXComponents, MDXComponentsToc} from '../components/MDX/MDXComponents';
1010
import {MaxWidthWrapperPlugin} from './mdx/MaxWidthWrapperPlugin';
1111
import {ExtractedTOC, TOCExtractorPlugin} from './mdx/TOCExtractorPlugin';
1212
import {MetaAttributesPlugin} from './mdx/MetaAttributesPlugin';
@@ -96,22 +96,44 @@ export async function generateMDX(
9696
});
9797

9898
const {data: meta} = grayMatter(mdx);
99+
const toc = compiled.data.toc as ExtractedTOC[];
99100
const result: CachedResult = {
100101
code: String(compiled),
101-
toc: compiled.data.toc as ExtractedTOC[],
102+
toc,
102103
meta,
103104
};
104105

105106
await writeToCache(store, hash, result, path);
106107

108+
const tocWithMDX = await Promise.all(
109+
toc.map(async (item) => {
110+
if (typeof item.node !== 'string') {
111+
return item;
112+
}
113+
114+
const compiled = await compile(item.node, {
115+
outputFormat: 'function-body',
116+
});
117+
118+
const {default: MDXContent} = await run(compiled, {
119+
...runtime,
120+
baseUrl: import.meta.url,
121+
});
122+
123+
item.node = <MDXContent components={{...MDXComponentsToc}} />;
124+
125+
return item;
126+
})
127+
);
128+
107129
const {default: MDXContent} = await run(result.code, {
108130
...runtime,
109131
baseUrl: import.meta.url,
110132
});
111133

112134
return {
113135
content: <MDXContent components={{...MDXComponents}} />,
114-
toc: result.toc,
136+
toc: tocWithMDX,
115137
meta: result.meta,
116138
};
117139
}

src/utils/mdx/TOCExtractorPlugin.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@ interface MDXJsxFlowElementNode extends Node {
2626

2727
export interface ExtractedTOC {
2828
url: string;
29-
text: string;
29+
node: string | JSX.Element;
3030
depth: number;
3131
}
3232

3333
interface PluginOptions {
3434
maxDepth?: number;
3535
}
3636

37-
export function TOCExtractorPlugin({maxDepth = Infinity}: PluginOptions = {}) {
37+
export function TOCExtractorPlugin({maxDepth = 3}: PluginOptions = {}) {
3838
return (tree: Node, file: any) => {
3939
const toc: ExtractedTOC[] = [];
4040

@@ -45,17 +45,30 @@ export function TOCExtractorPlugin({maxDepth = Infinity}: PluginOptions = {}) {
4545
if (headingNode.depth > maxDepth) {
4646
return;
4747
}
48+
49+
const mdxSource = file.value
50+
.slice(
51+
// @ts-ignore
52+
node.children[0].position!.start.offset,
53+
// @ts-ignore
54+
node.children[0].position!.end.offset
55+
)
56+
.trim();
57+
58+
console.log(mdxSource);
59+
4860
const text = headingNode.children
4961
.filter((child) => child.type === 'text' && child.value)
5062
.map((child) => child.value!)
5163
.join('');
64+
5265
const id =
5366
headingNode.data?.hProperties?.id ||
5467
text.toLowerCase().replace(/\s+/g, '-');
5568

5669
toc.push({
5770
depth: headingNode.depth,
58-
text,
71+
node: mdxSource,
5972
url: `#${id}`,
6073
});
6174
}
@@ -81,7 +94,7 @@ export function TOCExtractorPlugin({maxDepth = Infinity}: PluginOptions = {}) {
8194
toc.push({
8295
url: `#${permalink}`,
8396
depth: 3,
84-
text: name,
97+
node: name,
8598
});
8699
break;
87100
}
@@ -90,14 +103,14 @@ export function TOCExtractorPlugin({maxDepth = Infinity}: PluginOptions = {}) {
90103
toc.push({
91104
url: '#challenges',
92105
depth: 2,
93-
text: 'Challenges',
106+
node: 'Challenges',
94107
});
95108
break;
96109
case 'Recap':
97110
toc.push({
98111
url: '#recap',
99112
depth: 2,
100-
text: 'Recap',
113+
node: 'Recap',
101114
});
102115
break;
103116
default:
@@ -110,7 +123,7 @@ export function TOCExtractorPlugin({maxDepth = Infinity}: PluginOptions = {}) {
110123
if (toc.length > 0) {
111124
toc.unshift({
112125
url: '#',
113-
text: 'Overview',
126+
node: 'Overview',
114127
depth: 2,
115128
});
116129
}

0 commit comments

Comments
 (0)