11import { ChevronRight , FileText } from "lucide-react" ;
2- import { isValidElement , useEffect , useState } from "react" ;
2+ import { isValidElement , useEffect , useMemo , useState } from "react" ;
33import ReactMarkdown from "react-markdown" ;
44import { Link , useParams } from "react-router-dom" ;
55import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" ;
@@ -13,156 +13,191 @@ import { Card } from "../components/ui/card";
1313import { useTheme } from "../lib/ThemeProvider" ;
1414
1515type DocEntry = {
16- id : string ;
16+ route : string ;
17+ relativePath : string ;
1718 title : string ;
18- path : string ;
1919 draft ?: boolean ;
2020} ;
2121
22- function hrefToDocsRoute ( href : string ) {
23- const normalized = href . replace ( / ^ \. \/ / , "" ) ;
22+ type Frontmatter = Record < string , string > ;
2423
25- const isSchemasPath =
26- normalized . startsWith ( "/docs/schemas/" ) ||
27- normalized . startsWith ( "schemas/" ) ;
28- const isDraftPath =
29- normalized . includes ( "/schemas/draft/" ) ||
30- normalized . startsWith ( "draft/" ) ||
31- normalized . startsWith ( "/docs/schemas/draft/" ) ||
32- normalized . startsWith ( "schemas/draft/" ) ;
24+ function parseFrontmatter ( markdown : string ) : {
25+ frontmatter : Frontmatter ;
26+ body : string ;
27+ } {
28+ if ( ! markdown . startsWith ( "---\n" ) ) return { frontmatter : { } , body : markdown } ;
29+ const endIndex = markdown . indexOf ( "\n---\n" , 4 ) ;
30+ if ( endIndex === - 1 ) return { frontmatter : { } , body : markdown } ;
31+
32+ const raw = markdown . slice ( 4 , endIndex ) ;
33+ const body = markdown . slice ( endIndex + "\n---\n" . length ) ;
34+ const frontmatter : Frontmatter = { } ;
35+
36+ for ( const line of raw . split ( "\n" ) ) {
37+ const trimmed = line . trim ( ) ;
38+ if ( ! trimmed ) continue ;
39+ const match = / ^ ( [ A - Z a - z 0 - 9 _ - ] + ) \s * : \s * ( .* ) $ / . exec ( trimmed ) ;
40+ if ( ! match ) continue ;
41+ const key = match [ 1 ] ?? "" ;
42+ if ( ! key ) continue ;
43+ const value = ( match [ 2 ] ?? "" ) . replace ( / ^ [ " ' ] | [ " ' ] $ / g, "" ) . trim ( ) ;
44+ frontmatter [ key ] = value ;
45+ }
46+
47+ return { frontmatter, body } ;
48+ }
49+
50+ function extractTitleFromMarkdown ( markdown : string ) : string | null {
51+ const lines = markdown . split ( "\n" ) ;
52+ for ( const line of lines ) {
53+ const trimmed = line . trim ( ) ;
54+ if ( ! trimmed ) continue ;
55+ if ( trimmed . startsWith ( "# " ) ) return trimmed . replace ( / ^ # \s + / , "" ) . trim ( ) ;
56+ if ( trimmed . startsWith ( "## " ) ) return trimmed . replace ( / ^ # # \s + / , "" ) . trim ( ) ;
57+ }
58+ return null ;
59+ }
60+
61+ function posixJoin ( baseDir : string , hrefPath : string ) {
62+ const parts = [ ...baseDir . split ( "/" ) , ...hrefPath . split ( "/" ) ] . filter ( Boolean ) ;
63+ const out : string [ ] = [ ] ;
64+ for ( const part of parts ) {
65+ if ( part === "." ) continue ;
66+ if ( part === ".." ) out . pop ( ) ;
67+ else out . push ( part ) ;
68+ }
69+ return out . join ( "/" ) ;
70+ }
71+
72+ const RAW_DOCS = import . meta. glob ( "../../../docs/schemas/**/*.md" , {
73+ query : "?raw" ,
74+ import : "default" ,
75+ eager : true ,
76+ } ) as Record < string , string > ;
77+
78+ function makeDocIndex ( ) : DocEntry [ ] {
79+ const entries : DocEntry [ ] = [ ] ;
80+
81+ for ( const [ sourcePath , raw ] of Object . entries ( RAW_DOCS ) ) {
82+ const marker = "/docs/schemas/" ;
83+ const idx = sourcePath . lastIndexOf ( marker ) ;
84+ if ( idx === - 1 ) continue ;
85+ const relativePath = sourcePath . slice ( idx + marker . length ) ;
86+ const { frontmatter, body } = parseFrontmatter ( raw ) ;
87+ const draft = relativePath . startsWith ( "draft/" ) ;
88+
89+ const frontmatterTitle = frontmatter . title ?. trim ( ) ;
90+ const headingTitle = extractTitleFromMarkdown ( body ) ?. trim ( ) ;
91+ const fallbackTitle = relativePath
92+ . replace ( / \. m d $ / , "" )
93+ . split ( "/" )
94+ . pop ( )
95+ ?. replace ( / _ / g, " " ) ;
96+ const title =
97+ frontmatterTitle || headingTitle || fallbackTitle || "Documentation" ;
98+
99+ const route = `/docs/schemas/${ relativePath . replace ( / \. m d $ / , "" ) } ` ;
100+ entries . push ( { route, relativePath, title, draft } ) ;
101+ }
102+
103+ entries . sort ( ( a , b ) => {
104+ const aIsReadme = a . relativePath . toLowerCase ( ) === "readme.md" ;
105+ const bIsReadme = b . relativePath . toLowerCase ( ) === "readme.md" ;
106+ if ( aIsReadme !== bIsReadme ) return aIsReadme ? - 1 : 1 ;
107+ if ( a . draft !== b . draft ) return a . draft ? 1 : - 1 ;
108+ return a . relativePath . localeCompare ( b . relativePath ) ;
109+ } ) ;
110+
111+ return entries ;
112+ }
113+
114+ const DOC_INDEX = makeDocIndex ( ) ;
115+ const DOC_BY_ROUTE = new Map ( DOC_INDEX . map ( ( d ) => [ d . route , d ] ) ) ;
116+ const DEFAULT_ROUTE = "/docs/schemas/README" ;
117+
118+ function canonicalRouteFromSplat ( splat ?: string ) {
119+ const cleaned = ( splat || "" ) . replace ( / ^ \/ + / , "" ) . replace ( / \/ + $ / , "" ) ;
120+ if ( ! cleaned ) return DEFAULT_ROUTE ;
121+ if ( cleaned === "schemas" ) return DEFAULT_ROUTE ;
122+ if ( cleaned === "schemas/README" ) return DEFAULT_ROUTE ;
123+
124+ if ( cleaned . startsWith ( "schemas/" ) ) {
125+ const normalized = cleaned . replace ( / \. m d $ / , "" ) ;
126+ return `/docs/${ normalized } ` ;
127+ }
128+
129+ const old = cleaned . replace ( / \. m d $ / , "" ) ;
130+ if ( old . startsWith ( "draft_" ) )
131+ return `/docs/schemas/draft/${ old . replace ( / ^ d r a f t _ / , "" ) } ` ;
132+ return `/docs/schemas/${ old } ` ;
133+ }
134+
135+ function hrefToDocsRoute ( href : string , currentDocRelativePath : string ) {
136+ const [ pathPart , hashPart ] = href . split ( "#" , 2 ) ;
137+ const hash = hashPart ? `#${ hashPart } ` : "" ;
138+ const normalized = ( pathPart || "" ) . replace ( / ^ \. \/ / , "" ) ;
33139
34140 const filenameMatch = / ( [ ^ / ] + ) \. m d $ / . exec ( normalized ) ;
35141 if ( ! filenameMatch ) return href ;
36142
37- const docBaseName = filenameMatch [ 1 ] ;
38- const docId = isDraftPath ? `draft_${ docBaseName } ` : docBaseName ;
143+ if ( normalized . startsWith ( "/docs/" ) ) {
144+ const stripped = normalized . replace ( / ^ \/ d o c s \/ / , "" ) . replace ( / \. m d $ / , "" ) ;
145+ if ( stripped . startsWith ( "schemas/" ) ) return `/docs/${ stripped } ${ hash } ` ;
146+ if ( stripped . startsWith ( "draft_" ) )
147+ return `/docs/schemas/draft/${ stripped . replace ( / ^ d r a f t _ / , "" ) } ${ hash } ` ;
148+ return `/docs/schemas/${ stripped } ${ hash } ` ;
149+ }
39150
40- if ( isSchemasPath || isDraftPath ) return `/docs/${ docId } ` ;
41- return `/docs/${ docId } ` ;
42- }
151+ const underSchemas = normalized . startsWith ( "schemas/" ) ;
152+ const schemaRelative = underSchemas
153+ ? normalized . replace ( / ^ s c h e m a s \/ / , "" )
154+ : normalized ;
155+ const baseDir = currentDocRelativePath . split ( "/" ) . slice ( 0 , - 1 ) . join ( "/" ) ;
156+ const resolvedRelative = schemaRelative . startsWith ( "/" )
157+ ? schemaRelative . replace ( / ^ \/ / , "" )
158+ : posixJoin ( baseDir , schemaRelative ) ;
43159
44- const DOCS : DocEntry [ ] = [
45- { id : "README" , title : "Overview" , path : "/docs/schemas/README.md" } ,
46- { id : "locate" , title : "Locate" , path : "/docs/schemas/locate.md" } ,
47- { id : "symbol" , title : "Symbol" , path : "/docs/schemas/symbol.md" } ,
48- {
49- id : "symbol_outline" ,
50- title : "Symbol Outline" ,
51- path : "/docs/schemas/symbol_outline.md" ,
52- } ,
53- {
54- id : "definition" ,
55- title : "Definition" ,
56- path : "/docs/schemas/definition.md" ,
57- } ,
58- { id : "reference" , title : "Reference" , path : "/docs/schemas/reference.md" } ,
59- {
60- id : "implementation" ,
61- title : "Implementation" ,
62- path : "/docs/schemas/implementation.md" ,
63- } ,
64- {
65- id : "call_hierarchy" ,
66- title : "Call Hierarchy" ,
67- path : "/docs/schemas/call_hierarchy.md" ,
68- } ,
69- {
70- id : "type_hierarchy" ,
71- title : "Type Hierarchy" ,
72- path : "/docs/schemas/type_hierarchy.md" ,
73- } ,
74- { id : "workspace" , title : "Workspace" , path : "/docs/schemas/workspace.md" } ,
75- {
76- id : "completion" ,
77- title : "Completion" ,
78- path : "/docs/schemas/completion.md" ,
79- } ,
80- {
81- id : "diagnostics" ,
82- title : "Diagnostics" ,
83- path : "/docs/schemas/diagnostics.md" ,
84- } ,
85- { id : "rename" , title : "Rename" , path : "/docs/schemas/rename.md" } ,
86- {
87- id : "inlay_hints" ,
88- title : "Inlay Hints" ,
89- path : "/docs/schemas/inlay_hints.md" ,
90- } ,
91- {
92- id : "draft_implementation" ,
93- title : "Implementation" ,
94- path : "/docs/schemas/draft/implementation.md" ,
95- draft : true ,
96- } ,
97- {
98- id : "draft_call_hierarchy" ,
99- title : "Call Hierarchy" ,
100- path : "/docs/schemas/draft/call_hierarchy.md" ,
101- draft : true ,
102- } ,
103- {
104- id : "draft_type_hierarchy" ,
105- title : "Type Hierarchy" ,
106- path : "/docs/schemas/draft/type_hierarchy.md" ,
107- draft : true ,
108- } ,
109- {
110- id : "draft_diagnostics" ,
111- title : "Diagnostics" ,
112- path : "/docs/schemas/draft/diagnostics.md" ,
113- draft : true ,
114- } ,
115- {
116- id : "draft_rename" ,
117- title : "Rename" ,
118- path : "/docs/schemas/draft/rename.md" ,
119- draft : true ,
120- } ,
121- {
122- id : "draft_inlay_hints" ,
123- title : "Inlay Hints" ,
124- path : "/docs/schemas/draft/inlay_hints.md" ,
125- draft : true ,
126- } ,
127- ] ;
160+ const withoutExt = resolvedRelative . replace ( / \. m d $ / , "" ) ;
161+ return `/docs/schemas/${ withoutExt } ${ hash } ` ;
162+ }
128163
129164export default function DocsPage ( ) {
130- const { docId } = useParams ( ) ;
165+ const params = useParams ( ) ;
166+ const splat = params [ "*" ] ;
131167 const [ content , setContent ] = useState < string > ( "" ) ;
132168 const [ loading , setLoading ] = useState ( true ) ;
133169 const { resolvedTheme } = useTheme ( ) ;
134170
135- const cleanDocId = docId ?. replace ( / \. m d $ / , "" ) ;
136- const currentDoc = DOCS . find ( ( d ) => d . id === cleanDocId ) ?? DOCS [ 0 ] ;
171+ const docs = useMemo ( ( ) => DOC_INDEX , [ ] ) ;
172+ const canonicalRoute = useMemo ( ( ) => canonicalRouteFromSplat ( splat ) , [ splat ] ) ;
173+ const currentDoc =
174+ DOC_BY_ROUTE . get ( canonicalRoute ) ?? DOC_BY_ROUTE . get ( DEFAULT_ROUTE ) ;
137175
138176 useEffect ( ( ) => {
139177 const titleSuffix = currentDoc ?. draft ? " (draft)" : "" ;
140178 document . title = `${
141179 currentDoc ?. title ?? "Documentation"
142180 } ${ titleSuffix } | LSAP`;
143- } , [ currentDoc ?. id ] ) ;
181+ } , [ currentDoc ?. route , currentDoc ?. title , currentDoc ?. draft ] ) ;
144182
145183 useEffect ( ( ) => {
146184 if ( ! currentDoc ) return ;
147-
148185 setLoading ( true ) ;
149- const baseUrl = ( import . meta. env . BASE_URL || "/" ) . replace ( / \/ $ / , "" ) ;
150- fetch ( `${ baseUrl } ${ currentDoc . path } ` )
151- . then ( ( res ) => {
152- if ( ! res . ok ) throw new Error ( "Failed to fetch" ) ;
153- return res . text ( ) ;
154- } )
155- . then ( ( text ) => {
156- setContent ( text ) ;
157- setLoading ( false ) ;
158- } )
159- . catch ( ( ) => {
160- setContent (
161- "# Documentation Not Found\n\nThe requested documentation could not be loaded."
162- ) ;
163- setLoading ( false ) ;
164- } ) ;
165- } , [ currentDoc ?. path ] ) ;
186+ const sourcePath = Object . keys ( RAW_DOCS ) . find ( ( p ) =>
187+ p . endsWith ( `/docs/schemas/${ currentDoc . relativePath } ` )
188+ ) ;
189+ const raw = sourcePath ? RAW_DOCS [ sourcePath ] ?? "" : "" ;
190+ const { body } = parseFrontmatter ( raw ) ;
191+ if ( ! body ) {
192+ setContent (
193+ "# Documentation Not Found\n\nThe requested documentation could not be loaded."
194+ ) ;
195+ setLoading ( false ) ;
196+ return ;
197+ }
198+ setContent ( body ) ;
199+ setLoading ( false ) ;
200+ } , [ currentDoc ?. route , currentDoc ?. relativePath ] ) ;
166201
167202 if ( ! currentDoc ) return null ;
168203
@@ -172,21 +207,20 @@ export default function DocsPage() {
172207
173208 < div className = "container max-w-7xl mx-auto px-6 lg:px-8 py-8" >
174209 < div className = "grid grid-cols-1 lg:grid-cols-4 gap-8" >
175- { /* Sidebar */ }
176210 < aside className = "lg:col-span-1" >
177211 < Card className = "p-4 sticky top-24" >
178212 < h2 className = "font-mono text-sm font-medium mb-4 text-foreground" >
179213 Documentation
180214 </ h2 >
181215 < nav className = "space-y-1" >
182- { DOCS . map ( ( doc ) => (
216+ { docs . map ( ( doc ) => (
183217 < Link
184- key = { doc . id }
185- to = { `/docs/ ${ doc . id } ` }
218+ key = { doc . route }
219+ to = { doc . route }
186220 className = { `
187221 flex items-center gap-2 px-3 py-2 rounded-sm text-sm transition-colors
188222 ${
189- currentDoc . id === doc . id
223+ currentDoc . route === doc . route
190224 ? "bg-primary/10 text-primary font-medium"
191225 : "text-muted-foreground hover:text-foreground hover:bg-muted"
192226 }
@@ -203,7 +237,7 @@ export default function DocsPage() {
203237 draft
204238 </ span >
205239 ) }
206- { currentDoc . id === doc . id && (
240+ { currentDoc . route === doc . route && (
207241 < ChevronRight className = "h-3.5 w-3.5 ml-auto" />
208242 ) }
209243 </ Link >
@@ -212,7 +246,6 @@ export default function DocsPage() {
212246 </ Card >
213247 </ aside >
214248
215- { /* Main Content */ }
216249 < main className = "lg:col-span-3" >
217250 < Card className = "p-8 lg:p-12" >
218251 { loading ? (
@@ -368,9 +401,10 @@ export default function DocsPage() {
368401 ! href . startsWith ( "//" ) &&
369402 ! href . startsWith ( "#" ) ;
370403 if ( isInternal ) {
371- const to = href . startsWith ( "/" )
372- ? hrefToDocsRoute ( href )
373- : hrefToDocsRoute ( href ) ;
404+ const to = hrefToDocsRoute (
405+ href ,
406+ currentDoc . relativePath
407+ ) ;
374408 return (
375409 < Link
376410 to = { to }
0 commit comments