@@ -641,33 +641,35 @@ def _are_on_same_page(self, item1_name: str, item2_name: str) -> bool:
641641 return item1_page == item2_page
642642
643643 def _get_page_for_item (self , full_name : str ) -> str :
644- """Get the page where this item is rendered."""
644+ """Get the page where this item is rendered.
645+
646+ Based on CLI logic: only packages and modules get their own files.
647+ All other items (classes, functions, methods, etc.) are rendered
648+ on their parent module/package page.
649+ """
645650 item = self .get_item (full_name )
646651 if not item :
647652 return full_name
648653
649654 item_type = item ['type' ]
650655 parts = full_name .split ('.' )
651656
652- # Only packages, modules, and classes get their own dedicated pages
653- if item_type in ('package' , 'module' , 'class' ):
657+ # Only packages and modules get their own dedicated pages/files
658+ if item_type in ('package' , 'module' ):
654659 return full_name
655660
656- # Functions, methods, properties, attributes, data are rendered on their parent's page
657- elif item_type in ('function' , 'method' , 'property' , 'attribute' , 'data' ):
658- # Find the parent (class, module, or package)
661+ # All other items (classes, functions, methods, properties, attributes, data)
662+ # are rendered on their parent module/package page
663+ else :
664+ # Find the parent module or package
659665 for i in range (len (parts ) - 1 , 0 , - 1 ):
660666 parent_name = '.' .join (parts [:i ])
661667 parent_item = self .get_item (parent_name )
662- if parent_item and parent_item ['type' ] in ('package' , 'module' , 'class' ):
668+ if parent_item and parent_item ['type' ] in ('package' , 'module' ):
663669 return parent_name
664670
665- # Fallback - shouldn't happen
666- return full_name
667-
668- else :
669- # Unknown type, assume it gets its own page
670- return full_name
671+ # Fallback - shouldn't happen, but return the root module
672+ return parts [0 ] if parts else full_name
671673
672674 def _get_cross_reference_link (self , target_name : str , display_name : str = None , current_page : str = None ) -> str :
673675 """Generate cross-reference link to another documented object."""
@@ -706,23 +708,9 @@ def _format_code_block_with_links(self, code: str, language: str = "python") ->
706708 import re
707709 word_pattern = r'\b' + re .escape (short_name ) + r'\b'
708710 if re .search (word_pattern , code ) and item ['type' ] in ('class' , 'function' , 'method' ):
709- # Determine the correct page and anchor
710- if item ['type' ] == 'method' :
711- # Methods appear on their class page, but we need the module page slug for the current context
712- class_parts = full_name .split ('.' )
713- if len (class_parts ) >= 3 : # module.Class.method
714- module_name = '.' .join (class_parts [:- 2 ]) # Just the module
715- page_slug = self ._generate_slug (module_name )
716- else :
717- page_slug = self ._generate_slug ('.' .join (class_parts [:- 1 ]))
718- else :
719- # Classes and functions link to their own pages
720- if item ['type' ] in ('class' , 'function' ):
721- page_slug = self ._generate_slug (full_name )
722- else :
723- parent_name = '.' .join (full_name .split ('.' )[:- 1 ]) if '.' in full_name else full_name
724- page_slug = self ._generate_slug (parent_name )
725-
711+ # Use the corrected page mapping logic
712+ page_name = self ._get_page_for_item (full_name )
713+ page_slug = self ._generate_slug (page_name )
726714 anchor_id = self ._generate_anchor_id (full_name )
727715 links [short_name ] = f"{ page_slug } #{ anchor_id } "
728716
@@ -757,6 +745,51 @@ def replace_ref(match):
757745
758746 return re .sub (pattern , replace_ref , text )
759747
748+ def validate_all_links (self , output_dir : str = None ) -> dict [str , list [str ]]:
749+ """Validate all generated links and return any issues found.
750+
751+ Fast lightweight validation focusing on core link integrity.
752+
753+ Returns:
754+ Dict with 'errors' and 'warnings' keys containing lists of issues.
755+ """
756+ issues = {"errors" : [], "warnings" : []}
757+
758+ # Sample a few items to validate the core logic works
759+ sample_items = []
760+ for item_type in ("package" , "module" , "class" , "function" ):
761+ type_items = list (self ._db .get_by_type (item_type ))
762+ if type_items :
763+ sample_items .append (type_items [0 ]) # Just take first item of each type
764+
765+ for item in sample_items :
766+ full_name = item ["full_name" ]
767+
768+ # Validate that we can determine the correct page for this item
769+ try :
770+ page_name = self ._get_page_for_item (full_name )
771+ anchor_id = self ._generate_anchor_id (full_name )
772+
773+ if not anchor_id :
774+ issues ["errors" ].append (f"Empty anchor ID generated for { full_name } " )
775+
776+ # Test cross-reference link generation
777+ test_link = self ._get_cross_reference_link (full_name , None , "test.module" )
778+ if not test_link or test_link == full_name :
779+ issues ["warnings" ].append (f"Link generation may have issues for { full_name } " )
780+
781+ except Exception as e :
782+ issues ["errors" ].append (f"Error processing { full_name } : { e } " )
783+
784+ # Quick check: verify some common patterns
785+ packages = list (self ._db .get_by_type ("package" ))
786+ modules = list (self ._db .get_by_type ("module" ))
787+
788+ if not packages and not modules :
789+ issues ["errors" ].append ("No packages or modules found - this seems wrong" )
790+
791+ return issues
792+
760793 def generate_navigation_yaml (self ) -> str :
761794 """Generate navigation YAML for Fern docs.yml following sphinx autodoc2 toctree logic."""
762795 import yaml
0 commit comments