Skip to content

Commit aae3763

Browse files
committed
feat: add framework support to documentation components
1 parent 0bd8b98 commit aae3763

6 files changed

Lines changed: 122 additions & 47 deletions

File tree

src/components/Doc.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { renderMarkdown } from '~/utils/markdown'
99
import { DocBreadcrumb } from './DocBreadcrumb'
1010
import { MarkdownContent } from '~/components/markdown'
1111
import type { ConfigSchema } from '~/utils/config'
12+
import { useLocalCurrentFramework } from './FrameworkSelect'
13+
import { useParams } from '@tanstack/react-router'
1214

1315
type DocProps = {
1416
title: string
@@ -28,6 +30,8 @@ type DocProps = {
2830
config?: ConfigSchema
2931
// Footer content rendered after markdown
3032
footer?: React.ReactNode
33+
// Optional framework to use (overrides URL and local storage)
34+
framework?: string
3135
}
3236

3337
export function Doc({
@@ -45,13 +49,22 @@ export function Doc({
4549
pagePath,
4650
config,
4751
footer,
52+
framework: frameworkProp,
4853
}: DocProps) {
4954
// Extract headings synchronously during render to avoid hydration mismatch
5055
const { headings, markup } = React.useMemo(
5156
() => renderMarkdown(content),
5257
[content],
5358
)
5459

60+
// Get current framework from prop, URL params, or local storage
61+
const { framework: paramsFramework } = useParams({ strict: false })
62+
const localCurrentFramework = useLocalCurrentFramework()
63+
const currentFramework = React.useMemo(() => {
64+
const fw = frameworkProp || paramsFramework || localCurrentFramework.currentFramework || 'react'
65+
return typeof fw === 'string' ? fw.toLowerCase() : fw
66+
}, [frameworkProp, paramsFramework, localCurrentFramework.currentFramework])
67+
5568
const isTocVisible = shouldRenderToc && headings.length > 1
5669

5770
const markdownContainerRef = React.useRef<HTMLDivElement>(null)
@@ -170,6 +183,7 @@ export function Doc({
170183
colorFrom={colorFrom}
171184
colorTo={colorTo}
172185
textColor={textColor}
186+
currentFramework={currentFramework}
173187
/>
174188
</div>
175189
)}

src/components/Toc.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,21 @@ type TocProps = {
1818
colorTo?: string
1919
textColor?: string
2020
activeHeadings: Array<string>
21+
currentFramework?: string
2122
}
2223

23-
export function Toc({ headings, textColor, activeHeadings }: TocProps) {
24+
export function Toc({ headings, textColor, activeHeadings, currentFramework }: TocProps) {
25+
// Filter headings based on framework scope
26+
const visibleHeadings = React.useMemo(() => {
27+
return headings.filter((heading) => {
28+
console.log(heading)
29+
if (heading.framework) {
30+
return currentFramework && heading.framework === currentFramework.toLowerCase()
31+
}
32+
// If no framework attribute, always show (not framework-scoped)
33+
return true
34+
})
35+
}, [headings, currentFramework])
2436
return (
2537
<nav className="flex flex-col sticky top-[var(--navbar-height)] max-h-[calc(100dvh-var(--navbar-height))] overflow-hidden">
2638
<div className="py-1">
@@ -33,7 +45,7 @@ export function Toc({ headings, textColor, activeHeadings }: TocProps) {
3345
'py-1 flex flex-col overflow-y-auto text-[.6em] lg:text-[.65em] xl:text-[.7em] 2xl:text-[.75em]',
3446
)}
3547
>
36-
{headings?.map((heading) => (
48+
{visibleHeadings?.map((heading) => (
3749
<li
3850
key={heading.id}
3951
className={twMerge('w-full', headingLevels[heading.level])}

src/routes/$libraryId/$version.docs.framework.$framework.$.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export const Route = createFileRoute(
6767
function Docs() {
6868
const { title, content, filePath } = Route.useLoaderData()
6969
const { config } = docsRouteApi.useLoaderData()
70-
const { version, libraryId } = Route.useParams()
70+
const { version, libraryId, framework } = Route.useParams()
7171
const library = getLibrary(libraryId)
7272
const branch = getBranch(library, version)
7373
const location = useLocation()
@@ -89,6 +89,7 @@ function Docs() {
8989
libraryVersion={version === 'latest' ? library.latestVersion : version}
9090
pagePath={location.pathname}
9191
config={config}
92+
framework={framework}
9293
/>
9394
</DocContainer>
9495
)

src/utils/markdown/plugins/collectHeadings.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type MarkdownHeading = {
77
id: string
88
text: string
99
level: number
10+
framework?: string
1011
}
1112

1213
type HastElement = {
@@ -38,6 +39,14 @@ const isTabsAncestor = (ancestor: HastElement) => {
3839
return typeof component === 'string' && component.toLowerCase() === 'tabs'
3940
}
4041

42+
const isFrameworkPanelAncestor = (ancestor: HastElement) => {
43+
if (ancestor.type !== 'element') {
44+
return false
45+
}
46+
47+
return ancestor.tagName === 'md-framework-panel'
48+
}
49+
4150
export function rehypeCollectHeadings(initialHeadings?: MarkdownHeading[]) {
4251
const headings = initialHeadings ?? []
4352

@@ -62,10 +71,29 @@ export function rehypeCollectHeadings(initialHeadings?: MarkdownHeading[]) {
6271
return
6372
}
6473

74+
let currentFramework: string | undefined
75+
76+
const headingDataFramework = node.properties?.['data-framework']
77+
if (typeof headingDataFramework === 'string') {
78+
currentFramework = headingDataFramework
79+
} else if (Array.isArray(ancestors)) {
80+
const frameworkPanel = ancestors.find((ancestor) =>
81+
isFrameworkPanelAncestor(ancestor as HastElement),
82+
) as HastElement | undefined
83+
84+
if (frameworkPanel) {
85+
const dataFramework = frameworkPanel.properties?.['data-framework']
86+
if (typeof dataFramework === 'string') {
87+
currentFramework = dataFramework
88+
}
89+
}
90+
}
91+
6592
headings.push({
6693
id,
6794
level: Number(node.tagName.substring(1)),
6895
text: toString(node as any).trim(),
96+
framework: currentFramework,
6997
})
7098
})
7199

src/utils/markdown/plugins/transformFrameworkComponent.ts

Lines changed: 62 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,35 @@
11
import { toString } from 'hast-util-to-string'
22
import { visit } from 'unist-util-visit'
33

4-
import { isHeading, normalizeComponentName } from './helpers'
4+
import { normalizeComponentName } from './helpers'
55

66
type HastNode = {
77
type: string
8-
tagName: string
8+
tagName?: string
99
properties?: Record<string, unknown>
1010
children?: HastNode[]
11+
value?: string
1112
}
1213

1314
type FrameworkCodeBlock = {
1415
title: string
1516
code: string
1617
language: string
17-
preNode: HastNode
1818
}
1919

2020
type FrameworkExtraction = {
2121
codeBlocksByFramework: Record<string, FrameworkCodeBlock[]>
2222
contentByFramework: Record<string, HastNode[]>
2323
}
2424

25-
// Helper to extract text from nodes (used for code content)
26-
function extractText(nodes: any[]): string {
27-
let text = ''
28-
for (const node of nodes) {
29-
if (node.type === 'text') {
30-
text += node.value
31-
} else if (node.type === 'element' && node.children) {
32-
text += extractText(node.children)
33-
}
34-
}
35-
return text
36-
}
37-
3825
/**
3926
* Extract code block data (language, title, code) from a <pre> element.
40-
* Extracts title from data-code-title (set by rehypeCodeMeta).
4127
*/
4228
function extractCodeBlockData(preNode: HastNode): {
4329
language: string
4430
title: string
4531
code: string
4632
} | null {
47-
// Find the <code> child
4833
const codeNode = preNode.children?.find(
4934
(c: HastNode) => c.type === 'element' && c.tagName === 'code',
5035
)
@@ -61,6 +46,7 @@ function extractCodeBlockData(preNode: HastNode): {
6146
}
6247
}
6348

49+
// Extract title from data attributes
6450
let title = ''
6551
const props = preNode.properties || {}
6652
if (typeof props['dataCodeTitle'] === 'string') {
@@ -73,57 +59,93 @@ function extractCodeBlockData(preNode: HastNode): {
7359
title = props['data-filename']
7460
}
7561

76-
// Extract code content
62+
// Extract code text
63+
const extractText = (nodes: HastNode[]): string => {
64+
let text = ''
65+
for (const node of nodes) {
66+
if (node.type === 'text' && node.value) {
67+
text += node.value
68+
} else if (node.type === 'element' && node.children) {
69+
text += extractText(node.children)
70+
}
71+
}
72+
return text
73+
}
7774
const code = extractText(codeNode.children || [])
7875

7976
return { language, title, code }
8077
}
8178

82-
/**
83-
* Extract framework-specific content for framework component.
84-
* Groups all content (code blocks and general content) by framework headings.
85-
*/
8679
function extractFrameworkData(node: HastNode): FrameworkExtraction | null {
8780
const children = node.children ?? []
8881
const codeBlocksByFramework: Record<string, FrameworkCodeBlock[]> = {}
8982
const contentByFramework: Record<string, HastNode[]> = {}
9083

91-
let currentFramework: string | null = null
84+
// First pass: find the first H1 to determine the first framework
85+
let firstFramework: string | null = null
86+
for (const child of children) {
87+
if (child.type === 'element' && child.tagName === 'h1') {
88+
firstFramework = toString(child as any).trim().toLowerCase()
89+
break
90+
}
91+
}
92+
93+
// If no H1 found at all, return null
94+
if (!firstFramework) {
95+
return null
96+
}
97+
98+
// Second pass: collect content
99+
let currentFramework: string | null = firstFramework // Start with first framework for content before first H1
100+
101+
// Initialize the first framework
102+
contentByFramework[firstFramework] = []
103+
codeBlocksByFramework[firstFramework] = []
92104

93105
for (const child of children) {
94-
if (isHeading(child)) {
106+
// Check if this is an H1 heading (framework divider)
107+
if (child.type === 'element' && child.tagName === 'h1') {
108+
// Extract framework name from H1 text
95109
currentFramework = toString(child as any)
96110
.trim()
97111
.toLowerCase()
112+
98113
// Initialize arrays for this framework
99114
if (currentFramework && !contentByFramework[currentFramework]) {
100115
contentByFramework[currentFramework] = []
101116
codeBlocksByFramework[currentFramework] = []
102117
}
118+
// Don't include the H1 itself in content - it's just a divider
103119
continue
104120
}
105121

106-
// Skip if no framework heading found yet
107122
if (!currentFramework) continue
108123

109-
// Add all content to contentByFramework
110-
contentByFramework[currentFramework].push(child)
124+
// Create a shallow copy of the node
125+
const contentNode = Object.assign({}, child) as HastNode
126+
127+
// Mark all headings (h2-h6) with framework attribute so they appear in TOC only for this framework
128+
if (
129+
contentNode.type === 'element' &&
130+
contentNode.tagName &&
131+
/^h[2-6]$/.test(contentNode.tagName)
132+
) {
133+
contentNode.properties = (contentNode.properties || {}) as Record<string, unknown>
134+
contentNode.properties['data-framework'] = currentFramework
135+
}
111136

112-
// Look for <pre> elements (code blocks) under current framework
113-
if ((child as any).type === 'element' && (child as any).tagName === 'pre') {
114-
const codeBlockData = extractCodeBlockData(child)
115-
if (!codeBlockData) continue
137+
contentByFramework[currentFramework].push(contentNode)
116138

117-
codeBlocksByFramework[currentFramework].push({
118-
title: codeBlockData.title || 'Untitled',
119-
code: codeBlockData.code,
120-
language: codeBlockData.language,
121-
preNode: child,
122-
})
139+
// Extract code blocks for this framework
140+
if (contentNode.type === 'element' && contentNode.tagName === 'pre') {
141+
const codeBlockData = extractCodeBlockData(contentNode)
142+
if (codeBlockData) {
143+
codeBlocksByFramework[currentFramework].push(codeBlockData)
144+
}
123145
}
124146
}
125147

126-
// Return null only if no frameworks found at all
148+
// Return null if no frameworks found
127149
if (Object.keys(contentByFramework).length === 0) {
128150
return null
129151
}
@@ -188,3 +210,4 @@ export const rehypeTransformFrameworkComponents = () => {
188210
})
189211
}
190212
}
213+

src/utils/markdown/processor.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,11 @@ import {
1313
rehypeParseCommentComponents,
1414
rehypeTransformCommentComponents,
1515
rehypeTransformFrameworkComponents,
16+
type MarkdownHeading,
1617
} from '~/utils/markdown/plugins'
1718
import { extractCodeMeta } from '~/utils/markdown/plugins/extractCodeMeta'
1819

19-
export type MarkdownHeading = {
20-
id: string
21-
text: string
22-
level: number
23-
}
20+
export type { MarkdownHeading } from '~/utils/markdown/plugins'
2421

2522
export type MarkdownRenderResult = {
2623
markup: string

0 commit comments

Comments
 (0)