Skip to content

Commit ac22f1d

Browse files
committed
recursive composition graph
1 parent 0b54484 commit ac22f1d

File tree

2 files changed

+154
-25
lines changed

2 files changed

+154
-25
lines changed

src/FunctionalDocBlockExtractor.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99

1010
/**
1111
* @functional
12-
* Another class
13-
* * @nav Main Section / Sub Section / Another Page
12+
* Extracts functional documentation from PHP docblocks.
1413
*
14+
* @nav Main Section / Sub Section / Another Page
15+
* @uses \Xentral\LaravelDocs\MkDocsGenerator
1516
*/
1617
class FunctionalDocBlockExtractor extends NodeVisitorAbstract
1718
{

src/MkDocsGenerator.php

Lines changed: 151 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
use Illuminate\Filesystem\Filesystem;
66
use 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+
*/
815
class 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

Comments
 (0)