@@ -54,14 +54,94 @@ private static bool IsIndexPage(string relativePath)
5454 || name . Equals ( "README" , StringComparison . OrdinalIgnoreCase ) ;
5555 }
5656
57+ /// <summary>
58+ /// Builds a hierarchical nav from the pages' directory structure when no nav is
59+ /// authored — mirroring MkDocs' default behaviour. Each folder becomes a section
60+ /// (titleised from its name), an <c>index.md</c>/<c>README.md</c> becomes that
61+ /// section's landing page, and entries are ordered alphabetically with index pages
62+ /// first. A flat list (the previous behaviour) dumped every page into one level,
63+ /// which for large sites overflowed the header nav.
64+ /// </summary>
5765 private static IReadOnlyList < NavNode > AutoNav ( IReadOnlyList < Page > pages )
5866 {
59- return pages
60- . OrderBy ( p => p . RelativePath , StringComparer . OrdinalIgnoreCase )
61- . Select ( p => new NavNode { Title = p . Title , Page = p } )
67+ var root = new Dir ( "" ) ;
68+ foreach ( var page in pages )
69+ {
70+ var parts = page . RelativePath . Replace ( '\\ ' , '/' ) . Split ( '/' , StringSplitOptions . RemoveEmptyEntries ) ;
71+ var dir = root ;
72+ for ( var i = 0 ; i < parts . Length - 1 ; i ++ )
73+ dir = dir . Sub ( parts [ i ] ) ;
74+ dir . Pages . Add ( page ) ;
75+ }
76+
77+ var nodes = BuildLevel ( root ) . ToList ( ) ;
78+
79+ // A top-level index/README page becomes a plain landing link (e.g. "Home"),
80+ // not a section, so it isn't swallowed by BuildLevel's index handling.
81+ var home = root . Pages . FirstOrDefault ( p => IsIndexPage ( p . RelativePath ) ) ;
82+ if ( home is not null )
83+ nodes . Insert ( 0 , new NavNode { Title = home . Title . Length > 0 ? home . Title : "Home" , Page = home , Icon = PageIcon ( home ) } ) ;
84+
85+ return nodes ;
86+ }
87+
88+ /// <summary>Convert a directory's sub-folders and (non-index) pages into nav nodes,
89+ /// interleaved and ordered alphabetically by name.</summary>
90+ private static List < NavNode > BuildLevel ( Dir dir )
91+ {
92+ var entries = new List < ( string Key , NavNode Node ) > ( ) ;
93+
94+ foreach ( var sub in dir . Subs . Values )
95+ {
96+ var children = BuildLevel ( sub ) ;
97+ var index = sub . Pages . FirstOrDefault ( p => IsIndexPage ( p . RelativePath ) ) ;
98+ var section = new NavNode
99+ {
100+ Title = Titleize ( sub . Name ) ,
101+ Children = children ,
102+ SectionIndex = index ,
103+ Icon = index is not null ? PageIcon ( index ) : null ,
104+ } ;
105+ entries . Add ( ( sub . Name , section ) ) ;
106+ }
107+
108+ foreach ( var page in dir . Pages . Where ( p => ! IsIndexPage ( p . RelativePath ) ) )
109+ {
110+ var key = Path . GetFileNameWithoutExtension ( page . RelativePath ) ;
111+ entries . Add ( ( key , new NavNode { Title = page . Title , Page = page , Icon = PageIcon ( page ) } ) ) ;
112+ }
113+
114+ return entries
115+ . OrderBy ( e => e . Key , StringComparer . OrdinalIgnoreCase )
116+ . Select ( e => e . Node )
62117 . ToList ( ) ;
63118 }
64119
120+ /// <summary>Turn a folder/file slug into a human-readable section title
121+ /// (e.g. "account-management" -> "Account Management").</summary>
122+ private static string Titleize ( string slug )
123+ {
124+ var words = slug . Replace ( '-' , ' ' ) . Replace ( '_' , ' ' ) . Split ( ' ' , StringSplitOptions . RemoveEmptyEntries ) ;
125+ for ( var i = 0 ; i < words . Length ; i ++ )
126+ words [ i ] = char . ToUpperInvariant ( words [ i ] [ 0 ] ) + words [ i ] [ 1 ..] ;
127+ return words . Length > 0 ? string . Join ( ' ' , words ) : slug ;
128+ }
129+
130+ /// <summary>A node in the directory tree used to build the auto-nav.</summary>
131+ private sealed class Dir ( string name )
132+ {
133+ public string Name { get ; } = name ;
134+ public SortedDictionary < string , Dir > Subs { get ; } = new ( StringComparer . OrdinalIgnoreCase ) ;
135+ public List < Page > Pages { get ; } = [ ] ;
136+
137+ public Dir Sub ( string childName )
138+ {
139+ if ( ! Subs . TryGetValue ( childName , out var d ) )
140+ Subs [ childName ] = d = new Dir ( childName ) ;
141+ return d ;
142+ }
143+ }
144+
65145 /// <summary>Depth-first flatten of the nav tree to the linear order of real pages.</summary>
66146 public static List < Page > Flatten ( IReadOnlyList < NavNode > nodes )
67147 {
0 commit comments