55use Illuminate \Filesystem \Filesystem ;
66use Symfony \Component \Yaml \Yaml ;
77
8+ /**
9+ * @functional
10+ * Generates MkDocs documentation from extracted functional documentation.
11+ *
12+ * @nav Main Section / Generator / MkDocs Generator
13+ *
14+ * @uses \Illuminate\Filesystem\Filesystem
15+ */
816class MkDocsGenerator
917{
1018 private array $ validationWarnings = [];
@@ -67,7 +75,7 @@ public function generate(array $documentationNodes, string $docsBaseDir): void
6775 $ referencedBy = $ this ->buildReferencedByMap ($ processedNodes , $ registry , $ navPathMap , $ navIdMap );
6876
6977 // Generate the document tree
70- $ docTree = $ this ->generateDocTree ($ processedNodes , $ registry , $ navPathMap , $ navIdMap , $ usedBy , $ referencedBy );
78+ $ docTree = $ this ->generateDocTree ($ processedNodes , $ registry , $ navPathMap , $ navIdMap , $ usedBy , $ referencedBy, $ processedNodes );
7179
7280 // Prepare output directory
7381 $ this ->filesystem ->deleteDirectory ($ docsOutputDir );
@@ -495,7 +503,7 @@ private function buildReferencedByMap(array $documentationNodes, array $registry
495503 return $ referencedBy ;
496504 }
497505
498- private function generateDocTree (array $ documentationNodes , array $ registry , array $ navPathMap , array $ navIdMap , array $ usedBy , array $ referencedBy ): array
506+ private function generateDocTree (array $ documentationNodes , array $ registry , array $ navPathMap , array $ navIdMap , array $ usedBy , array $ referencedBy, array $ allNodes ): array
499507 {
500508 $ docTree = [];
501509 $ pathRegistry = [];
@@ -531,7 +539,7 @@ private function generateDocTree(array $documentationNodes, array $registry, arr
531539 }
532540
533541 // Generate the markdown content
534- $ markdownContent = $ this ->generateMarkdownContent ($ node , $ pageTitle , $ registry , $ navPathMap , $ navIdMap , $ usedBy , $ referencedBy );
542+ $ markdownContent = $ this ->generateMarkdownContent ($ node , $ pageTitle , $ registry , $ navPathMap , $ navIdMap , $ usedBy , $ referencedBy, $ allNodes );
535543
536544 // Build the path in the document tree
537545 $ docTree = $ this ->addToDocTree ($ docTree , $ pathSegments , $ originalPageTitle , $ pageFileName , $ markdownContent );
@@ -623,11 +631,11 @@ private function setInNestedArray(array $array, array $path, string $originalPag
623631 return $ array ;
624632 }
625633
626- private function generateMarkdownContent (array $ node , string $ pageTitle , array $ registry , array $ navPathMap , array $ navIdMap , array $ usedBy , array $ referencedBy ): string
634+ private function generateMarkdownContent (array $ node , string $ pageTitle , array $ registry , array $ navPathMap , array $ navIdMap , array $ usedBy , array $ referencedBy, array $ allNodes ): string
627635 {
628636 // Handle static content nodes differently
629637 if (isset ($ node ['type ' ]) && $ node ['type ' ] === 'static_content ' ) {
630- return $ this ->generateStaticContent ($ node , $ pageTitle , $ registry , $ navPathMap , $ navIdMap , $ usedBy , $ referencedBy );
638+ return $ this ->generateStaticContent ($ node , $ pageTitle , $ registry , $ navPathMap , $ navIdMap , $ usedBy , $ referencedBy, $ allNodes );
631639 }
632640
633641 $ markdownContent = "# {$ pageTitle }\n\n" ;
@@ -639,7 +647,7 @@ private function generateMarkdownContent(array $node, string $pageTitle, array $
639647
640648 // Add "Building Blocks Used" section
641649 if (! empty ($ node ['uses ' ])) {
642- $ markdownContent .= $ this ->generateUsedComponentsSection ($ node , $ registry , $ navPathMap );
650+ $ markdownContent .= $ this ->generateUsedComponentsSection ($ node , $ registry , $ navPathMap, $ allNodes );
643651 }
644652
645653 // Add "Used By Building Blocks" section
@@ -661,7 +669,7 @@ private function generateMarkdownContent(array $node, string $pageTitle, array $
661669 return $ markdownContent ;
662670 }
663671
664- private function generateStaticContent (array $ node , string $ pageTitle , array $ registry = [], array $ navPathMap = [], array $ navIdMap = [], array $ usedBy = [], array $ referencedBy = []): string
672+ private function generateStaticContent (array $ node , string $ pageTitle , array $ registry = [], array $ navPathMap = [], array $ navIdMap = [], array $ usedBy = [], array $ referencedBy = [], array $ allNodes = [] ): string
665673 {
666674 // For static content, we don't add the title since it might already be in the content
667675 // We also don't add the source subtitle
@@ -677,7 +685,7 @@ private function generateStaticContent(array $node, string $pageTitle, array $re
677685
678686 // Add "Building Blocks Used" section if uses are defined
679687 if (! empty ($ node ['uses ' ])) {
680- $ content .= $ this ->generateUsedComponentsSection ($ node , $ registry , $ navPathMap );
688+ $ content .= $ this ->generateUsedComponentsSection ($ node , $ registry , $ navPathMap, $ allNodes );
681689 }
682690
683691 // Add "Used By Building Blocks" section
@@ -981,39 +989,105 @@ private function generateReferencedBySection(string $ownerKey, array $referenced
981989 return $ content ;
982990 }
983991
984- private function generateUsedComponentsSection (array $ node , array $ registry , array $ navPathMap ): string
992+ /**
993+ * Recursively collect all dependencies (transitive closure)
994+ *
995+ * @param string $owner The owner to collect dependencies for
996+ * @param array $allNodes All documentation nodes
997+ * @param array $visited Track visited nodes to detect cycles
998+ * @param int $depth Current depth level
999+ * @param int $maxDepth Maximum recursion depth
1000+ * @return array Array of dependencies with structure: ['owner' => string, 'depth' => int, 'uses' => array]
1001+ */
1002+ private function collectRecursiveDependencies (string $ owner , array $ allNodes , array &$ visited = [], int $ depth = 0 , int $ maxDepth = 5 ): array
1003+ {
1004+ // Stop if max depth reached
1005+ if ($ depth >= $ maxDepth ) {
1006+ return [];
1007+ }
1008+
1009+ // Mark as visited to detect cycles
1010+ if (isset ($ visited [$ owner ])) {
1011+ return []; // Already visited, skip to avoid cycles
1012+ }
1013+ $ visited [$ owner ] = true ;
1014+
1015+ $ dependencies = [];
1016+
1017+ // Find the node for this owner
1018+ $ currentNode = null ;
1019+ foreach ($ allNodes as $ node ) {
1020+ if ($ node ['owner ' ] === $ owner ) {
1021+ $ currentNode = $ node ;
1022+ break ;
1023+ }
1024+ }
1025+
1026+ if (! $ currentNode || empty ($ currentNode ['uses ' ])) {
1027+ return [];
1028+ }
1029+
1030+ // Collect direct dependencies
1031+ foreach ($ currentNode ['uses ' ] as $ used ) {
1032+ $ usedKey = ltrim (trim ((string ) $ used ), '\\' );
1033+
1034+ $ dependencies [] = [
1035+ 'owner ' => $ usedKey ,
1036+ 'depth ' => $ depth ,
1037+ 'uses ' => [],
1038+ ];
1039+
1040+ // Recursively collect dependencies of this dependency
1041+ $ nestedDeps = $ this ->collectRecursiveDependencies ($ usedKey , $ allNodes , $ visited , $ depth + 1 , $ maxDepth );
1042+ if (! empty ($ nestedDeps )) {
1043+ $ dependencies [count ($ dependencies ) - 1 ]['uses ' ] = $ nestedDeps ;
1044+ }
1045+ }
1046+
1047+ return $ dependencies ;
1048+ }
1049+
1050+ private function generateUsedComponentsSection (array $ node , array $ registry , array $ navPathMap , array $ allNodes = []): string
9851051 {
9861052 $ content = "\n\n## Building Blocks Used \n\n" ;
9871053 $ content .= "This functionality is composed of the following reusable components: \n\n" ;
9881054
9891055 $ mermaidLinks = [];
990- $ mermaidContent = "graph LR \n" ;
1056+ $ mermaidContent = "graph TD \n" ; // Changed to TD (top-down) for better nested visualization
9911057 $ ownerId = $ this ->slug ($ node ['owner ' ]);
9921058 $ ownerNavPath = $ navPathMap [$ node ['owner ' ]] ?? '' ;
9931059 $ mermaidContent .= " {$ ownerId }[ \"{$ ownerNavPath }\"]; \n" ;
9941060
9951061 $ sourcePath = $ registry [$ node ['owner ' ]] ?? '' ;
9961062
1063+ // Recursively collect all dependencies
1064+ $ visited = [$ node ['owner ' ] => true ]; // Mark current node as visited to prevent self-references
1065+ $ allDependencies = [];
1066+
1067+ // Collect direct dependencies with recursive expansion
9971068 foreach ($ node ['uses ' ] as $ used ) {
9981069 $ usedRaw = trim ((string ) $ used );
9991070 $ lookupKey = ltrim ($ usedRaw , '\\' );
1000- $ usedId = $ this ->slug ($ usedRaw );
1001- $ usedNavPath = $ navPathMap [$ lookupKey ] ?? $ usedRaw ;
10021071
1003- if ( isset ( $ registry [ $ lookupKey ])) {
1004- $ targetPath = $ registry [ $ lookupKey ];
1005- $ relativeFilePath = $ this -> makeRelativePath ( $ targetPath , $ sourcePath ) ;
1006- $ relativeUrl = $ this ->toCleanUrl ( $ relativeFilePath );
1072+ // Collect recursive dependencies for this component
1073+ // Reset visited for each direct dependency, but keep current node marked
1074+ $ localVisited = $ visited ;
1075+ $ nestedDeps = $ this ->collectRecursiveDependencies ( $ lookupKey , $ allNodes , $ localVisited , 0 , 5 );
10071076
1008- $ content .= "* [ {$ usedNavPath }]( {$ relativeUrl }) \n" ;
1009- $ mermaidContent .= " {$ ownerId } --> {$ usedId }[ \"{$ usedNavPath }\"]; \n" ;
1010- $ mermaidLinks [] = "click {$ usedId } \"{$ relativeUrl }\" \"View documentation for {$ usedRaw }\"" ;
1011- } else {
1012- $ content .= "* {$ usedNavPath } (Not documented) \n" ;
1013- $ mermaidContent .= " {$ ownerId } --> {$ usedId }[ \"{$ usedNavPath }\"]; \n" ;
1014- }
1077+ $ allDependencies [] = [
1078+ 'owner ' => $ lookupKey ,
1079+ 'depth ' => 0 ,
1080+ 'raw ' => $ usedRaw ,
1081+ 'uses ' => $ nestedDeps ,
1082+ ];
10151083 }
10161084
1085+ // Generate list content (flat list with depth indication for readability)
1086+ $ this ->generateDependencyList ($ content , $ allDependencies , $ registry , $ navPathMap , $ sourcePath , 0 );
1087+
1088+ // Generate Mermaid diagram with connections
1089+ $ this ->addMermaidDependencies ($ mermaidContent , $ mermaidLinks , $ ownerId , $ allDependencies , $ registry , $ navPathMap , $ sourcePath );
1090+
10171091 $ content .= "\n\n### Composition Graph \n\n" ;
10181092 $ content .= "```mermaid \n" ;
10191093 $ content .= $ mermaidContent ;
@@ -1026,6 +1100,61 @@ private function generateUsedComponentsSection(array $node, array $registry, arr
10261100 return $ content ;
10271101 }
10281102
1103+ /**
1104+ * Generate a hierarchical list of dependencies
1105+ */
1106+ private function generateDependencyList (string &$ content , array $ dependencies , array $ registry , array $ navPathMap , string $ sourcePath , int $ depth = 0 ): void
1107+ {
1108+ $ indent = str_repeat (' ' , $ depth );
1109+
1110+ foreach ($ dependencies as $ dep ) {
1111+ $ lookupKey = $ dep ['owner ' ];
1112+ $ usedRaw = $ dep ['raw ' ] ?? $ lookupKey ;
1113+ $ usedNavPath = $ navPathMap [$ lookupKey ] ?? $ usedRaw ;
1114+
1115+ if (isset ($ registry [$ lookupKey ])) {
1116+ $ targetPath = $ registry [$ lookupKey ];
1117+ $ relativeFilePath = $ this ->makeRelativePath ($ targetPath , $ sourcePath );
1118+ $ relativeUrl = $ this ->toCleanUrl ($ relativeFilePath );
1119+ $ content .= "{$ indent }* [ {$ usedNavPath }]( {$ relativeUrl }) \n" ;
1120+ } else {
1121+ $ content .= "{$ indent }* {$ usedNavPath } (Not documented) \n" ;
1122+ }
1123+
1124+ // Recursively add nested dependencies
1125+ if (! empty ($ dep ['uses ' ])) {
1126+ $ this ->generateDependencyList ($ content , $ dep ['uses ' ], $ registry , $ navPathMap , $ sourcePath , $ depth + 1 );
1127+ }
1128+ }
1129+ }
1130+
1131+ /**
1132+ * Recursively add dependencies to Mermaid diagram
1133+ */
1134+ private function addMermaidDependencies (string &$ mermaidContent , array &$ mermaidLinks , string $ parentId , array $ dependencies , array $ registry , array $ navPathMap , string $ sourcePath ): void
1135+ {
1136+ foreach ($ dependencies as $ dep ) {
1137+ $ lookupKey = $ dep ['owner ' ];
1138+ $ usedRaw = $ dep ['raw ' ] ?? $ lookupKey ;
1139+ $ usedId = $ this ->slug ($ usedRaw );
1140+ $ usedNavPath = $ navPathMap [$ lookupKey ] ?? $ usedRaw ;
1141+
1142+ $ mermaidContent .= " {$ parentId } --> {$ usedId }[ \"{$ usedNavPath }\"]; \n" ;
1143+
1144+ if (isset ($ registry [$ lookupKey ])) {
1145+ $ targetPath = $ registry [$ lookupKey ];
1146+ $ relativeFilePath = $ this ->makeRelativePath ($ targetPath , $ sourcePath );
1147+ $ relativeUrl = $ this ->toCleanUrl ($ relativeFilePath );
1148+ $ mermaidLinks [] = "click {$ usedId } \"{$ relativeUrl }\" \"View documentation for {$ usedRaw }\"" ;
1149+ }
1150+
1151+ // Recursively add nested dependencies
1152+ if (! empty ($ dep ['uses ' ])) {
1153+ $ this ->addMermaidDependencies ($ mermaidContent , $ mermaidLinks , $ usedId , $ dep ['uses ' ], $ registry , $ navPathMap , $ sourcePath );
1154+ }
1155+ }
1156+ }
1157+
10291158 private function generateUsedBySection (string $ ownerKey , array $ usedBy , array $ registry , array $ navPathMap ): string
10301159 {
10311160 $ content = "\n\n## Used By Building Blocks \n\n" ;
0 commit comments