1
1
import "server-only"
2
2
3
3
// @ts -expect-error - we already have it in transitive deps and I want to avoid having a duplicate
4
- import { evaluate } from "@mdx-js/mdx"
4
+ import { compile , run } from "@mdx-js/mdx"
5
5
import * as runtime from "react/jsx-runtime"
6
+ import { Root , Element } from "hast"
7
+ import { visit } from "unist-util-visit"
8
+ import { toString } from "hast-util-to-string"
9
+ // @ts -expect-error
10
+ import Slugger from "github-slugger"
6
11
7
12
/**
8
13
* Nextra's builtin MDX support requires jumping out to a Client component
@@ -14,25 +19,68 @@ import * as runtime from "react/jsx-runtime"
14
19
export async function ServerComponentMarkdown ( {
15
20
markdown,
16
21
components = { } ,
22
+ extractToc = false ,
23
+ Wrapper = ( { children } ) => children ,
17
24
} : {
18
25
markdown : string
19
26
components ?: Record < string , React . ComponentType >
27
+ extractToc ?: boolean
28
+ Wrapper ?: React . ComponentType < {
29
+ children : React . ReactNode
30
+ data : { toc : TableOfContents }
31
+ } >
20
32
} ) {
21
33
try {
22
- const { default : Mdx } = await evaluate ( markdown , {
34
+ const rehypePlugins = extractToc ? [ rehypeExtractTableOfContents ] : [ ]
35
+
36
+ const vfile = await compile ( markdown , {
37
+ outputFormat : "function-body" ,
38
+ remarkPlugins : [ ] ,
39
+ rehypePlugins,
40
+ recmaPlugins : [ ] ,
41
+ } )
42
+
43
+ const { default : Mdx } = await run ( String ( vfile ) , {
23
44
...runtime ,
24
- useMDXComponents : ( arg : typeof components ) => {
25
- return {
26
- ...components ,
27
- ...arg ,
28
- }
29
- } ,
45
+ baseUrl : import . meta. url ,
30
46
} )
31
- return Mdx ( )
47
+
48
+ return < Wrapper data = { vfile . data } > { Mdx ( { components } ) } </ Wrapper >
32
49
} catch ( error ) {
33
50
console . error ( error )
34
51
return (
35
52
< div > { error instanceof Error ? error . message : "Error loading MDX" } </ div >
36
53
)
37
54
}
38
55
}
56
+
57
+ type TableOfContents = Array < { value : string ; id : string ; depth : number } >
58
+
59
+ /**
60
+ * Nextra has a built-in plugin like this, but it also steals the heading contents
61
+ * as is tightly coupled with other Nextra features.
62
+ */
63
+ function rehypeExtractTableOfContents ( ) {
64
+ const slugger = new Slugger ( )
65
+
66
+ return function ( tree : Root , file : any ) {
67
+ const toc : TableOfContents = [ ]
68
+
69
+ visit ( tree , "element" , ( node : Element ) => {
70
+ if ( node . tagName && / ^ h [ 1 - 6 ] $ / . test ( node . tagName ) ) {
71
+ const depth = parseInt ( node . tagName . charAt ( 1 ) , 10 )
72
+ const value = toString ( node )
73
+ const slug = slugger . slug ( value )
74
+
75
+ node . properties ||= node . properties || { }
76
+ // add id to the heading element if it's not already set
77
+ node . properties . id ||= slug
78
+
79
+ toc . push ( { value, id : slug , depth } )
80
+ }
81
+ } )
82
+
83
+ file . data = file . data || { }
84
+ file . data . toc = toc
85
+ }
86
+ }
0 commit comments