@@ -26,6 +26,13 @@ function parseMeta(entry: Record<string, any>) {
2626 return {}
2727}
2828
29+ interface TocLink {
30+ id: string
31+ depth: number
32+ text: string
33+ children? : TocLink []
34+ }
35+
2936function gatherNodeText(node : any ): string {
3037 if (! node || typeof node !== ' object' ) {
3138 return ' '
@@ -61,6 +68,75 @@ function extractFirstParagraphText(body: any): string | undefined {
6168 return undefined
6269}
6370
71+ function buildTocLinksFromBody(body : MarkdownRoot | undefined | null ): TocLink [] {
72+ const children = Array .isArray (body ?.children ) ? body ! .children : []
73+ const links: TocLink [] = []
74+ const stack: Array <{ depth: number , link: TocLink }> = []
75+
76+ for (const node of children ) {
77+ if (! node || typeof node !== ' object' ) {
78+ continue
79+ }
80+ const tag = typeof (node as any ).tag === ' string' ? (node as any ).tag : null
81+ if (! tag || ! tag .startsWith (' h' )) {
82+ continue
83+ }
84+
85+ const depth = Number .parseInt (tag .slice (1 ), 10 )
86+ if (! Number .isFinite (depth ) || depth < 2 ) {
87+ continue
88+ }
89+
90+ const id = typeof (node as any ).props ?.id === ' string' ? (node as any ).props .id : null
91+ if (! id ) {
92+ continue
93+ }
94+
95+ const text = gatherNodeText (node ).trim ()
96+ if (! text ) {
97+ continue
98+ }
99+
100+ const link: TocLink = {
101+ id ,
102+ depth ,
103+ text ,
104+ children: [],
105+ }
106+
107+ while (stack .length > 0 && stack [stack .length - 1 ]! .depth >= depth ) {
108+ stack .pop ()
109+ }
110+
111+ if (stack .length === 0 ) {
112+ links .push (link )
113+ }
114+ else {
115+ const parent = stack [stack .length - 1 ]! .link
116+ if (! parent .children ) {
117+ parent .children = []
118+ }
119+ parent .children .push (link )
120+ }
121+
122+ stack .push ({ depth , link })
123+ }
124+
125+ function normalize(nodes : TocLink []): TocLink [] {
126+ return nodes .map ((node ) => {
127+ if (node .children && node .children .length === 0 ) {
128+ delete node .children
129+ }
130+ else if (node .children ) {
131+ node .children = normalize (node .children )
132+ }
133+ return node
134+ })
135+ }
136+
137+ return normalize (links )
138+ }
139+
64140const { data : article } = await useAsyncData (` article:${contentPath } ` , async () => {
65141 const entry = await queryCollection (' articles' )
66142 .path (contentPath )
@@ -119,7 +195,13 @@ const { data: adjacent } = await useAsyncData(`article-nav:${contentPath}`, asyn
119195 }
120196})
121197
122- const tocLinks = computed (() => article .value ?.body ?.toc ?.links ?? [])
198+ const tocLinks = computed <TocLink []>(() => {
199+ const rawLinks = article .value ?.body ?.toc ?.links
200+ if (Array .isArray (rawLinks ) && rawLinks .length > 0 ) {
201+ return rawLinks as TocLink []
202+ }
203+ return buildTocLinksFromBody (article .value ?.body )
204+ })
123205const hasToc = computed (() => tocLinks .value .length > 0 )
124206const isTocOpen = ref (false )
125207const previousArticle = computed (() => adjacent .value ?.prev ?? null )
0 commit comments