@@ -9,12 +9,13 @@ pub mod sidebar;
99
1010use rmx:: prelude:: * ;
1111use rmx:: tera:: Tera ;
12- use rustdoc_types:: { Crate , Id , ItemKind , Visibility } ;
12+ use rustdoc_types:: { Crate , Id , ItemEnum , ItemKind , Visibility } ;
1313use std:: collections:: HashMap ;
14+ use std:: hash:: BuildHasher ;
1415use std:: path:: PathBuf ;
1516
1617use crate :: { RenderConfig , ModuleTree , GlobalItemIndex } ;
17- use crate :: types:: { build_module_tree, build_impl_index, ImplIndex } ;
18+ use crate :: types:: { build_module_tree, build_impl_index, get_type_id , ImplIndex } ;
1819
1920/// Where a re-export target's page exists.
2021pub enum ReexportTarget {
@@ -177,30 +178,201 @@ impl<'a> RenderContext<'a> {
177178 Some ( url)
178179 }
179180
181+ /// Pre-resolve an item's `links` field into a URL map.
182+ ///
183+ /// Takes the item's `links` field (mapping link text to item IDs) and resolves
184+ /// each ID to an actual HTML URL. The keys are normalized to strip backtick
185+ /// wrappers since the markdown preprocessor strips them.
186+ pub fn resolve_item_links < S : BuildHasher > (
187+ & self ,
188+ links : & std:: collections:: HashMap < String , Id , S > ,
189+ current_depth : usize ,
190+ ) -> HashMap < String , String > {
191+ let mut resolved = HashMap :: new ( ) ;
192+ for ( text, id) in links {
193+ if let Some ( url) = self . resolve_reexport_url ( id, current_depth) {
194+ // The links field keys may have backtick wrappers (`` `Builder` ``).
195+ // Strip them since preprocess_shortcut_links removes them from URLs.
196+ let clean_key = text. trim_matches ( '`' ) . to_string ( ) ;
197+ resolved. insert ( clean_key, url) ;
198+ }
199+ }
200+ resolved
201+ }
202+
203+ /// Resolve an item ID to a URL, preferring the re-export location.
204+ ///
205+ /// Handles types, modules, methods, and enum variants.
206+ fn resolve_reexport_url ( & self , id : & Id , current_depth : usize ) -> Option < String > {
207+ // First check krate.paths (has types, modules, variants, but not methods).
208+ if let Some ( summary) = self . krate . paths . get ( id) {
209+ // Handle enum variants: link to parent enum page with #variant.Name anchor.
210+ if summary. kind == ItemKind :: Variant {
211+ return self . resolve_variant_url ( & summary. path , summary. crate_id , current_depth) ;
212+ }
213+
214+ if summary. crate_id == 0 {
215+ // Local item. Check if it's re-exported to a public-facing location.
216+ if let Some ( url) = self . find_reexport_url ( id, summary. kind , current_depth) {
217+ return Some ( url) ;
218+ }
219+ // Fall back to definition path.
220+ return self . build_item_url ( & summary. path , summary. kind , current_depth) ;
221+ } else {
222+ // Cross-crate item.
223+ return self . resolve_cross_crate_url ( & summary. path , current_depth) ;
224+ }
225+ }
226+
227+ // Not in paths - check if it's a method/associated item in the index.
228+ if let Some ( item) = self . krate . index . get ( id) {
229+ if matches ! ( item. inner, ItemEnum :: Function ( _) ) {
230+ return self . resolve_method_url ( id, item, current_depth) ;
231+ }
232+ }
233+
234+ None
235+ }
236+
237+ /// Resolve an enum variant to a URL with #variant.Name anchor.
238+ fn resolve_variant_url (
239+ & self ,
240+ variant_path : & [ String ] ,
241+ crate_id : u32 ,
242+ current_depth : usize ,
243+ ) -> Option < String > {
244+ if variant_path. len ( ) < 2 {
245+ return None ;
246+ }
247+ // Path is like ["core", "result", "Result", "Ok"].
248+ // Parent enum path is everything except last segment.
249+ let enum_path = & variant_path[ ..variant_path. len ( ) - 1 ] ;
250+ let variant_name = variant_path. last ( ) ?;
251+
252+ let enum_url = if crate_id == 0 {
253+ self . build_item_url ( enum_path, ItemKind :: Enum , current_depth) ?
254+ } else {
255+ self . resolve_cross_crate_url ( enum_path, current_depth)
256+ . or_else ( || self . build_item_url ( enum_path, ItemKind :: Enum , current_depth) ) ?
257+ } ;
258+
259+ Some ( format ! ( "{}#variant.{}" , enum_url, variant_name) )
260+ }
261+
262+ /// Resolve a method/associated function to a URL with #method.name anchor.
263+ fn resolve_method_url (
264+ & self ,
265+ method_id : & Id ,
266+ method_item : & rustdoc_types:: Item ,
267+ current_depth : usize ,
268+ ) -> Option < String > {
269+ let method_name = method_item. name . as_deref ( ) ?;
270+
271+ // Find the parent type by searching impl blocks.
272+ for ( _impl_id, impl_item) in & self . krate . index {
273+ let ItemEnum :: Impl ( impl_) = & impl_item. inner else {
274+ continue ;
275+ } ;
276+ if !impl_. items . contains ( method_id) {
277+ continue ;
278+ }
279+ // Found the impl containing this method.
280+ // Get the type ID from the impl's for_ type.
281+ if let Some ( type_id) = get_type_id ( & impl_. for_ ) {
282+ if let Some ( type_url) = self . resolve_reexport_url ( type_id, current_depth) {
283+ // Strip any existing fragment.
284+ let base_url = type_url. split ( '#' ) . next ( ) . unwrap_or ( & type_url) ;
285+ return Some ( format ! ( "{}#method.{}" , base_url, method_name) ) ;
286+ }
287+ }
288+ break ;
289+ }
290+
291+ None
292+ }
293+
294+ /// Find a re-export URL for a local item by checking if it appears as a
295+ /// Use target in the module tree.
296+ fn find_reexport_url (
297+ & self ,
298+ target_id : & Id ,
299+ kind : ItemKind ,
300+ current_depth : usize ,
301+ ) -> Option < String > {
302+ // Walk the module tree looking for Use items targeting this ID.
303+ self . find_reexport_in_tree ( & self . module_tree , target_id, kind, current_depth)
304+ }
305+
306+ fn find_reexport_in_tree (
307+ & self ,
308+ tree : & crate :: types:: ModuleTree ,
309+ target_id : & Id ,
310+ kind : ItemKind ,
311+ current_depth : usize ,
312+ ) -> Option < String > {
313+ for item in & tree. items {
314+ if let ItemEnum :: Use ( use_item) = & item. item . inner {
315+ if use_item. id . as_ref ( ) == Some ( target_id) {
316+ return self . build_item_url ( & item. path , kind, current_depth) ;
317+ }
318+ }
319+ }
320+ for sub in & tree. submodules {
321+ if let Some ( url) = self . find_reexport_in_tree ( sub, target_id, kind, current_depth) {
322+ return Some ( url) ;
323+ }
324+ }
325+ None
326+ }
327+
180328 /// Render markdown to HTML.
181329 pub fn render_markdown ( & self , md : & str ) -> String {
182330 markdown:: render_markdown ( md, & self . highlighter )
183331 }
184332
185333 /// Render markdown to HTML with intra-doc link resolution.
186334 pub fn render_markdown_with_links ( & self , md : & str , current_depth : usize ) -> String {
335+ let empty = HashMap :: new ( ) ;
336+ self . render_markdown_with_item_links ( md, current_depth, & empty)
337+ }
338+
339+ /// Render markdown to HTML with intra-doc link resolution and pre-resolved item links.
340+ pub fn render_markdown_with_item_links (
341+ & self ,
342+ md : & str ,
343+ current_depth : usize ,
344+ pre_resolved_links : & HashMap < String , String > ,
345+ ) -> String {
187346 markdown:: render_markdown_with_links (
188347 md,
189348 & self . highlighter ,
190349 self . global_index ,
191350 self . crate_name ( ) ,
192351 current_depth,
352+ pre_resolved_links,
193353 )
194354 }
195355
196356 /// Render a short documentation string (first paragraph only) as inline HTML.
197357 pub fn render_short_doc ( & self , full_docs : & str , current_depth : usize ) -> String {
358+ let empty = HashMap :: new ( ) ;
359+ self . render_short_doc_with_item_links ( full_docs, current_depth, & empty)
360+ }
361+
362+ /// Render a short doc string with pre-resolved item links.
363+ pub fn render_short_doc_with_item_links (
364+ & self ,
365+ full_docs : & str ,
366+ current_depth : usize ,
367+ pre_resolved_links : & HashMap < String , String > ,
368+ ) -> String {
198369 markdown:: render_short_doc (
199370 full_docs,
200371 & self . highlighter ,
201372 self . global_index ,
202373 self . crate_name ( ) ,
203374 current_depth,
375+ pre_resolved_links,
204376 )
205377 }
206378
0 commit comments