From 0b544849b92f1ba6b9f0f6ff6de3d8c29d851882 Mon Sep 17 00:00:00 2001 From: Manuel Christlieb Date: Tue, 4 Nov 2025 14:43:48 +0100 Subject: [PATCH 1/3] fix stop printing nav in content --- src/FunctionalDocBlockExtractor.php | 10 +++++++++- workbench/config/docs.php | 18 +++--------------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/FunctionalDocBlockExtractor.php b/src/FunctionalDocBlockExtractor.php index b17687e..149e7f4 100644 --- a/src/FunctionalDocBlockExtractor.php +++ b/src/FunctionalDocBlockExtractor.php @@ -7,6 +7,12 @@ use PhpParser\Node\Stmt\Function_; use PhpParser\NodeVisitorAbstract; +/** + * @functional + * Another class + * * @nav Main Section / Sub Section / Another Page + * + */ class FunctionalDocBlockExtractor extends NodeVisitorAbstract { public array $foundDocs = []; @@ -137,7 +143,9 @@ private function parseDocComment(string $docComment, string $defaultTitle, strin } if ($inFunctionalBlock) { - if (str_starts_with(trim((string) $testLine), '@')) { + // Check if this line contains an annotation (even if it's in a bullet list) + $trimmedTest = ltrim(trim((string) $testLine), '* -'); + if (str_starts_with(trim($trimmedTest), '@')) { break; } $rawFunctionalLines[] = $testLine; diff --git a/workbench/config/docs.php b/workbench/config/docs.php index 90c8837..a1b1f7b 100644 --- a/workbench/config/docs.php +++ b/workbench/config/docs.php @@ -4,21 +4,9 @@ 'paths' => [dirname(__DIR__, 2).'/src', dirname(__DIR__).'/app'], 'output' => dirname(__DIR__, 2).'/docs', 'commands' => [ - // {path} and {port} will be replaced with the configured/passed values - 'build' => 'docker run --rm -v {path}:/docs squidfunk/mkdocs-material build', - 'serve' => [ - 'docker', 'run', '--rm', '-it', - '-p', '{port}:{port}', - '-v', '{path}:/docs', - '-e', 'ADD_MODULES=mkdocs-material pymdown-extensions', - '-e', 'LIVE_RELOAD_SUPPORT=true', - '-e', 'FAST_MODE=true', - '-e', 'DOCS_DIRECTORY=/docs', - '-e', 'AUTO_UPDATE=true', - '-e', 'UPDATE_INTERVAL=1', - '-e', 'DEV_ADDR=0.0.0.0:{port}', - 'polinux/mkdocs', - ], + 'build' => 'uvx -w mkdocs-material -w pymdown-extensions mkdocs build', + 'publish' => 'uvx -w mkdocs-material -w pymdown-extensions mkdocs gh-deploy', + 'serve' => 'uvx -w mkdocs-material -w pymdown-extensions mkdocs serve', ], 'config' => [ 'site_name' => 'Xentral Functional Documentation', From ac22f1d0513a43bcd2cba34097dac6d781c1ca1e Mon Sep 17 00:00:00 2001 From: Manuel Christlieb Date: Tue, 4 Nov 2025 14:52:17 +0100 Subject: [PATCH 2/3] recursive composition graph --- src/FunctionalDocBlockExtractor.php | 5 +- src/MkDocsGenerator.php | 174 ++++++++++++++++++++++++---- 2 files changed, 154 insertions(+), 25 deletions(-) diff --git a/src/FunctionalDocBlockExtractor.php b/src/FunctionalDocBlockExtractor.php index 149e7f4..6995635 100644 --- a/src/FunctionalDocBlockExtractor.php +++ b/src/FunctionalDocBlockExtractor.php @@ -9,9 +9,10 @@ /** * @functional - * Another class - * * @nav Main Section / Sub Section / Another Page + * Extracts functional documentation from PHP docblocks. * + * @nav Main Section / Sub Section / Another Page + * @uses \Xentral\LaravelDocs\MkDocsGenerator */ class FunctionalDocBlockExtractor extends NodeVisitorAbstract { diff --git a/src/MkDocsGenerator.php b/src/MkDocsGenerator.php index e34740b..503a6f0 100644 --- a/src/MkDocsGenerator.php +++ b/src/MkDocsGenerator.php @@ -5,6 +5,13 @@ use Illuminate\Filesystem\Filesystem; use Symfony\Component\Yaml\Yaml; +/** + * @functional + * Generates MkDocs documentation from extracted functional documentation. + * + * @nav Main Section / Generator / MkDocs Generator + * @uses \Illuminate\Filesystem\Filesystem + */ class MkDocsGenerator { private array $validationWarnings = []; @@ -67,7 +74,7 @@ public function generate(array $documentationNodes, string $docsBaseDir): void $referencedBy = $this->buildReferencedByMap($processedNodes, $registry, $navPathMap, $navIdMap); // Generate the document tree - $docTree = $this->generateDocTree($processedNodes, $registry, $navPathMap, $navIdMap, $usedBy, $referencedBy); + $docTree = $this->generateDocTree($processedNodes, $registry, $navPathMap, $navIdMap, $usedBy, $referencedBy, $processedNodes); // Prepare output directory $this->filesystem->deleteDirectory($docsOutputDir); @@ -495,7 +502,7 @@ private function buildReferencedByMap(array $documentationNodes, array $registry return $referencedBy; } - private function generateDocTree(array $documentationNodes, array $registry, array $navPathMap, array $navIdMap, array $usedBy, array $referencedBy): array + private function generateDocTree(array $documentationNodes, array $registry, array $navPathMap, array $navIdMap, array $usedBy, array $referencedBy, array $allNodes): array { $docTree = []; $pathRegistry = []; @@ -531,7 +538,7 @@ private function generateDocTree(array $documentationNodes, array $registry, arr } // Generate the markdown content - $markdownContent = $this->generateMarkdownContent($node, $pageTitle, $registry, $navPathMap, $navIdMap, $usedBy, $referencedBy); + $markdownContent = $this->generateMarkdownContent($node, $pageTitle, $registry, $navPathMap, $navIdMap, $usedBy, $referencedBy, $allNodes); // Build the path in the document tree $docTree = $this->addToDocTree($docTree, $pathSegments, $originalPageTitle, $pageFileName, $markdownContent); @@ -623,11 +630,11 @@ private function setInNestedArray(array $array, array $path, string $originalPag return $array; } - private function generateMarkdownContent(array $node, string $pageTitle, array $registry, array $navPathMap, array $navIdMap, array $usedBy, array $referencedBy): string + private function generateMarkdownContent(array $node, string $pageTitle, array $registry, array $navPathMap, array $navIdMap, array $usedBy, array $referencedBy, array $allNodes): string { // Handle static content nodes differently if (isset($node['type']) && $node['type'] === 'static_content') { - return $this->generateStaticContent($node, $pageTitle, $registry, $navPathMap, $navIdMap, $usedBy, $referencedBy); + return $this->generateStaticContent($node, $pageTitle, $registry, $navPathMap, $navIdMap, $usedBy, $referencedBy, $allNodes); } $markdownContent = "# {$pageTitle}\n\n"; @@ -639,7 +646,7 @@ private function generateMarkdownContent(array $node, string $pageTitle, array $ // Add "Building Blocks Used" section if (! empty($node['uses'])) { - $markdownContent .= $this->generateUsedComponentsSection($node, $registry, $navPathMap); + $markdownContent .= $this->generateUsedComponentsSection($node, $registry, $navPathMap, $allNodes); } // Add "Used By Building Blocks" section @@ -661,7 +668,7 @@ private function generateMarkdownContent(array $node, string $pageTitle, array $ return $markdownContent; } - private function generateStaticContent(array $node, string $pageTitle, array $registry = [], array $navPathMap = [], array $navIdMap = [], array $usedBy = [], array $referencedBy = []): string + private function generateStaticContent(array $node, string $pageTitle, array $registry = [], array $navPathMap = [], array $navIdMap = [], array $usedBy = [], array $referencedBy = [], array $allNodes = []): string { // For static content, we don't add the title since it might already be in the content // We also don't add the source subtitle @@ -677,7 +684,7 @@ private function generateStaticContent(array $node, string $pageTitle, array $re // Add "Building Blocks Used" section if uses are defined if (! empty($node['uses'])) { - $content .= $this->generateUsedComponentsSection($node, $registry, $navPathMap); + $content .= $this->generateUsedComponentsSection($node, $registry, $navPathMap, $allNodes); } // Add "Used By Building Blocks" section @@ -981,39 +988,105 @@ private function generateReferencedBySection(string $ownerKey, array $referenced return $content; } - private function generateUsedComponentsSection(array $node, array $registry, array $navPathMap): string + /** + * Recursively collect all dependencies (transitive closure) + * + * @param string $owner The owner to collect dependencies for + * @param array $allNodes All documentation nodes + * @param array $visited Track visited nodes to detect cycles + * @param int $depth Current depth level + * @param int $maxDepth Maximum recursion depth + * @return array Array of dependencies with structure: ['owner' => string, 'depth' => int, 'uses' => array] + */ + private function collectRecursiveDependencies(string $owner, array $allNodes, array &$visited = [], int $depth = 0, int $maxDepth = 5): array + { + // Stop if max depth reached + if ($depth >= $maxDepth) { + return []; + } + + // Mark as visited to detect cycles + if (isset($visited[$owner])) { + return []; // Already visited, skip to avoid cycles + } + $visited[$owner] = true; + + $dependencies = []; + + // Find the node for this owner + $currentNode = null; + foreach ($allNodes as $node) { + if ($node['owner'] === $owner) { + $currentNode = $node; + break; + } + } + + if (!$currentNode || empty($currentNode['uses'])) { + return []; + } + + // Collect direct dependencies + foreach ($currentNode['uses'] as $used) { + $usedKey = ltrim(trim((string) $used), '\\'); + + $dependencies[] = [ + 'owner' => $usedKey, + 'depth' => $depth, + 'uses' => [], + ]; + + // Recursively collect dependencies of this dependency + $nestedDeps = $this->collectRecursiveDependencies($usedKey, $allNodes, $visited, $depth + 1, $maxDepth); + if (!empty($nestedDeps)) { + $dependencies[count($dependencies) - 1]['uses'] = $nestedDeps; + } + } + + return $dependencies; + } + + private function generateUsedComponentsSection(array $node, array $registry, array $navPathMap, array $allNodes = []): string { $content = "\n\n## Building Blocks Used\n\n"; $content .= "This functionality is composed of the following reusable components:\n\n"; $mermaidLinks = []; - $mermaidContent = "graph LR\n"; + $mermaidContent = "graph TD\n"; // Changed to TD (top-down) for better nested visualization $ownerId = $this->slug($node['owner']); $ownerNavPath = $navPathMap[$node['owner']] ?? ''; $mermaidContent .= " {$ownerId}[\"{$ownerNavPath}\"];\n"; $sourcePath = $registry[$node['owner']] ?? ''; + // Recursively collect all dependencies + $visited = [$node['owner'] => true]; // Mark current node as visited to prevent self-references + $allDependencies = []; + + // Collect direct dependencies with recursive expansion foreach ($node['uses'] as $used) { $usedRaw = trim((string) $used); $lookupKey = ltrim($usedRaw, '\\'); - $usedId = $this->slug($usedRaw); - $usedNavPath = $navPathMap[$lookupKey] ?? $usedRaw; - if (isset($registry[$lookupKey])) { - $targetPath = $registry[$lookupKey]; - $relativeFilePath = $this->makeRelativePath($targetPath, $sourcePath); - $relativeUrl = $this->toCleanUrl($relativeFilePath); + // Collect recursive dependencies for this component + // Reset visited for each direct dependency, but keep current node marked + $localVisited = $visited; + $nestedDeps = $this->collectRecursiveDependencies($lookupKey, $allNodes, $localVisited, 0, 5); - $content .= "* [{$usedNavPath}]({$relativeUrl})\n"; - $mermaidContent .= " {$ownerId} --> {$usedId}[\"{$usedNavPath}\"];\n"; - $mermaidLinks[] = "click {$usedId} \"{$relativeUrl}\" \"View documentation for {$usedRaw}\""; - } else { - $content .= "* {$usedNavPath} (Not documented)\n"; - $mermaidContent .= " {$ownerId} --> {$usedId}[\"{$usedNavPath}\"];\n"; - } + $allDependencies[] = [ + 'owner' => $lookupKey, + 'depth' => 0, + 'raw' => $usedRaw, + 'uses' => $nestedDeps, + ]; } + // Generate list content (flat list with depth indication for readability) + $this->generateDependencyList($content, $allDependencies, $registry, $navPathMap, $sourcePath, 0); + + // Generate Mermaid diagram with connections + $this->addMermaidDependencies($mermaidContent, $mermaidLinks, $ownerId, $allDependencies, $registry, $navPathMap, $sourcePath); + $content .= "\n\n### Composition Graph\n\n"; $content .= "```mermaid\n"; $content .= $mermaidContent; @@ -1026,6 +1099,61 @@ private function generateUsedComponentsSection(array $node, array $registry, arr return $content; } + /** + * Generate a hierarchical list of dependencies + */ + private function generateDependencyList(string &$content, array $dependencies, array $registry, array $navPathMap, string $sourcePath, int $depth = 0): void + { + $indent = str_repeat(' ', $depth); + + foreach ($dependencies as $dep) { + $lookupKey = $dep['owner']; + $usedRaw = $dep['raw'] ?? $lookupKey; + $usedNavPath = $navPathMap[$lookupKey] ?? $usedRaw; + + if (isset($registry[$lookupKey])) { + $targetPath = $registry[$lookupKey]; + $relativeFilePath = $this->makeRelativePath($targetPath, $sourcePath); + $relativeUrl = $this->toCleanUrl($relativeFilePath); + $content .= "{$indent}* [{$usedNavPath}]({$relativeUrl})\n"; + } else { + $content .= "{$indent}* {$usedNavPath} (Not documented)\n"; + } + + // Recursively add nested dependencies + if (!empty($dep['uses'])) { + $this->generateDependencyList($content, $dep['uses'], $registry, $navPathMap, $sourcePath, $depth + 1); + } + } + } + + /** + * Recursively add dependencies to Mermaid diagram + */ + private function addMermaidDependencies(string &$mermaidContent, array &$mermaidLinks, string $parentId, array $dependencies, array $registry, array $navPathMap, string $sourcePath): void + { + foreach ($dependencies as $dep) { + $lookupKey = $dep['owner']; + $usedRaw = $dep['raw'] ?? $lookupKey; + $usedId = $this->slug($usedRaw); + $usedNavPath = $navPathMap[$lookupKey] ?? $usedRaw; + + $mermaidContent .= " {$parentId} --> {$usedId}[\"{$usedNavPath}\"];\n"; + + if (isset($registry[$lookupKey])) { + $targetPath = $registry[$lookupKey]; + $relativeFilePath = $this->makeRelativePath($targetPath, $sourcePath); + $relativeUrl = $this->toCleanUrl($relativeFilePath); + $mermaidLinks[] = "click {$usedId} \"{$relativeUrl}\" \"View documentation for {$usedRaw}\""; + } + + // Recursively add nested dependencies + if (!empty($dep['uses'])) { + $this->addMermaidDependencies($mermaidContent, $mermaidLinks, $usedId, $dep['uses'], $registry, $navPathMap, $sourcePath); + } + } + } + private function generateUsedBySection(string $ownerKey, array $usedBy, array $registry, array $navPathMap): string { $content = "\n\n## Used By Building Blocks\n\n"; From 2140c80a5eae882262979bfac3ffb57664df9251 Mon Sep 17 00:00:00 2001 From: bambamboole Date: Tue, 4 Nov 2025 13:53:25 +0000 Subject: [PATCH 3/3] Apply pint changes --- src/FunctionalDocBlockExtractor.php | 1 + src/MkDocsGenerator.php | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/FunctionalDocBlockExtractor.php b/src/FunctionalDocBlockExtractor.php index 6995635..0f6778f 100644 --- a/src/FunctionalDocBlockExtractor.php +++ b/src/FunctionalDocBlockExtractor.php @@ -12,6 +12,7 @@ * Extracts functional documentation from PHP docblocks. * * @nav Main Section / Sub Section / Another Page + * * @uses \Xentral\LaravelDocs\MkDocsGenerator */ class FunctionalDocBlockExtractor extends NodeVisitorAbstract diff --git a/src/MkDocsGenerator.php b/src/MkDocsGenerator.php index 503a6f0..f351c3c 100644 --- a/src/MkDocsGenerator.php +++ b/src/MkDocsGenerator.php @@ -10,6 +10,7 @@ * Generates MkDocs documentation from extracted functional documentation. * * @nav Main Section / Generator / MkDocs Generator + * * @uses \Illuminate\Filesystem\Filesystem */ class MkDocsGenerator @@ -1022,7 +1023,7 @@ private function collectRecursiveDependencies(string $owner, array $allNodes, ar } } - if (!$currentNode || empty($currentNode['uses'])) { + if (! $currentNode || empty($currentNode['uses'])) { return []; } @@ -1038,7 +1039,7 @@ private function collectRecursiveDependencies(string $owner, array $allNodes, ar // Recursively collect dependencies of this dependency $nestedDeps = $this->collectRecursiveDependencies($usedKey, $allNodes, $visited, $depth + 1, $maxDepth); - if (!empty($nestedDeps)) { + if (! empty($nestedDeps)) { $dependencies[count($dependencies) - 1]['uses'] = $nestedDeps; } } @@ -1121,7 +1122,7 @@ private function generateDependencyList(string &$content, array $dependencies, a } // Recursively add nested dependencies - if (!empty($dep['uses'])) { + if (! empty($dep['uses'])) { $this->generateDependencyList($content, $dep['uses'], $registry, $navPathMap, $sourcePath, $depth + 1); } } @@ -1148,7 +1149,7 @@ private function addMermaidDependencies(string &$mermaidContent, array &$mermaid } // Recursively add nested dependencies - if (!empty($dep['uses'])) { + if (! empty($dep['uses'])) { $this->addMermaidDependencies($mermaidContent, $mermaidLinks, $usedId, $dep['uses'], $registry, $navPathMap, $sourcePath); } }