Skip to content

Commit cc81c3e

Browse files
authored
Merge pull request #5 from xentral/ce-0-docs
Ce 0 docs
2 parents b9ff8ca + 2140c80 commit cc81c3e

File tree

3 files changed

+166
-39
lines changed

3 files changed

+166
-39
lines changed

src/FunctionalDocBlockExtractor.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
use PhpParser\Node\Stmt\Function_;
88
use PhpParser\NodeVisitorAbstract;
99

10+
/**
11+
* @functional
12+
* Extracts functional documentation from PHP docblocks.
13+
*
14+
* @nav Main Section / Sub Section / Another Page
15+
*
16+
* @uses \Xentral\LaravelDocs\MkDocsGenerator
17+
*/
1018
class FunctionalDocBlockExtractor extends NodeVisitorAbstract
1119
{
1220
public array $foundDocs = [];
@@ -137,7 +145,9 @@ private function parseDocComment(string $docComment, string $defaultTitle, strin
137145
}
138146

139147
if ($inFunctionalBlock) {
140-
if (str_starts_with(trim((string) $testLine), '@')) {
148+
// Check if this line contains an annotation (even if it's in a bullet list)
149+
$trimmedTest = ltrim(trim((string) $testLine), '* -');
150+
if (str_starts_with(trim($trimmedTest), '@')) {
141151
break;
142152
}
143153
$rawFunctionalLines[] = $testLine;

src/MkDocsGenerator.php

Lines changed: 152 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@
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+
*
14+
* @uses \Illuminate\Filesystem\Filesystem
15+
*/
816
class 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";

workbench/config/docs.php

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,9 @@
44
'paths' => [dirname(__DIR__, 2).'/src', dirname(__DIR__).'/app'],
55
'output' => dirname(__DIR__, 2).'/docs',
66
'commands' => [
7-
// {path} and {port} will be replaced with the configured/passed values
8-
'build' => 'docker run --rm -v {path}:/docs squidfunk/mkdocs-material build',
9-
'serve' => [
10-
'docker', 'run', '--rm', '-it',
11-
'-p', '{port}:{port}',
12-
'-v', '{path}:/docs',
13-
'-e', 'ADD_MODULES=mkdocs-material pymdown-extensions',
14-
'-e', 'LIVE_RELOAD_SUPPORT=true',
15-
'-e', 'FAST_MODE=true',
16-
'-e', 'DOCS_DIRECTORY=/docs',
17-
'-e', 'AUTO_UPDATE=true',
18-
'-e', 'UPDATE_INTERVAL=1',
19-
'-e', 'DEV_ADDR=0.0.0.0:{port}',
20-
'polinux/mkdocs',
21-
],
7+
'build' => 'uvx -w mkdocs-material -w pymdown-extensions mkdocs build',
8+
'publish' => 'uvx -w mkdocs-material -w pymdown-extensions mkdocs gh-deploy',
9+
'serve' => 'uvx -w mkdocs-material -w pymdown-extensions mkdocs serve',
2210
],
2311
'config' => [
2412
'site_name' => 'Xentral Functional Documentation',

0 commit comments

Comments
 (0)