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+ * @uses \Illuminate\Filesystem\Filesystem
14+ */
815class MkDocsGenerator
916{
1017 private array $ validationWarnings = [];
@@ -67,7 +74,7 @@ public function generate(array $documentationNodes, string $docsBaseDir): void
6774 $ referencedBy = $ this ->buildReferencedByMap ($ processedNodes , $ registry , $ navPathMap , $ navIdMap );
6875
6976 // Generate the document tree
70- $ docTree = $ this ->generateDocTree ($ processedNodes , $ registry , $ navPathMap , $ navIdMap , $ usedBy , $ referencedBy );
77+ $ docTree = $ this ->generateDocTree ($ processedNodes , $ registry , $ navPathMap , $ navIdMap , $ usedBy , $ referencedBy, $ processedNodes );
7178
7279 // Prepare output directory
7380 $ this ->filesystem ->deleteDirectory ($ docsOutputDir );
@@ -495,7 +502,7 @@ private function buildReferencedByMap(array $documentationNodes, array $registry
495502 return $ referencedBy ;
496503 }
497504
498- private function generateDocTree (array $ documentationNodes , array $ registry , array $ navPathMap , array $ navIdMap , array $ usedBy , array $ referencedBy ): array
505+ private function generateDocTree (array $ documentationNodes , array $ registry , array $ navPathMap , array $ navIdMap , array $ usedBy , array $ referencedBy, array $ allNodes ): array
499506 {
500507 $ docTree = [];
501508 $ pathRegistry = [];
@@ -531,7 +538,7 @@ private function generateDocTree(array $documentationNodes, array $registry, arr
531538 }
532539
533540 // Generate the markdown content
534- $ markdownContent = $ this ->generateMarkdownContent ($ node , $ pageTitle , $ registry , $ navPathMap , $ navIdMap , $ usedBy , $ referencedBy );
541+ $ markdownContent = $ this ->generateMarkdownContent ($ node , $ pageTitle , $ registry , $ navPathMap , $ navIdMap , $ usedBy , $ referencedBy, $ allNodes );
535542
536543 // Build the path in the document tree
537544 $ docTree = $ this ->addToDocTree ($ docTree , $ pathSegments , $ originalPageTitle , $ pageFileName , $ markdownContent );
@@ -623,11 +630,11 @@ private function setInNestedArray(array $array, array $path, string $originalPag
623630 return $ array ;
624631 }
625632
626- private function generateMarkdownContent (array $ node , string $ pageTitle , array $ registry , array $ navPathMap , array $ navIdMap , array $ usedBy , array $ referencedBy ): string
633+ private function generateMarkdownContent (array $ node , string $ pageTitle , array $ registry , array $ navPathMap , array $ navIdMap , array $ usedBy , array $ referencedBy, array $ allNodes ): string
627634 {
628635 // Handle static content nodes differently
629636 if (isset ($ node ['type ' ]) && $ node ['type ' ] === 'static_content ' ) {
630- return $ this ->generateStaticContent ($ node , $ pageTitle , $ registry , $ navPathMap , $ navIdMap , $ usedBy , $ referencedBy );
637+ return $ this ->generateStaticContent ($ node , $ pageTitle , $ registry , $ navPathMap , $ navIdMap , $ usedBy , $ referencedBy, $ allNodes );
631638 }
632639
633640 $ markdownContent = "# {$ pageTitle }\n\n" ;
@@ -639,7 +646,7 @@ private function generateMarkdownContent(array $node, string $pageTitle, array $
639646
640647 // Add "Building Blocks Used" section
641648 if (! empty ($ node ['uses ' ])) {
642- $ markdownContent .= $ this ->generateUsedComponentsSection ($ node , $ registry , $ navPathMap );
649+ $ markdownContent .= $ this ->generateUsedComponentsSection ($ node , $ registry , $ navPathMap, $ allNodes );
643650 }
644651
645652 // Add "Used By Building Blocks" section
@@ -661,7 +668,7 @@ private function generateMarkdownContent(array $node, string $pageTitle, array $
661668 return $ markdownContent ;
662669 }
663670
664- private function generateStaticContent (array $ node , string $ pageTitle , array $ registry = [], array $ navPathMap = [], array $ navIdMap = [], array $ usedBy = [], array $ referencedBy = []): string
671+ private function generateStaticContent (array $ node , string $ pageTitle , array $ registry = [], array $ navPathMap = [], array $ navIdMap = [], array $ usedBy = [], array $ referencedBy = [], array $ allNodes = [] ): string
665672 {
666673 // For static content, we don't add the title since it might already be in the content
667674 // We also don't add the source subtitle
@@ -677,7 +684,7 @@ private function generateStaticContent(array $node, string $pageTitle, array $re
677684
678685 // Add "Building Blocks Used" section if uses are defined
679686 if (! empty ($ node ['uses ' ])) {
680- $ content .= $ this ->generateUsedComponentsSection ($ node , $ registry , $ navPathMap );
687+ $ content .= $ this ->generateUsedComponentsSection ($ node , $ registry , $ navPathMap, $ allNodes );
681688 }
682689
683690 // Add "Used By Building Blocks" section
@@ -981,39 +988,105 @@ private function generateReferencedBySection(string $ownerKey, array $referenced
981988 return $ content ;
982989 }
983990
984- private function generateUsedComponentsSection (array $ node , array $ registry , array $ navPathMap ): string
991+ /**
992+ * Recursively collect all dependencies (transitive closure)
993+ *
994+ * @param string $owner The owner to collect dependencies for
995+ * @param array $allNodes All documentation nodes
996+ * @param array $visited Track visited nodes to detect cycles
997+ * @param int $depth Current depth level
998+ * @param int $maxDepth Maximum recursion depth
999+ * @return array Array of dependencies with structure: ['owner' => string, 'depth' => int, 'uses' => array]
1000+ */
1001+ private function collectRecursiveDependencies (string $ owner , array $ allNodes , array &$ visited = [], int $ depth = 0 , int $ maxDepth = 5 ): array
1002+ {
1003+ // Stop if max depth reached
1004+ if ($ depth >= $ maxDepth ) {
1005+ return [];
1006+ }
1007+
1008+ // Mark as visited to detect cycles
1009+ if (isset ($ visited [$ owner ])) {
1010+ return []; // Already visited, skip to avoid cycles
1011+ }
1012+ $ visited [$ owner ] = true ;
1013+
1014+ $ dependencies = [];
1015+
1016+ // Find the node for this owner
1017+ $ currentNode = null ;
1018+ foreach ($ allNodes as $ node ) {
1019+ if ($ node ['owner ' ] === $ owner ) {
1020+ $ currentNode = $ node ;
1021+ break ;
1022+ }
1023+ }
1024+
1025+ if (!$ currentNode || empty ($ currentNode ['uses ' ])) {
1026+ return [];
1027+ }
1028+
1029+ // Collect direct dependencies
1030+ foreach ($ currentNode ['uses ' ] as $ used ) {
1031+ $ usedKey = ltrim (trim ((string ) $ used ), '\\' );
1032+
1033+ $ dependencies [] = [
1034+ 'owner ' => $ usedKey ,
1035+ 'depth ' => $ depth ,
1036+ 'uses ' => [],
1037+ ];
1038+
1039+ // Recursively collect dependencies of this dependency
1040+ $ nestedDeps = $ this ->collectRecursiveDependencies ($ usedKey , $ allNodes , $ visited , $ depth + 1 , $ maxDepth );
1041+ if (!empty ($ nestedDeps )) {
1042+ $ dependencies [count ($ dependencies ) - 1 ]['uses ' ] = $ nestedDeps ;
1043+ }
1044+ }
1045+
1046+ return $ dependencies ;
1047+ }
1048+
1049+ private function generateUsedComponentsSection (array $ node , array $ registry , array $ navPathMap , array $ allNodes = []): string
9851050 {
9861051 $ content = "\n\n## Building Blocks Used \n\n" ;
9871052 $ content .= "This functionality is composed of the following reusable components: \n\n" ;
9881053
9891054 $ mermaidLinks = [];
990- $ mermaidContent = "graph LR \n" ;
1055+ $ mermaidContent = "graph TD \n" ; // Changed to TD (top-down) for better nested visualization
9911056 $ ownerId = $ this ->slug ($ node ['owner ' ]);
9921057 $ ownerNavPath = $ navPathMap [$ node ['owner ' ]] ?? '' ;
9931058 $ mermaidContent .= " {$ ownerId }[ \"{$ ownerNavPath }\"]; \n" ;
9941059
9951060 $ sourcePath = $ registry [$ node ['owner ' ]] ?? '' ;
9961061
1062+ // Recursively collect all dependencies
1063+ $ visited = [$ node ['owner ' ] => true ]; // Mark current node as visited to prevent self-references
1064+ $ allDependencies = [];
1065+
1066+ // Collect direct dependencies with recursive expansion
9971067 foreach ($ node ['uses ' ] as $ used ) {
9981068 $ usedRaw = trim ((string ) $ used );
9991069 $ lookupKey = ltrim ($ usedRaw , '\\' );
1000- $ usedId = $ this ->slug ($ usedRaw );
1001- $ usedNavPath = $ navPathMap [$ lookupKey ] ?? $ usedRaw ;
10021070
1003- if ( isset ( $ registry [ $ lookupKey ])) {
1004- $ targetPath = $ registry [ $ lookupKey ];
1005- $ relativeFilePath = $ this -> makeRelativePath ( $ targetPath , $ sourcePath ) ;
1006- $ relativeUrl = $ this ->toCleanUrl ( $ relativeFilePath );
1071+ // Collect recursive dependencies for this component
1072+ // Reset visited for each direct dependency, but keep current node marked
1073+ $ localVisited = $ visited ;
1074+ $ nestedDeps = $ this ->collectRecursiveDependencies ( $ lookupKey , $ allNodes , $ localVisited , 0 , 5 );
10071075
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- }
1076+ $ allDependencies [] = [
1077+ 'owner ' => $ lookupKey ,
1078+ 'depth ' => 0 ,
1079+ 'raw ' => $ usedRaw ,
1080+ 'uses ' => $ nestedDeps ,
1081+ ];
10151082 }
10161083
1084+ // Generate list content (flat list with depth indication for readability)
1085+ $ this ->generateDependencyList ($ content , $ allDependencies , $ registry , $ navPathMap , $ sourcePath , 0 );
1086+
1087+ // Generate Mermaid diagram with connections
1088+ $ this ->addMermaidDependencies ($ mermaidContent , $ mermaidLinks , $ ownerId , $ allDependencies , $ registry , $ navPathMap , $ sourcePath );
1089+
10171090 $ content .= "\n\n### Composition Graph \n\n" ;
10181091 $ content .= "```mermaid \n" ;
10191092 $ content .= $ mermaidContent ;
@@ -1026,6 +1099,61 @@ private function generateUsedComponentsSection(array $node, array $registry, arr
10261099 return $ content ;
10271100 }
10281101
1102+ /**
1103+ * Generate a hierarchical list of dependencies
1104+ */
1105+ private function generateDependencyList (string &$ content , array $ dependencies , array $ registry , array $ navPathMap , string $ sourcePath , int $ depth = 0 ): void
1106+ {
1107+ $ indent = str_repeat (' ' , $ depth );
1108+
1109+ foreach ($ dependencies as $ dep ) {
1110+ $ lookupKey = $ dep ['owner ' ];
1111+ $ usedRaw = $ dep ['raw ' ] ?? $ lookupKey ;
1112+ $ usedNavPath = $ navPathMap [$ lookupKey ] ?? $ usedRaw ;
1113+
1114+ if (isset ($ registry [$ lookupKey ])) {
1115+ $ targetPath = $ registry [$ lookupKey ];
1116+ $ relativeFilePath = $ this ->makeRelativePath ($ targetPath , $ sourcePath );
1117+ $ relativeUrl = $ this ->toCleanUrl ($ relativeFilePath );
1118+ $ content .= "{$ indent }* [ {$ usedNavPath }]( {$ relativeUrl }) \n" ;
1119+ } else {
1120+ $ content .= "{$ indent }* {$ usedNavPath } (Not documented) \n" ;
1121+ }
1122+
1123+ // Recursively add nested dependencies
1124+ if (!empty ($ dep ['uses ' ])) {
1125+ $ this ->generateDependencyList ($ content , $ dep ['uses ' ], $ registry , $ navPathMap , $ sourcePath , $ depth + 1 );
1126+ }
1127+ }
1128+ }
1129+
1130+ /**
1131+ * Recursively add dependencies to Mermaid diagram
1132+ */
1133+ private function addMermaidDependencies (string &$ mermaidContent , array &$ mermaidLinks , string $ parentId , array $ dependencies , array $ registry , array $ navPathMap , string $ sourcePath ): void
1134+ {
1135+ foreach ($ dependencies as $ dep ) {
1136+ $ lookupKey = $ dep ['owner ' ];
1137+ $ usedRaw = $ dep ['raw ' ] ?? $ lookupKey ;
1138+ $ usedId = $ this ->slug ($ usedRaw );
1139+ $ usedNavPath = $ navPathMap [$ lookupKey ] ?? $ usedRaw ;
1140+
1141+ $ mermaidContent .= " {$ parentId } --> {$ usedId }[ \"{$ usedNavPath }\"]; \n" ;
1142+
1143+ if (isset ($ registry [$ lookupKey ])) {
1144+ $ targetPath = $ registry [$ lookupKey ];
1145+ $ relativeFilePath = $ this ->makeRelativePath ($ targetPath , $ sourcePath );
1146+ $ relativeUrl = $ this ->toCleanUrl ($ relativeFilePath );
1147+ $ mermaidLinks [] = "click {$ usedId } \"{$ relativeUrl }\" \"View documentation for {$ usedRaw }\"" ;
1148+ }
1149+
1150+ // Recursively add nested dependencies
1151+ if (!empty ($ dep ['uses ' ])) {
1152+ $ this ->addMermaidDependencies ($ mermaidContent , $ mermaidLinks , $ usedId , $ dep ['uses ' ], $ registry , $ navPathMap , $ sourcePath );
1153+ }
1154+ }
1155+ }
1156+
10291157 private function generateUsedBySection (string $ ownerKey , array $ usedBy , array $ registry , array $ navPathMap ): string
10301158 {
10311159 $ content = "\n\n## Used By Building Blocks \n\n" ;
0 commit comments