@@ -61,6 +61,9 @@ public function generate(array $documentationNodes, string $docsBaseDir): void
6161 $ navIdMap = $ this ->buildNavIdMap ($ processedNodes );
6262 $ usedBy = $ this ->buildUsedByMap ($ processedNodes );
6363
64+ // Build reverse registry for file path -> owner lookup (needed for navigation hierarchy)
65+ $ reverseRegistry = $ this ->buildReverseRegistry ($ registry );
66+
6467 // Build cross-reference maps for bi-directional linking
6568 $ referencedBy = $ this ->buildReferencedByMap ($ processedNodes , $ registry , $ navPathMap , $ navIdMap );
6669
@@ -79,7 +82,7 @@ public function generate(array $documentationNodes, string $docsBaseDir): void
7982 $ this ->generateFiles ($ docTree , $ docsOutputDir );
8083
8184 // Generate navigation structure with title mapping
82- $ navStructure = $ this ->generateNavStructure ($ docTree , '' , $ navPathMap , $ processedNodes );
85+ $ navStructure = $ this ->generateNavStructure ($ docTree , '' , $ navPathMap , $ processedNodes, $ reverseRegistry );
8386 array_unshift ($ navStructure , ['Home ' => 'index.md ' ]);
8487
8588 // Generate config
@@ -398,6 +401,18 @@ private function buildRegistry(array $documentationNodes): array
398401 return $ registry ;
399402 }
400403
404+ private function buildReverseRegistry (array $ registry ): array
405+ {
406+ // Build reverse mapping: file path -> owner
407+ // This allows us to look up which node generated a specific file path
408+ $ reverseRegistry = [];
409+ foreach ($ registry as $ owner => $ filePath ) {
410+ $ reverseRegistry [$ filePath ] = $ owner ;
411+ }
412+
413+ return $ reverseRegistry ;
414+ }
415+
401416 private function buildNavPathMap (array $ documentationNodes ): array
402417 {
403418 $ navPathMap = [];
@@ -1016,7 +1031,7 @@ private function generateFiles(array $tree, string $currentPath): void
10161031 }
10171032 }
10181033
1019- private function generateNavStructure (array $ tree , string $ pathPrefix = '' , array $ navPathMap = [], array $ allNodes = []): array
1034+ private function generateNavStructure (array $ tree , string $ pathPrefix = '' , array $ navPathMap = [], array $ allNodes = [], array $ reverseRegistry = [] ): array
10201035 {
10211036 $ navItems = [];
10221037
@@ -1033,7 +1048,7 @@ private function generateNavStructure(array $tree, string $pathPrefix = '', arra
10331048 $ dirName = ucwords (str_replace (['_ ' , '- ' ], ' ' , $ key ));
10341049 $ navItems [] = [
10351050 'title ' => $ dirName ,
1036- 'content ' => $ this ->generateNavStructure ($ value , $ pathPrefix .$ key .'/ ' , $ navPathMap , $ allNodes ),
1051+ 'content ' => $ this ->generateNavStructure ($ value , $ pathPrefix .$ key .'/ ' , $ navPathMap , $ allNodes, $ reverseRegistry ),
10371052 'type ' => $ this ->getNavItemType ($ dirName ),
10381053 'sortKey ' => strtolower ($ dirName ),
10391054 'isChild ' => false ,
@@ -1042,7 +1057,7 @@ private function generateNavStructure(array $tree, string $pathPrefix = '', arra
10421057 } else {
10431058 // For files, find the display title and node metadata
10441059 $ displayTitle = $ this ->findDisplayTitleForFile ($ filePath , $ allNodes );
1045- $ nodeMetadata = $ this ->findNodeMetadataForFile ($ filePath , $ allNodes );
1060+ $ nodeMetadata = $ this ->findNodeMetadataForFile ($ filePath , $ allNodes, $ reverseRegistry );
10461061
10471062 if ($ displayTitle ) {
10481063 $ title = $ displayTitle ;
@@ -1072,7 +1087,7 @@ private function generateNavStructure(array $tree, string $pathPrefix = '', arra
10721087 }
10731088
10741089 // Sort the nav items with new parent-child logic
1075- usort ($ navItems , function ($ a , $ b ) use ($ allNodes ) {
1090+ usort ($ navItems , function ($ a , $ b ) use ($ allNodes, $ reverseRegistry ) {
10761091 // Apply type priority first: regular -> static -> uncategorised
10771092 if ($ a ['type ' ] !== $ b ['type ' ]) {
10781093 $ typePriority = ['regular ' => 1 , 'static ' => 2 , 'uncategorised ' => 3 ];
@@ -1082,10 +1097,10 @@ private function generateNavStructure(array $tree, string $pathPrefix = '', arra
10821097
10831098 // Within same type, handle parent-child relationships
10841099 // If one is child and the other is parent, parent comes first
1085- if ($ a ['isChild ' ] && ! $ b ['isChild ' ] && $ a ['parentKey ' ] === $ this ->findParentIdentifier ($ b , $ allNodes )) {
1100+ if ($ a ['isChild ' ] && ! $ b ['isChild ' ] && $ a ['parentKey ' ] === $ this ->findParentIdentifier ($ b , $ allNodes, $ reverseRegistry )) {
10861101 return 1 ; // a (child) comes after b (parent)
10871102 }
1088- if ($ b ['isChild ' ] && ! $ a ['isChild ' ] && $ b ['parentKey ' ] === $ this ->findParentIdentifier ($ a , $ allNodes )) {
1103+ if ($ b ['isChild ' ] && ! $ a ['isChild ' ] && $ b ['parentKey ' ] === $ this ->findParentIdentifier ($ a , $ allNodes, $ reverseRegistry )) {
10891104 return -1 ; // a (parent) comes before b (child)
10901105 }
10911106
@@ -1162,7 +1177,7 @@ private function findDisplayTitleForFile(string $filePath, array $allNodes): ?st
11621177 return null ;
11631178 }
11641179
1165- private function findNodeMetadataForFile (string $ filePath , array $ allNodes ): ?array
1180+ private function findNodeMetadataForFile (string $ filePath , array $ allNodes, array $ reverseRegistry = [] ): ?array
11661181 {
11671182 // Normalize function to handle case and space/underscore differences
11681183 $ normalize = (fn ($ path ) => strtolower (str_replace (' ' , '_ ' , $ path )));
@@ -1188,23 +1203,31 @@ private function findNodeMetadataForFile(string $filePath, array $allNodes): ?ar
11881203 return $ node ; // Return the entire node as metadata
11891204 }
11901205 }
1191- } else {
1192- // For PHPDoc content, we could match based on generated paths
1193- // This would require more complex path matching logic
1194- // For now, we'll skip this and handle only static content
1206+ }
1207+ }
1208+
1209+ // For PHPDoc content, use the reverse registry to find the owner
1210+ if (! empty ($ reverseRegistry ) && isset ($ reverseRegistry [$ filePath ])) {
1211+ $ owner = $ reverseRegistry [$ filePath ];
1212+
1213+ // Find the node with this owner
1214+ foreach ($ allNodes as $ node ) {
1215+ if ($ node ['owner ' ] === $ owner ) {
1216+ return $ node ; // Return the entire node as metadata
1217+ }
11951218 }
11961219 }
11971220
11981221 return null ;
11991222 }
12001223
1201- private function findParentIdentifier (array $ navItem , array $ allNodes ): ?string
1224+ private function findParentIdentifier (array $ navItem , array $ allNodes, array $ reverseRegistry = [] ): ?string
12021225 {
12031226 // For navigation items that are files, we need to find their corresponding node
12041227 // and return the parent identifier (navId or owner)
12051228 if (isset ($ navItem ['content ' ]) && is_string ($ navItem ['content ' ])) {
12061229 $ filePath = $ navItem ['content ' ];
1207- $ nodeMetadata = $ this ->findNodeMetadataForFile ($ filePath , $ allNodes );
1230+ $ nodeMetadata = $ this ->findNodeMetadataForFile ($ filePath , $ allNodes, $ reverseRegistry );
12081231
12091232 if ($ nodeMetadata ) {
12101233 return $ nodeMetadata ['navId ' ] ?? $ nodeMetadata ['owner ' ] ?? null ;
@@ -1231,9 +1254,14 @@ private function makeRelativePath(string $path, string $base): string
12311254 // Calculate proper relative path between two locations in the docs tree
12321255 // This handles cross-references between different directory structures
12331256 // using proper ../ notation that MkDocs expects
1234-
1235- $ pathParts = explode ('/ ' , $ path );
1236- $ baseParts = explode ('/ ' , dirname ($ base ));
1257+ //
1258+ // Strip .md extension from both paths before calculating relative path
1259+ // because MkDocs serves each .md file as a directory (e.g., main-process.md -> /main-process/)
1260+ $ path = preg_replace ('/\.md$/ ' , '' , $ path );
1261+ $ base = preg_replace ('/\.md$/ ' , '' , $ base );
1262+
1263+ $ pathParts = explode ('/ ' , (string ) $ path );
1264+ $ baseParts = explode ('/ ' , (string ) $ base );
12371265
12381266 // Remove common path prefix
12391267 while (count ($ pathParts ) > 0 && count ($ baseParts ) > 0 && $ pathParts [0 ] === $ baseParts [0 ]) {
0 commit comments