diff --git a/nx-dev/ui-markdoc/src/index.ts b/nx-dev/ui-markdoc/src/index.ts index 78dd435c98e67..5610632a050fb 100644 --- a/nx-dev/ui-markdoc/src/index.ts +++ b/nx-dev/ui-markdoc/src/index.ts @@ -56,6 +56,8 @@ import { pill } from './lib/tags/pill.schema'; import { fence } from './lib/nodes/fence.schema'; import { FenceWrapper } from './lib/nodes/fence-wrapper.component'; import { VideoPlayer, videoPlayer } from './lib/tags/video-player.component'; +import { TableOfContents } from './lib/tags/table-of-contents.component'; +import { tableOfContents } from './lib/tags/table-of-contents.schema'; // TODO fix this export export { GithubRepository } from './lib/tags/github-repository.component'; @@ -92,6 +94,7 @@ export const getMarkdocCustomConfig = ( tab, tabs, 'terminal-video': terminalVideo, + toc: tableOfContents, tweet, youtube, 'video-link': videoLink, @@ -121,6 +124,7 @@ export const getMarkdocCustomConfig = ( SideBySide, Tab, Tabs, + TableOfContents, TerminalVideo, Tweet, YouTube, diff --git a/nx-dev/ui-markdoc/src/lib/tags/table-of-contents.component.tsx b/nx-dev/ui-markdoc/src/lib/tags/table-of-contents.component.tsx new file mode 100644 index 0000000000000..5ffc459de4dfe --- /dev/null +++ b/nx-dev/ui-markdoc/src/lib/tags/table-of-contents.component.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +interface TocItem { + id: string; + text: string; + level: number; +} + +interface TableOfContentsProps { + maxDepth?: number; +} + +export function TableOfContents({ + maxDepth = 3, +}: TableOfContentsProps): JSX.Element { + const [headings, setHeadings] = useState([]); + + useEffect(() => { + // Find the main content wrapper where markdown content is rendered + const content = document.querySelector('[data-document="main"]'); + if (!content) return; + + // Get all headings h1-h6 within the content + const headingElements = content.querySelectorAll('h1, h2, h3, h4, h5, h6'); + + const items: TocItem[] = Array.from(headingElements) + .map((heading) => { + const level = parseInt(heading.tagName[1]); + if (level > maxDepth) return null; + + return { + id: heading.id, + text: heading.textContent || '', + level, + }; + }) + .filter((item): item is TocItem => item !== null); + + setHeadings(items); + }, [maxDepth]); + + if (headings.length === 0) { + return null; + } + + return ( + + ); +} diff --git a/nx-dev/ui-markdoc/src/lib/tags/table-of-contents.schema.ts b/nx-dev/ui-markdoc/src/lib/tags/table-of-contents.schema.ts new file mode 100644 index 0000000000000..b25d123ca9afc --- /dev/null +++ b/nx-dev/ui-markdoc/src/lib/tags/table-of-contents.schema.ts @@ -0,0 +1,11 @@ +import { Schema } from '@markdoc/markdoc'; + +export const tableOfContents: Schema = { + render: 'TableOfContents', + attributes: { + maxDepth: { + type: 'Number', + default: 3, + }, + }, +};