@@ -37,6 +37,7 @@ fn build_engine() -> Result<HandlebarsTemplateEngine<'static>, DocsError> {
3737 . register_helper ( "md" , Box :: new ( markdown_helper) ) ;
3838 handlebars_helper ! ( inc: |x: i64 | x + 1 ) ;
3939 handlebars_helper ! ( dec: |x: i64 | x - 1 ) ;
40+ handlebars_helper ! ( sub: |a: i64 , b: i64 | a - b) ;
4041 handlebars_helper ! ( concat: |* args| {
4142 args. iter( )
4243 . map( |v| v. as_str( ) . unwrap_or( "" ) )
@@ -54,6 +55,21 @@ fn build_engine() -> Result<HandlebarsTemplateEngine<'static>, DocsError> {
5455 other => other. to_string( ) ,
5556 }
5657 } ) ;
58+ // slug helper for building category URLs
59+ handlebars_helper ! ( slug: |s: str | {
60+ let mut out = String :: new( ) ;
61+ let mut prev_dash = false ;
62+ for ch in s. to_lowercase( ) . chars( ) {
63+ if ch. is_ascii_alphanumeric( ) {
64+ out. push( ch) ;
65+ prev_dash = false ;
66+ } else if !prev_dash {
67+ out. push( '-' ) ;
68+ prev_dash = true ;
69+ }
70+ }
71+ out. trim_matches( '-' ) . to_string( )
72+ } ) ;
5773 engine
5874 . registry ( )
5975 . register_helper ( "display" , Box :: new ( display) ) ;
@@ -62,13 +78,32 @@ fn build_engine() -> Result<HandlebarsTemplateEngine<'static>, DocsError> {
6278 . register_helper ( "concat" , Box :: new ( concat) ) ;
6379 engine. registry ( ) . register_helper ( "inc" , Box :: new ( inc) ) ;
6480 engine. registry ( ) . register_helper ( "dec" , Box :: new ( dec) ) ;
81+ engine. registry ( ) . register_helper ( "sub" , Box :: new ( sub) ) ;
82+ engine. registry ( ) . register_helper ( "slug" , Box :: new ( slug) ) ;
6583 engine
6684 . registry ( )
6785 . register_embed_templates :: < Templates > ( )
6886 . map_err ( |e| DocsError :: Template ( Box :: new ( e) ) ) ?;
6987 Ok ( engine)
7088}
7189
90+ // Simple slugify for route paths
91+ fn slugify < S : AsRef < str > > ( s : S ) -> String {
92+ let s = s. as_ref ( ) . to_lowercase ( ) ;
93+ let mut out = String :: new ( ) ;
94+ let mut prev_dash = false ;
95+ for ch in s. chars ( ) {
96+ if ch. is_ascii_alphanumeric ( ) {
97+ out. push ( ch) ;
98+ prev_dash = false ;
99+ } else if !prev_dash {
100+ out. push ( '-' ) ;
101+ prev_dash = true ;
102+ }
103+ }
104+ out. trim_matches ( '-' ) . to_string ( )
105+ }
106+
72107pub fn generate_docs (
73108 data : & RepositoryData ,
74109 output : String ,
@@ -101,10 +136,83 @@ pub fn generate_docs(
101136 "luna" . to_string ( ) ,
102137 json ! ( { "name" : env!( "CARGO_PKG_NAME" ) , "version" : env!( "CARGO_PKG_VERSION" ) } ) ,
103138 ) ;
139+
140+ // Build categories list and summaries
141+ let mut all_categories: Vec < String > = if !data. info . categories . is_empty ( ) {
142+ data. info . categories . clone ( )
143+ } else {
144+ let mut set = std:: collections:: BTreeSet :: new ( ) ;
145+ for a in & data. assets {
146+ for c in & a. categories {
147+ set. insert ( c. clone ( ) ) ;
148+ }
149+ }
150+ set. into_iter ( ) . collect ( )
151+ } ;
152+ // Ensure stable order
153+ all_categories. sort ( ) ;
154+
155+ let mut categories_summaries: Vec < Map < String , Value > > = Vec :: new ( ) ;
156+ for name in & all_categories {
157+ let count = data
158+ . assets
159+ . iter ( )
160+ . filter ( |a| a. categories . contains ( name) )
161+ . count ( ) ;
162+ let mut m = Map :: new ( ) ;
163+ m. insert ( "name" . to_string ( ) , Value :: String ( name. clone ( ) ) ) ;
164+ m. insert ( "count" . to_string ( ) , json ! ( count) ) ;
165+ categories_summaries. push ( m) ;
166+ }
167+ base_context. insert ( "categories" . to_string ( ) , json ! ( all_categories) ) ;
168+
104169 let context = & Value :: Object ( base_context. clone ( ) ) ;
105170 add_route ( & mut router, "index.html" , "templates/index.hbs" , context) ?;
106171 add_route ( & mut router, "search.html" , "templates/search.hbs" , context) ?;
107172
173+ // Categories overview page
174+ {
175+ let mut ctx = base_context. clone ( ) ;
176+ ctx. insert (
177+ "categories_summaries" . to_string ( ) ,
178+ json ! ( categories_summaries) ,
179+ ) ;
180+ let v = & Value :: Object ( ctx) ;
181+ add_route ( & mut router, "categories.html" , "templates/categories.hbs" , v) ?;
182+ }
183+
184+ // Per-category pages (paginated)
185+ for cat in all_categories. iter ( ) {
186+ let assets_for_cat: Vec < _ > = data
187+ . assets
188+ . iter ( )
189+ . filter ( |a| a. categories . contains ( cat) )
190+ . collect ( ) ;
191+ let mut chunks = assets_for_cat. chunks ( page_size) . collect :: < Vec < _ > > ( ) ;
192+ if chunks. is_empty ( ) {
193+ chunks. push ( & [ ] ) ;
194+ }
195+ let page_count = chunks. len ( ) ;
196+ for ( i, chunk) in chunks. into_iter ( ) . enumerate ( ) {
197+ let mut ctx = base_context. clone ( ) ;
198+ let slug = slugify ( cat) ;
199+ // pagination helpers and counts
200+ let assets_count = chunk. len ( ) ;
201+ ctx. insert ( "category" . to_string ( ) , json ! ( { "name" : cat } ) ) ;
202+ ctx. insert ( "assets" . to_string ( ) , json ! ( chunk) ) ;
203+ ctx. insert ( "assets_count" . to_string ( ) , json ! ( assets_count) ) ;
204+ ctx. insert ( "current_page" . to_string ( ) , json ! ( i + 1 ) ) ;
205+ ctx. insert ( "last_page" . to_string ( ) , json ! ( page_count) ) ;
206+ let path = if i == 0 {
207+ format ! ( "categories/{}.html" , slug)
208+ } else {
209+ format ! ( "categories/{}/{}.html" , slug, i)
210+ } ;
211+ let v = & Value :: Object ( ctx) ;
212+ add_route ( & mut router, & path, "templates/category.hbs" , v) ?;
213+ }
214+ }
215+
108216 let asset_pages = data. assets . chunks ( page_size) ;
109217 let page_count = asset_pages. len ( ) ;
110218 for ( i, assets) in asset_pages. enumerate ( ) {
0 commit comments