refactor: Cleanup git state - commit all staged changes

Major refactoring cleanup:
- Add new controller architecture (class-controller-*.php)
- Add new settings-v2 UI (views/settings-v2/)
- Add new CSS architecture (agentic-sidebar.css, tokens)
- Add esbuild build pipeline (scripts/build.js, package.json)
- Add composer dependencies (vendor/)
- Add frontend src directory (assets/js/src/index.jsx)
- Add documentation files
- Remove old/obsolete files (class-settings.php, old CSS)

This commits all pending changes from previous refactoring efforts.
This commit is contained in:
Dwindi Ramadhana
2026-06-17 05:27:58 +07:00
parent d3f142222c
commit 690991c526
7963 changed files with 941566 additions and 67372 deletions

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler;
use phpDocumentor\Guides\Compiler\NodeTransformers\NodeTransformerFactory;
use phpDocumentor\Guides\Compiler\NodeTransformers\TransformerPass;
use phpDocumentor\Guides\Nodes\DocumentNode;
use SplPriorityQueue;
final class Compiler
{
/** @var SplPriorityQueue<int, CompilerPass> */
private readonly SplPriorityQueue $passes;
/** @param iterable<CompilerPass> $passes */
public function __construct(
iterable $passes,
NodeTransformerFactory $nodeTransformerFactory,
) {
$this->passes = new SplPriorityQueue();
foreach ($passes as $pass) {
$this->passes->insert($pass, $pass->getPriority());
}
$transformerPriorities = $nodeTransformerFactory->getPriorities();
foreach ($transformerPriorities as $transformerPriority) {
$this->passes->insert(
new TransformerPass(new DocumentNodeTraverser($nodeTransformerFactory, $transformerPriority), $transformerPriority),
$transformerPriority,
);
}
}
/**
* @param DocumentNode[] $documents
*
* @return DocumentNode[]
*/
public function run(array $documents, CompilerContext $compilerContext): array
{
$clonedPasses = clone$this->passes;
foreach ($clonedPasses as $pass) {
$documents = $pass->run($documents, $compilerContext);
}
return $documents;
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler;
use Doctrine\Deprecations\Deprecation;
use Exception;
use phpDocumentor\Guides\Compiler\ShadowTree\TreeNode;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\ProjectNode;
/**
* Context class used in compiler passes to store the state of the nodes.
*
* The {@see Compiler} is making changes to the nodes in a {@see DocumentNode} as the nodes are immutable cannot
* do this directly. This class helps to modify the nodes in the {@see DocumentNode} by creating a shadow tree.
*
* The class is final and should not be extended, if you need to provide more information to the compiler pass
* you can use the {@see CompilerContextInterface} and decorate this class.
*
* @final
*/
class CompilerContext implements CompilerContextInterface
{
/** @var TreeNode<Node> */
private TreeNode $shadowTree;
public function __construct(
private readonly ProjectNode $projectNode,
) {
if (self::class === static::class) {
return;
}
Deprecation::trigger(
'phpdocumentor/guides',
'https://github.com/phpDocumentor/guides/issues/971',
'Extending CompilerContext is deprecated, please use the CompilerContextInterface instead.',
);
}
public function getProjectNode(): ProjectNode
{
return $this->projectNode;
}
public function getDocumentNode(): DocumentNode
{
if (!isset($this->shadowTree)) {
throw new Exception('DocumentNode must be set in compiler context');
}
return $this->shadowTree->getRoot()->getNode();
}
public function withDocumentShadowTree(DocumentNode $documentNode): static
{
$that = clone $this;
$that->shadowTree = TreeNode::createFromDocument($documentNode);
return $that;
}
/** @param TreeNode<Node> $shadowTree */
public function withShadowTree(TreeNode $shadowTree): static
{
$that = clone $this;
$that->shadowTree = $shadowTree;
return $that;
}
/** @return TreeNode<Node> */
public function getShadowTree(): TreeNode
{
if (!isset($this->shadowTree)) {
throw new Exception('DocumentNode must be set in compiler context');
}
return $this->shadowTree;
}
/** @return array<string, string> */
public function getLoggerInformation(): array
{
if (!isset($this->shadowTree)) {
return [];
}
return [...$this->getDocumentNode()->getLoggerInformation()];
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler;
use phpDocumentor\Guides\Compiler\ShadowTree\TreeNode;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\ProjectNode;
interface CompilerContextInterface
{
public function getProjectNode(): ProjectNode;
public function getDocumentNode(): DocumentNode;
public function withDocumentShadowTree(DocumentNode $documentNode): self;
/** @param TreeNode<Node> $shadowTree */
public function withShadowTree(TreeNode $shadowTree): self;
/** @return TreeNode<Node> */
public function getShadowTree(): TreeNode;
/** @return array<string, string> */
public function getLoggerInformation(): array;
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler;
use phpDocumentor\Guides\Nodes\DocumentNode;
interface CompilerPass
{
/**
* @param DocumentNode[] $documents
*
* @return DocumentNode[]
*/
public function run(array $documents, CompilerContext $compilerContext): array;
public function getPriority(): int;
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler;
use phpDocumentor\Guides\Compiler\NodeTransformers\NodeTransformerFactory;
use phpDocumentor\Guides\Compiler\ShadowTree\TreeNode;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Nodes\Node;
final class DocumentNodeTraverser
{
public function __construct(
private readonly NodeTransformerFactory $nodeTransformerFactory,
private readonly int $priority,
) {
}
public function traverse(DocumentNode $node, CompilerContext $compilerContext): Node|null
{
foreach ($this->nodeTransformerFactory->getTransformers() as $transformer) {
if ($transformer->getPriority() !== $this->priority) {
continue;
}
$this->traverseForTransformer($transformer, $compilerContext->getShadowTree(), $compilerContext);
}
return $compilerContext->getShadowTree()->getNode();
}
/**
* @param NodeTransformer<Node> $transformer
* @param TreeNode<Node>|TreeNode<DocumentNode> $shadowNode
*
* return TNode|null
*/
private function traverseForTransformer(
NodeTransformer $transformer,
TreeNode $shadowNode,
CompilerContext $compilerContext,
): void {
$node = $shadowNode->getNode();
$supports = $transformer->supports($node);
if ($supports) {
$transformed = $transformer->enterNode($node, $compilerContext);
if ($transformed !== $node) {
$shadowNode->getParent()?->replaceChild($node, $transformed);
}
}
foreach ($shadowNode->getChildren() as $shadowChild) {
$this->traverseForTransformer($transformer, $shadowChild, $compilerContext->withShadowTree($shadowChild));
}
if (!$supports) {
return;
}
$transformed = $transformer->leaveNode($node, $compilerContext);
if ($transformed !== null) {
if ($transformed !== $node) {
$shadowNode->getParent()?->replaceChild($node, $transformed);
}
return;
}
$shadowNode->getParent()?->removeChild($node);
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler;
use phpDocumentor\Guides\Nodes\Node;
/** @template T of Node */
interface NodeTransformer
{
/**
* @param T $node
*
* @return T
*/
public function enterNode(Node $node, CompilerContext $compilerContext): Node;
/**
* @param T $node
*
* @return T|null
*/
public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null;
/** @psalm-assert-if-true T $node */
public function supports(Node $node): bool;
/**
* The higher the priority the earlier the NodeTransformer is executed.
*/
public function getPriority(): int;
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Nodes\Inline\CitationInlineNode;
use phpDocumentor\Guides\Nodes\Node;
/** @implements NodeTransformer<Node> */
final class CitationInlineNodeTransformer implements NodeTransformer
{
public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node
{
if ($node instanceof CitationInlineNode) {
$internalTarget = $compilerContext->getProjectNode()->getCitationTarget($node->getName());
$node->setInternalTarget($internalTarget);
}
return $node;
}
public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null
{
return $node;
}
public function supports(Node $node): bool
{
return $node instanceof CitationInlineNode;
}
public function getPriority(): int
{
// After CitationTargetTransformer
return 2000;
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Meta\CitationTarget;
use phpDocumentor\Guides\Nodes\CitationNode;
use phpDocumentor\Guides\Nodes\Node;
/** @implements NodeTransformer<Node> */
final class CitationTargetTransformer implements NodeTransformer
{
public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node
{
if ($node instanceof CitationNode) {
$compilerContext->getProjectNode()->addCitationTarget(
new CitationTarget(
$compilerContext->getDocumentNode()->getFilePath(),
$node->getAnchor(),
$node->getName(),
),
);
}
return $node;
}
public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null
{
return $node;
}
public function supports(Node $node): bool
{
return $node instanceof CitationNode;
}
public function getPriority(): int
{
return 20_000;
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Nodes\ClassNode;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Nodes\Node;
use function array_merge;
/**
* @implements NodeTransformer<Node>
*
* The "class" directive sets the "classes" attribute value on its content or on the first immediately following
* non-comment element. https://docutils.sourceforge.io/docs/ref/rst/directives.html#class
*/
final class ClassNodeTransformer implements NodeTransformer
{
/** @var string[] */
private array $classes = [];
public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node
{
if ($node instanceof DocumentNode) {
// unset classes when entering the next document
$this->classes = [];
}
if ($node instanceof ClassNode) {
$this->classes = $node->getClasses();
}
if ($this->classes !== [] && !$node instanceof ClassNode) {
$node->setClasses(array_merge($node->getClasses(), $this->classes));
// Unset the classes after applied to the first direct successor
$this->classes = [];
}
return $node;
}
public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null
{
if ($node instanceof ClassNode) {
//Remove the class node from the tree.
return null;
}
return $node;
}
public function supports(Node $node): bool
{
// Every node can have a class attached to it, however the node renderer decides on if to render the class
return true;
}
public function getPriority(): int
{
return 40_000;
}
}

View File

@@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Exception\DuplicateLinkAnchorException;
use phpDocumentor\Guides\Meta\InternalTarget;
use phpDocumentor\Guides\Nodes\AnchorNode;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Nodes\LinkTargetNode;
use phpDocumentor\Guides\Nodes\MultipleLinkTargetsNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\OptionalLinkTargetsNode;
use phpDocumentor\Guides\Nodes\PrefixedLinkTargetNode;
use phpDocumentor\Guides\Nodes\SectionNode;
use phpDocumentor\Guides\ReferenceResolvers\AnchorNormalizer;
use Psr\Log\LoggerInterface;
use SplStack;
use Webmozart\Assert\Assert;
use function sprintf;
/** @implements NodeTransformer<DocumentNode|AnchorNode|SectionNode> */
final class CollectLinkTargetsTransformer implements NodeTransformer
{
/** @var SplStack<DocumentNode> */
private readonly SplStack $documentStack;
public function __construct(
private readonly AnchorNormalizer $anchorReducer,
private LoggerInterface|null $logger = null,
) {
/*
* TODO: remove stack here, as we should not have sub documents in this way, sub documents are
* now produced by the {@see \phpDocumentor\Guides\RestructuredText\MarkupLanguageParser::getSubParser}
* as this works right now in isolation includes do not work as they should.
*/
$this->documentStack = new SplStack();
}
public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node
{
if ($node instanceof DocumentNode) {
$this->documentStack->push($node);
return $node;
}
if ($node instanceof AnchorNode) {
$currentDocument = $compilerContext->getDocumentNode();
$parentSection = $compilerContext->getShadowTree()->getParent()?->getNode();
$title = null;
if ($parentSection instanceof SectionNode) {
$title = $parentSection->getTitle()->toString();
}
$anchorName = $this->anchorReducer->reduceAnchor($node->toString());
try {
$compilerContext->getProjectNode()->addLinkTarget(
$anchorName,
new InternalTarget(
$currentDocument->getFilePath(),
$node->toString(),
$title,
),
);
} catch (DuplicateLinkAnchorException $exception) {
$this->logger?->warning($exception->getMessage(), $compilerContext->getLoggerInformation());
}
return $node;
}
if ($node instanceof SectionNode) {
$currentDocument = $this->documentStack->top();
Assert::notNull($currentDocument);
$anchorName = $node->getId();
foreach ($node->getChildren() as $childNode) {
if ($childNode instanceof AnchorNode) {
$anchorName = $childNode->getValue();
break;
}
}
try {
$compilerContext->getProjectNode()->addLinkTarget(
$node->getId(),
new InternalTarget(
$currentDocument->getFilePath(),
$anchorName,
$node->getLinkText(),
SectionNode::STD_TITLE,
),
);
} catch (DuplicateLinkAnchorException $exception) {
$this->logger?->warning($exception->getMessage(), $compilerContext->getLoggerInformation());
}
return $node;
}
if ($node instanceof LinkTargetNode) {
if ($node instanceof OptionalLinkTargetsNode && $node->isNoindex()) {
return $node;
}
$currentDocument = $this->documentStack->top();
Assert::notNull($currentDocument);
$anchor = $this->anchorReducer->reduceAnchor($node->getId());
$prefix = '';
if ($node instanceof PrefixedLinkTargetNode) {
$prefix = $node->getPrefix();
}
$this->addLinkTargetToProject(
$compilerContext,
new InternalTarget(
$currentDocument->getFilePath(),
$anchor,
$node->getLinkText(),
$node->getLinkType(),
$prefix,
),
);
if ($node instanceof MultipleLinkTargetsNode) {
foreach ($node->getAdditionalIds() as $id) {
$anchor = $this->anchorReducer->reduceAnchor($id);
$this->addLinkTargetToProject(
$compilerContext,
new InternalTarget(
$currentDocument->getFilePath(),
$anchor,
$node->getLinkText(),
$node->getLinkType(),
),
);
}
}
}
return $node;
}
public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null
{
if ($node instanceof DocumentNode) {
$this->documentStack->pop();
}
return $node;
}
public function supports(Node $node): bool
{
return $node instanceof DocumentNode || $node instanceof AnchorNode || $node instanceof LinkTargetNode;
}
public function getPriority(): int
{
// After MetasPass
return 5000;
}
private function addLinkTargetToProject(CompilerContextInterface $compilerContext, InternalTarget $internalTarget): void
{
if ($compilerContext->getProjectNode()->hasInternalTarget($internalTarget->getAnchor(), $internalTarget->getLinkType())) {
$otherLink = $compilerContext->getProjectNode()->getInternalTarget($internalTarget->getAnchor(), $internalTarget->getLinkType());
$this->logger?->warning(
sprintf(
'Duplicate anchor "%s" for link type "%s" in document "%s". The anchor is already used at "%s"',
$internalTarget->getAnchor(),
$internalTarget->getLinkType(),
$compilerContext->getDocumentNode()->getFilePath(),
$otherLink?->getDocumentPath(),
),
$compilerContext->getLoggerInformation(),
);
return;
}
try {
$compilerContext->getProjectNode()->addLinkTarget(
$internalTarget->getAnchor(),
$internalTarget,
);
} catch (DuplicateLinkAnchorException $exception) {
$this->logger?->warning($exception->getMessage(), $compilerContext->getLoggerInformation());
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Nodes\Node;
use function array_map;
use function array_unique;
use function sort;
final class CustomNodeTransformerFactory implements NodeTransformerFactory
{
/** @param iterable<NodeTransformer<Node>> $transformers */
public function __construct(private readonly iterable $transformers)
{
}
/** @return iterable<NodeTransformer<Node>> */
public function getTransformers(): iterable
{
return $this->transformers;
}
/** @return int[] */
public function getPriorities(): array
{
$transformers = [...$this->transformers];
$priorites = array_map(
static fn (NodeTransformer $transformer): int => $transformer->getPriority(),
$transformers,
);
sort($priorites);
$priorites = array_unique($priorites);
return $priorites;
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Nodes\DocumentBlockNode;
use phpDocumentor\Guides\Nodes\Menu\TocNode;
use phpDocumentor\Guides\Nodes\Node;
/**
* @implements NodeTransformer<Node>
*
* The "class" directive sets the "classes" attribute value on its content or on the first immediately following
* non-comment element. https://docutils.sourceforge.io/docs/ref/rst/directives.html#class
*/
final class DocumentBlockNodeTransformer implements NodeTransformer
{
public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node
{
return $node;
}
public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null
{
if ($node instanceof DocumentBlockNode) {
$children = [];
foreach ($node->getValue() as $child) {
if ($child instanceof TocNode) {
$child = $child->withOptions([...$child->getOptions(), 'menu' => $node->getIdentifier()]);
}
$child = $child->withOptions([...$child->getOptions(), 'documentBlock' => $node->getIdentifier()]);
$children[] = $child;
}
$compilerContext->getDocumentNode()->addDocumentPart($node->getIdentifier(), $children);
// Remove the node as it should not be rendered in the defined place but
// wherever the theme defines
return null;
}
return $node;
}
public function supports(Node $node): bool
{
return $node instanceof DocumentBlockNode;
}
public function getPriority(): int
{
return 3000;
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Event\ModifyDocumentEntryAdditionalData;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\TitleNode;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use function assert;
use function is_string;
/** @implements NodeTransformer<Node> */
final class DocumentEntryRegistrationTransformer implements NodeTransformer
{
public function __construct(
private readonly LoggerInterface $logger,
private readonly EventDispatcherInterface|null $eventDispatcher = null,
) {
}
public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node
{
return $node;
}
public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null
{
if (!$node instanceof DocumentNode) {
return $node;
}
if ($node->getTitle() === null && !$node->isOrphan()) {
$this->logger->warning('Document has no title', $compilerContext->getLoggerInformation());
}
$additionalData = [];
if (is_string($node->getNavigationTitle())) {
$additionalData['navigationTitle'] = TitleNode::fromString($node->getNavigationTitle());
}
if ($this->eventDispatcher !== null) {
$event = $this->eventDispatcher->dispatch(new ModifyDocumentEntryAdditionalData($additionalData, $node, $compilerContext));
assert($event instanceof ModifyDocumentEntryAdditionalData);
$additionalData = $event->getAdditionalData();
}
$entry = new DocumentEntryNode(
$node->getFilePath(),
$node->getTitle() ?? TitleNode::emptyNode(),
$node->isRoot(),
$additionalData,
$node->isOrphan(),
);
$compilerContext->getProjectNode()->addDocumentEntry($entry);
return $node->setDocumentEntry($entry);
}
public function supports(Node $node): bool
{
return $node instanceof DocumentNode;
}
public function getPriority(): int
{
// Before TocNodeWithDocumentEntryTransformer and SectionEntryRegistrationTransformer
return 5000;
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Meta\FootnoteTarget;
use phpDocumentor\Guides\Nodes\FootnoteNode;
use phpDocumentor\Guides\Nodes\Node;
/** @implements NodeTransformer<Node> */
final class FootNodeNamedTransformer implements NodeTransformer
{
public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node
{
if ($node instanceof FootnoteNode && $this->supports($node)) {
$number = $compilerContext->getDocumentNode()->addFootnoteTarget(new FootnoteTarget(
$compilerContext->getDocumentNode()->getFilePath(),
$node->getAnchor(),
$node->getName(),
0,
));
$node->setNumber($number);
}
return $node;
}
public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null
{
return $node;
}
public function supports(Node $node): bool
{
return $node instanceof FootnoteNode && $node->getNumber() <= 0;
}
public function getPriority(): int
{
// must be run *after* FootNodeNumberedTransformer
return 20_000;
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Meta\FootnoteTarget;
use phpDocumentor\Guides\Nodes\FootnoteNode;
use phpDocumentor\Guides\Nodes\Node;
/** @implements NodeTransformer<Node> */
final class FootNodeNumberedTransformer implements NodeTransformer
{
public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node
{
if ($node instanceof FootnoteNode && $this->supports($node)) {
$compilerContext->getDocumentNode()->addFootnoteTarget(new FootnoteTarget(
$compilerContext->getDocumentNode()->getFilePath(),
$node->getAnchor(),
'',
$node->getNumber(),
));
}
return $node;
}
public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null
{
return $node;
}
public function supports(Node $node): bool
{
return $node instanceof FootnoteNode && $node->getNumber() > 0;
}
public function getPriority(): int
{
// must be run *before* FootNodeNamedTransformer
return 30_000;
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Nodes\Inline\FootnoteInlineNode;
use phpDocumentor\Guides\Nodes\Node;
/** @implements NodeTransformer<Node> */
final class FootnoteInlineNodeTransformer implements NodeTransformer
{
public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node
{
if ($node instanceof FootnoteInlineNode) {
if ($node->getNumber() > 0) {
$internalTarget = $compilerContext->getDocumentNode()->getFootnoteTarget($node->getNumber());
} elseif ($node->getName() !== '') {
$internalTarget = $compilerContext->getDocumentNode()->getFootnoteTargetByName($node->getName());
} else {
$internalTarget = $compilerContext->getDocumentNode()->getFootnoteTargetAnonymous();
}
$node->setInternalTarget($internalTarget);
}
return $node;
}
public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null
{
return $node;
}
public function supports(Node $node): bool
{
return $node instanceof FootnoteInlineNode;
}
public function getPriority(): int
{
// After FooternoteTargetTransformer
return 2000;
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Nodes\ListItemNode;
use phpDocumentor\Guides\Nodes\ListNode;
use phpDocumentor\Guides\Nodes\Node;
use Psr\Log\LoggerInterface;
use function assert;
/** @implements NodeTransformer<ListNode> */
final class ListNodeTransformer implements NodeTransformer
{
public function __construct(
private readonly LoggerInterface $logger,
) {
}
public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node
{
return $node;
}
public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null
{
assert($node instanceof ListNode);
foreach ($node->getChildren() as $listItemNode) {
assert($listItemNode instanceof ListItemNode);
if (!empty($listItemNode->getChildren())) {
continue;
}
$this->logger->warning('List item without content', $compilerContext->getLoggerInformation());
}
return $node;
}
public function supports(Node $node): bool
{
return $node instanceof ListNode;
}
public function getPriority(): int
{
return 1000;
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers;
use Exception;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode;
use phpDocumentor\Guides\Nodes\Menu\MenuNode;
use phpDocumentor\Guides\Nodes\Node;
use Psr\Log\LoggerInterface;
use function assert;
use function count;
/** @implements NodeTransformer<MenuEntryNode> */
abstract class AbstractMenuEntryNodeTransformer implements NodeTransformer
{
public function __construct(
protected readonly LoggerInterface $logger,
) {
}
final public function enterNode(Node $node, CompilerContextInterface $compilerContext): MenuEntryNode
{
return $node;
}
/** @param MenuEntryNode $node */
final public function leaveNode(Node $node, CompilerContextInterface $compilerContext): MenuEntryNode|null
{
assert($node instanceof MenuEntryNode);
$currentMenuShaddow = $compilerContext->getShadowTree()->getParent();
while ($currentMenuShaddow !== null && !$currentMenuShaddow->getNode() instanceof MenuNode) {
$currentMenuShaddow = $currentMenuShaddow->getParent();
}
$currentMenu = $currentMenuShaddow?->getNode();
if (!$currentMenu instanceof MenuNode) {
throw new Exception('A MenuEntryNode must be attached to a MenuNode');
}
$menuEntries = $this->handleMenuEntry($currentMenu, $node, $compilerContext);
if (count($menuEntries) === 0) {
return null;
}
if (count($menuEntries) === 1) {
return $menuEntries[0];
}
foreach ($menuEntries as $menuEntry) {
$compilerContext->getShadowTree()->getParent()?->addChild($menuEntry);
}
return null;
}
/** @return list<MenuEntryNode> */
abstract protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNode, CompilerContextInterface $compilerContext): array;
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Nodes\DocumentTree\SectionEntryNode;
use phpDocumentor\Guides\Nodes\Menu\ContentMenuNode;
use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode;
use phpDocumentor\Guides\Nodes\Menu\MenuNode;
use phpDocumentor\Guides\Nodes\Menu\SectionMenuEntryNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\SectionNode;
use function assert;
use const PHP_INT_MAX;
final class ContentsMenuEntryNodeTransformer extends AbstractMenuEntryNodeTransformer
{
use SubSectionHierarchyHandler;
private const DEFAULT_MAX_LEVELS = PHP_INT_MAX;
public function supports(Node $node): bool
{
return $node instanceof SectionMenuEntryNode;
}
/** @return list<MenuEntryNode> */
protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNode, CompilerContextInterface $compilerContext): array
{
if (!$currentMenu instanceof ContentMenuNode) {
return [$entryNode];
}
assert($entryNode instanceof SectionMenuEntryNode);
$depth = (int) $currentMenu->getOption('depth', self::DEFAULT_MAX_LEVELS - 1) + 1;
$documentEntry = $compilerContext->getDocumentNode()->getDocumentEntry();
if ($currentMenu->isLocal()) {
$sectionNode = $compilerContext->getShadowTree()->getParent()?->getParent()?->getNode();
if (!$sectionNode instanceof SectionNode) {
$this->logger->error('Section of contents directive not found. ', $compilerContext->getLoggerInformation());
return [];
}
$sectionEntry = $documentEntry->findSectionEntry($sectionNode);
if (!$sectionEntry instanceof SectionEntryNode) {
$this->logger->error('Section of contents directive not found. ', $compilerContext->getLoggerInformation());
return [];
}
$newEntryNode = new SectionMenuEntryNode(
$documentEntry->getFile(),
$entryNode->getValue() ?? $sectionEntry->getTitle(),
1,
$sectionEntry->getId(),
);
$this->addSubSections($newEntryNode, $sectionEntry, $documentEntry, 1, $depth);
} else {
$newEntryNode = new SectionMenuEntryNode(
$documentEntry->getFile(),
$entryNode->getValue() ?? $documentEntry->getTitle(),
1,
);
$this->addSubSectionsToMenuEntries($documentEntry, $newEntryNode, $depth);
}
return $newEntryNode->getSections();
}
public function getPriority(): int
{
// After DocumentEntryTransformer
return 4500;
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Nodes\DocumentTree\ExternalEntryNode;
use phpDocumentor\Guides\Nodes\Menu\ExternalMenuEntryNode;
use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode;
use phpDocumentor\Guides\Nodes\Menu\MenuNode;
use phpDocumentor\Guides\Nodes\Menu\TocNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\TitleNode;
use Psr\Log\LoggerInterface;
use function assert;
final class ExternalMenuEntryNodeTransformer extends AbstractMenuEntryNodeTransformer
{
use MenuEntryManagement;
use SubSectionHierarchyHandler;
public function __construct(
LoggerInterface $logger,
) {
parent::__construct($logger);
}
public function supports(Node $node): bool
{
return $node instanceof ExternalMenuEntryNode;
}
/** @return list<MenuEntryNode> */
protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNode, CompilerContextInterface $compilerContext): array
{
assert($entryNode instanceof ExternalMenuEntryNode);
$newEntryNode = new ExternalEntryNode(
$entryNode->getUrl(),
($entryNode->getValue() ?? TitleNode::emptyNode())->toString(),
);
if ($currentMenu instanceof TocNode) {
$this->attachDocumentEntriesToParents([$newEntryNode], $compilerContext, '');
}
return [$entryNode];
}
public function getPriority(): int
{
// After DocumentEntryTransformer
return 4500;
}
}

View File

@@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Nodes\Menu\GlobMenuEntryNode;
use phpDocumentor\Guides\Nodes\Menu\InternalMenuEntryNode;
use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode;
use phpDocumentor\Guides\Nodes\Menu\MenuNode;
use phpDocumentor\Guides\Nodes\Menu\TocNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\TitleNode;
use function array_pop;
use function assert;
use function explode;
use function implode;
use function in_array;
use function is_string;
use function preg_match;
use function preg_replace;
final class GlobMenuEntryNodeTransformer extends AbstractMenuEntryNodeTransformer
{
use MenuEntryManagement;
use SubSectionHierarchyHandler;
// Setting a default level prevents PHP errors in case of circular references
private const DEFAULT_MAX_LEVELS = 10;
/** @return list<MenuEntryNode> */
protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNode, CompilerContextInterface $compilerContext): array
{
assert($entryNode instanceof GlobMenuEntryNode);
$maxDepth = (int) $currentMenu->getOption('maxdepth', self::DEFAULT_MAX_LEVELS);
$documentEntries = $compilerContext->getProjectNode()->getAllDocumentEntries();
$currentPath = $compilerContext->getDocumentNode()->getFilePath();
$globExclude = explode(',', $currentMenu->getOption('globExclude') . '');
$menuEntries = [];
foreach ($documentEntries as $documentEntry) {
if ($documentEntry->isOrphan()) {
continue;
}
if (
!self::matches($documentEntry->getFile(), $entryNode, $currentPath, $globExclude)
) {
continue;
}
if ($currentMenu instanceof TocNode && self::isCurrent($documentEntry, $currentPath)) {
// TocNodes do not select the current page in glob mode. In a menu we might want to display it
continue;
}
foreach ($currentMenu->getChildren() as $currentMenuEntry) {
if ($currentMenuEntry instanceof InternalMenuEntryNode && $currentMenuEntry->getUrl() === $documentEntry->getFile()) {
// avoid duplicates
continue 2;
}
}
$titleNode = $documentEntry->getTitle();
$navigationTitle = $documentEntry->getAdditionalData('navigationTitle');
if ($navigationTitle instanceof TitleNode) {
$titleNode = $navigationTitle;
}
$documentEntriesInTree[] = $documentEntry;
$newEntryNode = new InternalMenuEntryNode(
$documentEntry->getFile(),
$titleNode,
[],
false,
1,
'',
self::isInRootline($documentEntry, $compilerContext->getDocumentNode()->getDocumentEntry()),
self::isCurrent($documentEntry, $currentPath),
);
if (!$currentMenu->hasOption('titlesonly') && $maxDepth > 1) {
$this->addSubSectionsToMenuEntries($documentEntry, $newEntryNode, $maxDepth - 1);
}
if ($currentMenu instanceof TocNode) {
$this->attachDocumentEntriesToParents($documentEntriesInTree, $compilerContext, $currentPath);
}
$menuEntries[] = $newEntryNode;
}
return $menuEntries;
}
public function supports(Node $node): bool
{
return $node instanceof GlobMenuEntryNode;
}
public function getPriority(): int
{
// After GlobMenuEntryNodeTransformer
// Before SubInternalMenuEntry
return 4000;
}
/** @param String[] $globExclude */
private static function matches(string $actualFile, GlobMenuEntryNode $parsedMenuEntryNode, string $currentFile, array $globExclude): bool
{
$expectedFile = $parsedMenuEntryNode->getUrl();
if (self::isAbsoluteFile($expectedFile)) {
if ($expectedFile === '/' . $actualFile) {
return true;
}
return self::isGlob($actualFile, $currentFile, $expectedFile, '/', $globExclude);
}
$current = explode('/', $currentFile);
array_pop($current);
$current[] = $expectedFile;
$absoluteExpectedFile = implode('/', $current);
if ($absoluteExpectedFile === $actualFile) {
return true;
}
return self::isGlob($actualFile, $currentFile, $absoluteExpectedFile, '', $globExclude);
}
/** @param String[] $globExclude */
private static function isGlob(string $documentEntryFile, string $currentPath, string $file, string $prefix, array $globExclude): bool
{
if (!in_array($documentEntryFile, $globExclude, true)) {
$file = preg_replace('/(?<!\*)\*(?!\*)/', '[^\/]*', $file);
assert(is_string($file));
$file = preg_replace('/\*{2}/', '.*', $file);
$pattern = '`^' . $file . '$`';
return preg_match($pattern, $prefix . $documentEntryFile) > 0;
}
return false;
}
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Nodes\Menu\InternalMenuEntryNode;
use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode;
use phpDocumentor\Guides\Nodes\Menu\MenuNode;
use phpDocumentor\Guides\Nodes\Menu\TocNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\TitleNode;
use phpDocumentor\Guides\ReferenceResolvers\DocumentNameResolverInterface;
use Psr\Log\LoggerInterface;
use function array_pop;
use function assert;
use function explode;
use function implode;
use function sprintf;
final class InternalMenuEntryNodeTransformer extends AbstractMenuEntryNodeTransformer
{
use MenuEntryManagement;
use SubSectionHierarchyHandler;
// Setting a default level prevents PHP errors in case of circular references
private const DEFAULT_MAX_LEVELS = 10;
public function __construct(
LoggerInterface $logger,
private readonly DocumentNameResolverInterface $documentNameResolver,
) {
parent::__construct($logger);
}
public function supports(Node $node): bool
{
return $node instanceof InternalMenuEntryNode;
}
/** @return list<MenuEntryNode> */
protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNode, CompilerContextInterface $compilerContext): array
{
assert($entryNode instanceof InternalMenuEntryNode);
$documentEntries = $compilerContext->getProjectNode()->getAllDocumentEntries();
$currentPath = $compilerContext->getDocumentNode()->getFilePath();
$maxDepth = (int) $currentMenu->getOption('maxdepth', self::DEFAULT_MAX_LEVELS);
foreach ($documentEntries as $documentEntry) {
if (
!self::matches($documentEntry->getFile(), $entryNode, $currentPath)
) {
continue;
}
$titleNode = $documentEntry->getTitle();
$navigationTitle = $documentEntry->getAdditionalData('navigationTitle');
if ($navigationTitle instanceof TitleNode) {
$titleNode = $navigationTitle;
}
if ($entryNode->getValue() instanceof TitleNode) {
$titleNode = $entryNode->getValue();
}
$documentEntriesInTree[] = $documentEntry;
$newEntryNode = new InternalMenuEntryNode(
$documentEntry->getFile(),
$titleNode,
[],
false,
1,
'',
self::isInRootline($documentEntry, $compilerContext->getDocumentNode()->getDocumentEntry()),
self::isCurrent($documentEntry, $currentPath),
);
if (!$currentMenu->hasOption('titlesonly') && $maxDepth > 1) {
$this->addSubSectionsToMenuEntries($documentEntry, $newEntryNode, $maxDepth);
}
if ($currentMenu instanceof TocNode) {
$this->attachDocumentEntriesToParents($documentEntriesInTree, $compilerContext, $currentPath);
}
return [$newEntryNode];
}
$this->logger->warning(sprintf('Menu entry "%s" was not found in the document tree. Ignoring it. ', $entryNode->getUrl()), $compilerContext->getLoggerInformation());
return [];
}
private function matches(string $actualFile, InternalMenuEntryNode $parsedMenuEntryNode, string $currentFile): bool
{
$expectedFile = $parsedMenuEntryNode->getUrl();
if (self::isAbsoluteFile($expectedFile)) {
return $expectedFile === '/' . $actualFile;
}
$current = explode('/', $currentFile);
array_pop($current);
$absoluteExpectedFile = $this->documentNameResolver->canonicalUrl(implode('/', $current), $expectedFile);
return $absoluteExpectedFile === $actualFile;
}
public function getPriority(): int
{
// After DocumentEntryTransformer
return 4500;
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode;
use phpDocumentor\Guides\Nodes\DocumentTree\ExternalEntryNode;
use function sprintf;
use function str_starts_with;
trait MenuEntryManagement
{
/** @param array<DocumentEntryNode|ExternalEntryNode> $entryNodes */
private function attachDocumentEntriesToParents(
array $entryNodes,
CompilerContextInterface $compilerContext,
string $currentPath,
): void {
foreach ($entryNodes as $entryNode) {
if ($entryNode instanceof DocumentEntryNode) {
if (($entryNode->isRoot() || $currentPath === $entryNode->getFile())) {
// The root page may not be attached to any other
continue;
}
if ($entryNode->getParent() !== null && $entryNode->getParent() !== $compilerContext->getDocumentNode()->getDocumentEntry()) {
$this->logger->warning(sprintf(
'Document %s has been added to parents %s and %s. The `toctree` directive changes the '
. 'position of documents in the document tree. Use the `menu` directive to only display a menu without changing the document tree.',
$entryNode->getFile(),
$entryNode->getParent()->getFile(),
$compilerContext->getDocumentNode()->getDocumentEntry()->getFile(),
), $compilerContext->getLoggerInformation());
}
if ($entryNode->getParent() !== null) {
continue;
}
}
$entryNode->setParent($compilerContext->getDocumentNode()->getDocumentEntry());
$compilerContext->getDocumentNode()->getDocumentEntry()->addChild($entryNode);
}
}
private static function isInRootline(DocumentEntryNode $menuEntry, DocumentEntryNode $currentDoc): bool
{
return $menuEntry->getFile() === $currentDoc->getFile()
|| ($currentDoc->getParent() !== null
&& self::isInRootline($menuEntry, $currentDoc->getParent()));
}
private static function isCurrent(DocumentEntryNode $menuEntry, string $currentPath): bool
{
return $menuEntry->getFile() === $currentPath;
}
private static function isAbsoluteFile(string $expectedFile): bool
{
return str_starts_with($expectedFile, '/');
}
}

View File

@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Exception\DocumentEntryNotFound;
use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode;
use phpDocumentor\Guides\Nodes\DocumentTree\ExternalEntryNode;
use phpDocumentor\Guides\Nodes\Menu\ExternalMenuEntryNode;
use phpDocumentor\Guides\Nodes\Menu\InternalMenuEntryNode;
use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode;
use phpDocumentor\Guides\Nodes\Menu\MenuNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\TitleNode;
use function assert;
use function sprintf;
final class SubInternalMenuEntryNodeTransformer extends AbstractMenuEntryNodeTransformer
{
use MenuEntryManagement;
use SubSectionHierarchyHandler;
// Setting a default level prevents PHP errors in case of circular references
private const DEFAULT_MAX_LEVELS = 10;
public function supports(Node $node): bool
{
return $node instanceof InternalMenuEntryNode;
}
/** @return list<MenuEntryNode> */
protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNode, CompilerContextInterface $compilerContext): array
{
assert($entryNode instanceof InternalMenuEntryNode);
$maxDepth = (int) $currentMenu->getOption('maxdepth', self::DEFAULT_MAX_LEVELS);
try {
$documentEntryOfMenuEntry = $compilerContext->getProjectNode()->getDocumentEntry($entryNode->getUrl());
} catch (DocumentEntryNotFound) {
$this->logger->warning(sprintf('Menu entry "%s" was not found in the document tree. Ignoring it. ', $entryNode->getUrl()), $compilerContext->getLoggerInformation());
return [];
}
$this->addSubEntries($currentMenu, $compilerContext, $entryNode, $documentEntryOfMenuEntry, $entryNode->getLevel() + 1, $maxDepth);
return [$entryNode];
}
public function getPriority(): int
{
// After MenuEntries are resolved
return 3000;
}
private function addSubEntries(
MenuNode $currentMenu,
CompilerContextInterface $compilerContext,
InternalMenuEntryNode $sectionMenuEntry,
DocumentEntryNode $documentEntry,
int $currentLevel,
int $maxDepth,
): void {
if ($maxDepth < $currentLevel) {
return;
}
foreach ($documentEntry->getMenuEntries() as $subEntryNode) {
if ($subEntryNode instanceof DocumentEntryNode) {
$titleNode = $subEntryNode->getTitle();
$navigationTitle = $subEntryNode->getAdditionalData('navigationTitle');
if ($navigationTitle instanceof TitleNode) {
$titleNode = $navigationTitle;
}
$subMenuEntry = new InternalMenuEntryNode(
$subEntryNode->getFile(),
$titleNode,
[],
false,
$currentLevel,
'',
self::isInRootline($subEntryNode, $compilerContext->getDocumentNode()->getDocumentEntry()),
self::isCurrent($subEntryNode, $compilerContext->getDocumentNode()->getFilePath()),
);
if (!$currentMenu->hasOption('titlesonly') && $maxDepth - $currentLevel + 1 > 1) {
$this->addSubSectionsToMenuEntries($subEntryNode, $subMenuEntry, $maxDepth - $currentLevel + 2);
}
$sectionMenuEntry->addMenuEntry($subMenuEntry);
$this->addSubEntries($currentMenu, $compilerContext, $subMenuEntry, $subEntryNode, $currentLevel + 1, $maxDepth);
continue;
}
if (!($subEntryNode instanceof ExternalEntryNode)) {
continue;
}
$subMenuEntry = new ExternalMenuEntryNode(
$subEntryNode->getValue(),
TitleNode::fromString($subEntryNode->getTitle()),
$currentLevel,
);
$sectionMenuEntry->addMenuEntry($subMenuEntry);
}
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers;
use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode;
use phpDocumentor\Guides\Nodes\DocumentTree\SectionEntryNode;
use phpDocumentor\Guides\Nodes\Menu\InternalMenuEntryNode;
use phpDocumentor\Guides\Nodes\Menu\SectionMenuEntryNode;
use function assert;
trait SubSectionHierarchyHandler
{
private function addSubSectionsToMenuEntries(DocumentEntryNode $documentEntry, InternalMenuEntryNode|SectionMenuEntryNode $menuEntry, int $maxLevel): void
{
foreach ($documentEntry->getSections() as $section) {
// We do not add the main section as it repeats the document title
foreach ($section->getChildren() as $subSectionEntryNode) {
assert($subSectionEntryNode instanceof SectionEntryNode);
$currentLevel = $menuEntry->getLevel() + 1;
$sectionMenuEntry = new SectionMenuEntryNode(
$documentEntry->getFile(),
$subSectionEntryNode->getTitle(),
$currentLevel,
$subSectionEntryNode->getId(),
);
$menuEntry->addSection($sectionMenuEntry);
$this->addSubSections($sectionMenuEntry, $subSectionEntryNode, $documentEntry, $currentLevel, $maxLevel);
}
}
}
private function addSubSections(
SectionMenuEntryNode $sectionMenuEntry,
SectionEntryNode $sectionEntryNode,
DocumentEntryNode $documentEntry,
int $currentLevel,
int $maxLevel,
): void {
if ($currentLevel >= $maxLevel) {
return;
}
foreach ($sectionEntryNode->getChildren() as $subSectionEntryNode) {
$subSectionMenuEntry = new SectionMenuEntryNode(
$documentEntry->getFile(),
$subSectionEntryNode->getTitle(),
$currentLevel,
$subSectionEntryNode->getId(),
);
$sectionMenuEntry->addSection($subSectionMenuEntry);
$this->addSubSections(
$subSectionMenuEntry,
$subSectionEntryNode,
$documentEntry,
$currentLevel + 1,
$maxLevel,
);
}
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Nodes\Menu\MenuNode;
use phpDocumentor\Guides\Nodes\Menu\NavMenuNode;
use phpDocumentor\Guides\Nodes\Menu\TocNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Settings\SettingsManager;
use Psr\Log\LoggerInterface;
/** @implements NodeTransformer<MenuNode> */
final class TocNodeReplacementTransformer implements NodeTransformer
{
public function __construct(
private readonly LoggerInterface $logger,
private readonly SettingsManager $settingsManager,
) {
}
public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node
{
return $node;
}
public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null
{
if (!$node instanceof TocNode) {
return $node;
}
if (!$this->settingsManager->getProjectSettings()->isAutomaticMenu()) {
return $node;
}
if ($node->hasOption('hidden')) {
$this->logger->warning('The `.. toctree::` directive with option `:hidden:` is not supported in automatic-menu mode. ', $compilerContext->getLoggerInformation());
return null;
}
$this->logger->warning('The `.. toctree::` directive is not supported in automatic-menu mode. Use `.. menu::` instead. ', $compilerContext->getLoggerInformation());
$menuNode = new NavMenuNode($node->getMenuEntries());
$menuNode = $menuNode->withOptions($node->getOptions());
$menuNode = $menuNode->withCaption($node->getCaption());
return $menuNode;
}
public function supports(Node $node): bool
{
return $node instanceof TocNode;
}
public function getPriority(): int
{
return 20_000;
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Nodes\Menu\TocNode;
use phpDocumentor\Guides\Nodes\Node;
use function assert;
/** @implements NodeTransformer<TocNode> */
final class TocNodeTransformer implements NodeTransformer
{
public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node
{
assert($node instanceof TocNode);
$compilerContext->getDocumentNode()->addTocNode($node);
return $node;
}
public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null
{
return $node;
}
public function supports(Node $node): bool
{
return $node instanceof TocNode;
}
public function getPriority(): int
{
return 1000;
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers\MenuNodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContext;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Nodes\Menu\TocNode;
use phpDocumentor\Guides\Nodes\Node;
use function array_reverse;
use function is_array;
/** @implements NodeTransformer<TocNode> */
final class ToctreeSortingTransformer implements NodeTransformer
{
public function getPriority(): int
{
return 3200;
}
public function enterNode(Node $node, CompilerContext $compilerContext): Node
{
if (!$node instanceof TocNode) {
return $node;
}
if (!$node->isReversed()) {
return $node;
}
$entries = $node->getValue();
$documentEntry = $compilerContext->getDocumentNode()->getDocumentEntry();
$documentMenuEntries = $documentEntry->getMenuEntries();
if (is_array($entries)) {
$entries = array_reverse($entries);
$documentMenuEntries = array_reverse($documentMenuEntries);
}
$documentEntry->setMenuEntries($documentMenuEntries);
$node->setValue($entries);
return $node;
}
public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null
{
return $node;
}
public function supports(Node $node): bool
{
return $node instanceof TocNode;
}
}

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
use ArrayIterator;
use LogicException;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Compiler\ShadowTree\TreeNode;
use phpDocumentor\Guides\Nodes\AnchorNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\SectionNode;
use WeakMap;
/** @implements NodeTransformer<AnchorNode> */
final class MoveAnchorTransformer implements NodeTransformer
{
/** @var WeakMap<AnchorNode, true> */
private WeakMap $seen;
public function __construct()
{
$this->seen = new WeakMap();
}
public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node
{
return $node;
}
public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null
{
//When exists in seen, it means that the node has already been processed. Ignore it.
if (isset($this->seen[$node])) {
return $node;
}
$this->seen[$node] = true;
$parent = $compilerContext->getShadowTree()->getParent();
if ($parent === null) {
throw new LogicException('Node not found in shadow tree');
}
$position = $parent->findPosition($node);
if ($position === null) {
throw new LogicException('Node not found in shadow tree');
}
return $this->attemptMoveToNeighbour($parent, $position, $node);
}
public function supports(Node $node): bool
{
return $node instanceof AnchorNode;
}
public function getPriority(): int
{
return 30_000;
}
/** @param TreeNode<Node> $parent */
private function attemptMoveToNeighbour(TreeNode $parent, int $position, AnchorNode $node): AnchorNode|null
{
$current = $this->findNextSection($parent, $position);
if ($current === null) {
if ($parent->getParent() === null) {
return $node;
}
$position = $parent->getParent()->findPosition($parent->getNode());
if ($position === null) {
throw new LogicException('Node not found in shadow tree');
}
return $this->attemptMoveToNeighbour($parent->getParent(), $position, $node);
}
if ($current->getNode() instanceof SectionNode) {
$current->pushChild($node);
return null;
}
return $node;
}
/**
* @param TreeNode<Node> $parent
*
* @return TreeNode<Node>|null
*/
private function findNextSection(TreeNode $parent, int $position): TreeNode|null
{
$children = new ArrayIterator($parent->getChildren());
if ($children->count() <= $position + 1) {
return null;
}
$children->seek($position + 1);
while ($children->valid() && $children->current()->getNode() instanceof AnchorNode) {
$children->next();
}
return $children->current();
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Nodes\Node;
interface NodeTransformerFactory
{
/** @return iterable<NodeTransformer<Node>> */
public function getTransformers(): iterable;
/** @return int[] */
public function getPriorities(): array;
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContext;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode;
use phpDocumentor\Guides\Nodes\InlineCompoundNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\ParagraphNode;
use phpDocumentor\Guides\Nodes\RawNode;
use Psr\Log\LoggerInterface;
use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
use function assert;
/** @implements NodeTransformer<Node> */
final class RawNodeEscapeTransformer implements NodeTransformer
{
private HtmlSanitizer $htmlSanitizer;
public function __construct(
private readonly bool $escapeRawNodes,
private readonly LoggerInterface $logger,
HtmlSanitizerConfig $htmlSanitizerConfig,
) {
$this->htmlSanitizer = new HtmlSanitizer($htmlSanitizerConfig);
}
public function enterNode(Node $node, CompilerContext $compilerContext): Node
{
return $node;
}
public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null
{
assert($node instanceof RawNode);
if ($this->escapeRawNodes) {
$this->logger->warning('We do not support plain HTML for security reasons. Escaping all HTML ');
return new ParagraphNode([new InlineCompoundNode([new PlainTextInlineNode($node->getValue())])]);
}
if ($node->getOption('format', 'html') === 'html') {
return new RawNode($this->htmlSanitizer->sanitize($node->getValue()));
}
return $node;
}
public function supports(Node $node): bool
{
return $node instanceof RawNode;
}
public function getPriority(): int
{
return 1000;
}
}

View File

@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\SectionNode;
use phpDocumentor\Guides\Nodes\TitleNode;
use function array_pop;
use function count;
use function end;
use const PHP_INT_MAX;
/** @implements NodeTransformer<Node> */
final class SectionCreationTransformer implements NodeTransformer
{
/** @var SectionNode[] $sectionStack */
private array $sectionStack = [];
private int $firstLevel = 1;
public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node
{
if ($node instanceof DocumentNode) {
$this->firstLevel = 1;
$this->sectionStack = [];
}
if (!$compilerContext->getShadowTree()->getParent()?->getNode() instanceof DocumentNode) {
return $node;
}
if (!$node instanceof TitleNode) {
$lastSection = end($this->sectionStack);
if ($lastSection instanceof SectionNode) {
$lastSection->addChildNode($node);
}
}
return $node;
}
public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null
{
if (!$compilerContext->getShadowTree()->getParent()?->getNode() instanceof DocumentNode) {
return $node;
}
if ($node instanceof SectionNode) {
return $node;
}
if (count($this->sectionStack) === 0 && !$node instanceof TitleNode) {
return $node;
}
if (count($this->sectionStack) > 0 && $compilerContext->getShadowTree()->isLastChildOfParent()) {
$lastSection = end($this->sectionStack);
while ($lastSection?->getTitle()->getLevel() > $this->firstLevel) {
$lastSection = array_pop($this->sectionStack);
}
return $lastSection;
}
if (!$node instanceof TitleNode) {
// Remove all nodes that will be attached to a section
return null;
}
$lastSection = end($this->sectionStack);
if ($lastSection instanceof SectionNode && $node !== $lastSection->getTitle() && $node->getLevel() <= $lastSection->getTitle()->getLevel()) {
while (end($this->sectionStack) instanceof SectionNode && $node !== end($this->sectionStack)->getTitle() && $node->getLevel() <= end($this->sectionStack)->getTitle()->getLevel()) {
$lastSection = array_pop($this->sectionStack);
}
$newSection = new SectionNode($node);
// Attach the new section to the last one still on the stack if there still is one
if (end($this->sectionStack) instanceof SectionNode) {
end($this->sectionStack)->addChildNode($newSection);
}
$this->pushNewSectionToStack($newSection);
return $lastSection?->getTitle()->getLevel() <= $this->firstLevel ? $lastSection : null;
}
$newSection = new SectionNode($node);
if ($lastSection instanceof SectionNode) {
$lastSection->addChildNode($newSection);
}
$this->pushNewSectionToStack($newSection);
return null;
}
public function supports(Node $node): bool
{
return true;
}
public function getPriority(): int
{
// Should run as first transformer
return PHP_INT_MAX;
}
/**
* Pushes the new section to the stack.
*
* The stack is used to track the current level of nodes and adding child
* nodes to the section. As not all documentation formats are using the
* correct level of title nodes we need to track the level of the first
* title node to determine the correct level of the section.
*/
private function pushNewSectionToStack(SectionNode $newSection): void
{
if (count($this->sectionStack) === 0) {
$this->firstLevel = $newSection->getTitle()->getLevel();
}
$this->sectionStack[] = $newSection;
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Nodes\DocumentTree\SectionEntryNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\SectionNode;
use function array_pop;
use function assert;
use function count;
use function end;
/** @implements NodeTransformer<Node> */
final class SectionEntryRegistrationTransformer implements NodeTransformer
{
/** @var SectionEntryNode[] $sectionStack */
private array $sectionStack = [];
public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node
{
if (!$node instanceof SectionNode) {
return $node;
}
$sectionEntryNode = new SectionEntryNode($node->getTitle());
if (count($this->sectionStack) === 0) {
$compilerContext->getDocumentNode()->getDocumentEntry()->addSection($sectionEntryNode);
} else {
$parentSection = end($this->sectionStack);
assert($parentSection instanceof SectionEntryNode);
$parentSection->addChild($sectionEntryNode);
}
$this->sectionStack[] = $sectionEntryNode;
return $node;
}
public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null
{
if (!$node instanceof SectionNode) {
return $node;
}
array_pop($this->sectionStack);
return $node;
}
public function supports(Node $node): bool
{
return $node instanceof SectionNode;
}
public function getPriority(): int
{
// After DocumentEntryRegistrationTransformer
return 4900;
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContext;
use phpDocumentor\Guides\Compiler\CompilerPass;
use phpDocumentor\Guides\Compiler\DocumentNodeTraverser;
use phpDocumentor\Guides\Nodes\DocumentNode;
use function array_filter;
/**
* The TransformerPass is a special kind of CompilerPass that traverses all documents and
* Calls the DocumentNodeTraverser for each.
*
* The TransformerPass cannot be injected as there must be one for each available priority of
* NodeTransformer.
*/
final class TransformerPass implements CompilerPass
{
public function __construct(
private readonly DocumentNodeTraverser $documentNodeTraverser,
private readonly int $priority,
) {
}
/** {@inheritDoc} */
public function run(array $documents, CompilerContext $compilerContext): array
{
foreach ($documents as $key => $document) {
if (!($document instanceof DocumentNode)) {
continue;
}
$compilerContext = $compilerContext->withDocumentShadowTree($document);
$documents[$key] = $this->documentNodeTraverser->traverse($document, $compilerContext);
}
return array_filter($documents, static fn ($document): bool => $document instanceof DocumentNode);
}
public function getPriority(): int
{
return $this->priority;
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode;
use phpDocumentor\Guides\Nodes\Inline\VariableInlineNode;
use phpDocumentor\Guides\Nodes\Node;
use Psr\Log\LoggerInterface;
/**
* @implements NodeTransformer<Node>
*
* The "class" directive sets the "classes" attribute value on its content or on the first immediately following
* non-comment element. https://docutils.sourceforge.io/docs/ref/rst/directives.html#class
*/
final class VariableInlineNodeTransformer implements NodeTransformer
{
public function __construct(
private readonly LoggerInterface $logger,
) {
}
public function enterNode(Node $node, CompilerContextInterface $compilerContext): Node
{
return $node;
}
public function leaveNode(Node $node, CompilerContextInterface $compilerContext): Node|null
{
if (!$node instanceof VariableInlineNode) {
return $node;
}
$nodeReplacement = $compilerContext->getDocumentNode()->getVariable($node->getValue(), null);
$nodeReplacement ??= $compilerContext->getProjectNode()->getVariable($node->getValue(), null);
if ($nodeReplacement instanceof Node) {
$node->setChild($nodeReplacement);
} else {
$this->logger->warning(
'No replacement was found for variable |' . $node->getValue() . '|',
$compilerContext->getLoggerInformation(),
);
$node->setChild(new PlainTextInlineNode('|' . $node->getValue() . '|'));
}
return $node;
}
public function supports(Node $node): bool
{
return $node instanceof VariableInlineNode;
}
public function getPriority(): int
{
// Late, other replacements should already have happened
return 30_000;
}
}

View File

@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\Passes;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\CompilerPass;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Settings\SettingsManager;
use Psr\Log\LoggerInterface;
use function array_pop;
use function count;
use function explode;
use function implode;
use function in_array;
use function sprintf;
final class AutomaticMenuPass implements CompilerPass
{
public function __construct(
private readonly SettingsManager $settingsManager,
private readonly LoggerInterface|null $logger = null,
) {
}
public function getPriority(): int
{
return 20; // must be run very late
}
/**
* @param DocumentNode[] $documents
*
* @return DocumentNode[]
*/
public function run(array $documents, CompilerContextInterface $compilerContext): array
{
if (!$this->settingsManager->getProjectSettings()->isAutomaticMenu()) {
return $documents;
}
$projectNode = $compilerContext->getProjectNode();
$rootDocumentEntry = $projectNode->getRootDocumentEntry();
$indexNames = explode(',', $this->settingsManager->getProjectSettings()->getIndexName());
foreach ($documents as $documentNode) {
if ($documentNode->isOrphan()) {
// Do not add orphans to the automatic menu
continue;
}
if ($documentNode->isRoot()) {
continue;
}
$filePath = $documentNode->getFilePath();
$pathParts = explode('/', $filePath);
$documentEntry = $projectNode->getDocumentEntry($filePath);
if (count($pathParts) === 1 || count($pathParts) === 2 && in_array($pathParts[1], $indexNames, true)) {
$documentEntry->setParent($rootDocumentEntry);
$rootDocumentEntry->addChild($documentEntry);
continue;
}
$fileName = array_pop($pathParts);
$path = implode('/', $pathParts);
if (in_array($fileName, $indexNames, true)) {
array_pop($pathParts);
$path = implode('/', $pathParts);
}
$parentFound = false;
foreach ($indexNames as $indexName) {
$indexFile = $path . '/' . $indexName;
$parentEntry = $projectNode->findDocumentEntry($indexFile);
if ($parentEntry === null) {
continue;
}
$documentEntry->setParent($parentEntry);
$parentEntry->addChild($documentEntry);
$parentFound = true;
break;
}
if ($parentFound) {
continue;
}
$parentEntry = $projectNode->findDocumentEntry($path);
if ($parentEntry === null) {
$this->logger?->warning(sprintf('No parent found for file "%s/%s" attaching it to the document root instead. ', $path, $fileName));
continue;
}
$documentEntry->setParent($parentEntry);
$parentEntry->addChild($documentEntry);
}
return $documents;
}
}

View File

@@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\Passes;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\CompilerPass;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode;
use phpDocumentor\Guides\Nodes\DocumentTree\EntryNode;
use phpDocumentor\Guides\Nodes\DocumentTree\ExternalEntryNode;
use phpDocumentor\Guides\Nodes\Menu\ExternalMenuEntryNode;
use phpDocumentor\Guides\Nodes\Menu\InternalMenuEntryNode;
use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode;
use phpDocumentor\Guides\Nodes\Menu\NavMenuNode;
use phpDocumentor\Guides\Nodes\Menu\TocNode;
use phpDocumentor\Guides\Nodes\TitleNode;
use phpDocumentor\Guides\Settings\SettingsManager;
use Throwable;
use function array_map;
use function assert;
use function count;
use const PHP_INT_MAX;
final class GlobalMenuPass implements CompilerPass
{
public function __construct(
private readonly SettingsManager $settingsManager,
) {
}
public function getPriority(): int
{
return 20; // must be run very late
}
/**
* @param DocumentNode[] $documents
*
* @return DocumentNode[]
*/
public function run(array $documents, CompilerContextInterface $compilerContext): array
{
$projectNode = $compilerContext->getProjectNode();
try {
$rootDocumentEntry = $projectNode->getRootDocumentEntry();
} catch (Throwable) {
// Todo: Functional tests have no root document entry
return $documents;
}
$rootDocument = null;
$rootFile = $rootDocumentEntry->getFile();
foreach ($documents as $document) {
if ($document->getDocumentEntry()->getFile() === $rootFile) {
$rootDocument = $document;
break;
}
}
if ($rootDocument === null) {
return $documents;
}
$menuNodes = [];
foreach ($rootDocument->getTocNodes() as $tocNode) {
$menuNode = $this->getNavMenuNodefromTocNode($compilerContext, $tocNode);
$menuNodes[] = $menuNode->withCaption($tocNode->getCaption());
}
if ($this->settingsManager->getProjectSettings()->isAutomaticMenu() && count($menuNodes) === 0) {
$menuNodes[] = $this->getNavMenuNodeFromDocumentEntries($compilerContext);
}
$projectNode->setGlobalMenues($menuNodes);
return $documents;
}
private function getNavMenuNodeFromDocumentEntries(CompilerContextInterface $compilerContext): NavMenuNode
{
$rootDocumentEntry = $compilerContext->getProjectNode()->getRootDocumentEntry();
$menuEntries = $this->getMenuEntriesFromDocumentEntries($rootDocumentEntry);
return new NavMenuNode($menuEntries);
}
/** @return InternalMenuEntryNode[] */
public function getMenuEntriesFromDocumentEntries(DocumentEntryNode $rootDocumentEntry): array
{
$menuEntries = [];
foreach ($rootDocumentEntry->getChildren() as $documentEntryNode) {
$children = $this->getMenuEntriesFromDocumentEntries($documentEntryNode);
$newMenuEntry = new InternalMenuEntryNode($documentEntryNode->getFile(), $documentEntryNode->getTitle(), $children, false, 1);
$menuEntries[] = $newMenuEntry;
}
return $menuEntries;
}
private function getNavMenuNodefromTocNode(CompilerContextInterface $compilerContext, TocNode $tocNode, string|null $menuType = null): NavMenuNode
{
$self = $this;
$menuEntries = array_map(static function (MenuEntryNode $tocEntry) use ($compilerContext, $self) {
return $self->getMenuEntryWithChildren($compilerContext, $tocEntry);
}, $tocNode->getMenuEntries());
$node = new NavMenuNode($menuEntries);
$options = $tocNode->getOptions();
unset($options['hidden']);
unset($options['titlesonly']);
unset($options['maxdepth']);
if ($menuType !== null) {
$options['menu'] = $menuType;
}
$node = $node->withOptions($options);
assert($node instanceof NavMenuNode);
return $node;
}
private function getMenuEntryWithChildren(CompilerContextInterface $compilerContext, MenuEntryNode $menuEntry): MenuEntryNode
{
if (!$menuEntry instanceof InternalMenuEntryNode) {
return $menuEntry;
}
$newMenuEntry = new InternalMenuEntryNode($menuEntry->getUrl(), $menuEntry->getValue(), [], false, 1);
$maxdepth = $this->settingsManager->getProjectSettings()->getMaxMenuDepth();
$maxdepth = $maxdepth < 1 ? PHP_INT_MAX : $maxdepth + 1;
$documentEntryOfMenuEntry = $compilerContext->getProjectNode()->getDocumentEntry($menuEntry->getUrl());
$this->addSubEntries($compilerContext, $newMenuEntry, $documentEntryOfMenuEntry, 2, $maxdepth);
return $newMenuEntry;
}
/** @param EntryNode<DocumentEntryNode|ExternalEntryNode>|ExternalEntryNode $entryNode */
private function addSubEntries(
CompilerContextInterface $compilerContext,
MenuEntryNode $sectionMenuEntry,
EntryNode $entryNode,
int $currentLevel,
int $maxDepth,
): void {
if ($maxDepth <= $currentLevel) {
return;
}
if (!$sectionMenuEntry instanceof InternalMenuEntryNode) {
return;
}
if (!$entryNode instanceof DocumentEntryNode) {
return;
}
foreach ($entryNode->getMenuEntries() as $subEntryNode) {
$subMenuEntry = match ($subEntryNode::class) {
DocumentEntryNode::class => $this->createInternalMenuEntry($subEntryNode, $currentLevel),
ExternalEntryNode::class => $this->createExternalMenuEntry($subEntryNode, $currentLevel),
};
$sectionMenuEntry->addMenuEntry($subMenuEntry);
$this->addSubEntries(
$compilerContext,
$subMenuEntry,
$subEntryNode,
$currentLevel + 1,
$maxDepth,
);
}
}
private function createInternalMenuEntry(DocumentEntryNode $subEntryNode, int $currentLevel): InternalMenuEntryNode
{
$titleNode = $subEntryNode->getTitle();
$navigationTitle = $subEntryNode->getAdditionalData('navigationTitle');
if ($navigationTitle instanceof TitleNode) {
$titleNode = $navigationTitle;
}
return new InternalMenuEntryNode(
$subEntryNode->getFile(),
$titleNode,
[],
false,
$currentLevel,
'',
);
}
private function createExternalMenuEntry(ExternalEntryNode $subEntryNode, int $currentLevel): ExternalMenuEntryNode
{
return new ExternalMenuEntryNode(
$subEntryNode->getValue(),
TitleNode::fromString($subEntryNode->getTitle()),
$currentLevel,
);
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\Passes;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\CompilerPass;
use phpDocumentor\Guides\Nodes\AnchorNode;
use phpDocumentor\Guides\Nodes\CompoundNode;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\SectionNode;
use function array_map;
use function array_merge;
use function current;
use function in_array;
use function next;
/**
* Resolves the hyperlink target for each section in the document.
*
* This follows the reStructuredText rules as outlined in:
* https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#implicit-hyperlink-targets
*/
final class ImplicitHyperlinkTargetPass implements CompilerPass
{
public function getPriority(): int
{
return 20_000; // must be run *before* MetasPass
}
/** {@inheritDoc} */
public function run(array $documents, CompilerContextInterface $compilerContext): array
{
return array_map(function (DocumentNode $document): DocumentNode {
// implicit references must not conflict with explicit ones
$knownReferences = $this->fetchExplicitReferences($document);
$nodes = $document->getNodes();
$this->deduplicateSectionIds($nodes, $knownReferences);
return $document;
}, $documents);
}
/**
* @param array<int, Node> $nodes
* @param list<string> $knownIds
*
* @return list<string>
*/
private function deduplicateSectionIds(array $nodes, array $knownIds): array
{
$node = current($nodes);
do {
if ($node instanceof SectionNode) {
$realId = $sectionId = $node->getTitle()->getId();
// resolve conflicting references by appending an increasing number
$i = 1;
while (in_array($realId, $knownIds, true)) {
$realId = $sectionId . '-' . ($i++);
}
$node->getTitle()->setId($realId);
$knownIds[] = $realId;
}
if ($node instanceof CompoundNode) {
$knownIds = $this->deduplicateSectionIds($node->getChildren(), $knownIds);
}
//phpcs:ignore SlevomatCodingStandard.ControlStructures.AssignmentInCondition.AssignmentInCondition
} while ($node = next($nodes));
return $knownIds;
}
/** @return string[] */
private function fetchExplicitReferences(Node $node): array
{
if ($node instanceof AnchorNode) {
return [$node->getValue()];
}
$anchors = [];
if ($node instanceof CompoundNode) {
foreach ($node->getChildren() as $child) {
$anchors[] = $this->fetchExplicitReferences($child);
}
}
return array_merge(...$anchors);
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\Passes;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Compiler\CompilerPass;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode;
use phpDocumentor\Guides\Nodes\ProjectNode;
use phpDocumentor\Guides\Settings\ProjectSettings;
use phpDocumentor\Guides\Settings\SettingsManager;
use Psr\Log\LoggerInterface;
final class ToctreeValidationPass implements CompilerPass
{
private SettingsManager $settingsManager;
public function __construct(
private readonly LoggerInterface $logger,
SettingsManager|null $settingsManager = null,
) {
// if for backward compatibility reasons no settings manager was passed, use the defaults
$this->settingsManager = $settingsManager ?? new SettingsManager(new ProjectSettings());
}
public function getPriority(): int
{
return 20; // must be run very late
}
/**
* @param DocumentNode[] $documents
*
* @return DocumentNode[]
*/
public function run(array $documents, CompilerContextInterface $compilerContext): array
{
if ($this->settingsManager->getProjectSettings()->isAutomaticMenu()) {
return $documents;
}
$projectNode = $compilerContext->getProjectNode();
foreach ($documents as $document) {
if (!$this->isMissingInToctree($projectNode->getDocumentEntry($document->getFilePath()), $projectNode) || $document->isOrphan()) {
continue;
}
$this->logger->warning(
'Document "' . $document->getFilePath() . '" isn\'t included in any toctree. Include it in a `.. toctree::` directive or add `:orphan:` in the first line of the rst document',
$document->getLoggerInformation(),
);
}
return $documents;
}
public function isMissingInToctree(DocumentEntryNode $documentEntry, ProjectNode $projectNode): bool
{
return $documentEntry->getParent() === null
&& $documentEntry->getFile() !== $projectNode->getRootDocumentEntry()->getFile();
}
}

View File

@@ -0,0 +1,211 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Compiler\ShadowTree;
use LogicException;
use phpDocumentor\Guides\Nodes\CompoundNode;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Nodes\Node;
use function array_unshift;
use function array_values;
use function count;
/** @template-covariant TNode of Node */
final class TreeNode
{
/** @var TreeNode<DocumentNode> */
private TreeNode $root;
/** @var self<Node>[] */
private array $children = [];
private function __construct(
/** @var TNode */
private Node $node,
/** @var self<Node>|self<DocumentNode>|null */
private self|null $parent = null,
) {
}
/** @return TreeNode<DocumentNode> */
public static function createFromDocument(DocumentNode $document): self
{
$node = new self($document);
$node->setChildren(self::createFromCompoundNode($document, $node));
$node->setRoot($node);
return $node;
}
/**
* @param CompoundNode<Node> $node
* @param self<Node>|self<DocumentNode>|null $parent
*
* @return TreeNode<Node>[]
*/
private static function createFromCompoundNode(CompoundNode $node, self|null $parent): array
{
$children = [];
foreach ($node->getChildren() as $child) {
$children[] = self::createFromNode($child, $parent);
}
return $children;
}
/**
* @param TValue $node
* @param self<Node>|self<DocumentNode>|null $parent
*
* @return TreeNode<TValue>
*
* @template TValue of Node
*/
private static function createFromNode(Node $node, self|null $parent = null): self
{
$treeNode = new self($node, $parent);
if ($node instanceof CompoundNode === false) {
return $treeNode;
}
$treeNode->setChildren(self::createFromCompoundNode($node, $treeNode));
return $treeNode;
}
/** @param self<Node>[] $children */
private function setChildren(array $children): void
{
foreach ($children as $child) {
$child->parent = $this;
}
$this->children = $children;
}
/** @return self<DocumentNode> */
public function getRoot(): self
{
return $this->root;
}
/** @param self<DocumentNode> $root */
private function setRoot(self $root): void
{
$this->root = $root;
foreach ($this->children as $child) {
$child->setRoot($root);
}
}
/** @return TNode */
public function getNode(): Node
{
return $this->node;
}
/** @return TreeNode<Node>[] */
public function getChildren(): array
{
return $this->children;
}
public function addChild(Node $child): void
{
if ($this->node instanceof CompoundNode === false) {
throw new LogicException('Cannot add a child to a non-compound node');
}
$shadowNode = self::createFromNode($child, $this);
$shadowNode->setRoot($this->root);
$this->children[] = $shadowNode;
$this->node->addChildNode($child);
}
public function pushChild(Node $child): void
{
if ($this->node instanceof CompoundNode === false) {
throw new LogicException('Cannot add a child to a non-compound node');
}
$shadowNode = self::createFromNode($child, $this);
$shadowNode->setRoot($this->root);
array_unshift($this->children, $shadowNode);
$this->node->pushChildNode($child);
}
/** @return self<Node>|self<DocumentNode>|null */
public function getParent(): TreeNode|null
{
return $this->parent;
}
public function removeChild(Node $node): void
{
if ($this->node instanceof CompoundNode === false) {
throw new LogicException('Cannot remove a child from a non-compound node');
}
foreach ($this->children as $key => $child) {
if ($child->getNode() === $node) {
unset($this->children[$key]);
$child->parent = null;
$newNode = $this->node->removeNode($key);
$this->parent?->replaceChild($this->node, $newNode);
$this->node = $newNode;
break;
}
}
$this->children = array_values($this->children);
}
public function replaceChild(Node $oldChildNode, Node $newChildNode): void
{
if ($this->node instanceof CompoundNode === false) {
throw new LogicException('Cannot remove a child from a non-compound node');
}
foreach ($this->children as $key => $child) {
if ($child->getNode() === $oldChildNode) {
$child->node = $newChildNode;
$newNode = $this->node->replaceNode($key, $newChildNode);
$this->parent?->replaceChild($this->node, $newNode);
$this->node = $newNode;
break;
}
}
}
public function findPosition(Node $node): int|null
{
foreach ($this->children as $key => $child) {
if ($child->getNode() === $node) {
return $key;
}
}
return null;
}
public function isLastChildOfParent(): bool
{
if ($this->parent === null) {
return false;
}
return $this->parent->findPosition($this->node) === count($this->parent->getChildren()) - 1;
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\DependencyInjection;
use League\Tactician\Exception\MissingHandlerException;
use League\Tactician\Handler\Locator\HandlerLocator;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use function assert;
use function is_object;
use function sprintf;
final class CommandLocator implements HandlerLocator
{
public function __construct(private readonly ContainerInterface $commands)
{
}
/** {@inheritDoc} */
public function getHandlerForCommand($commandName): object
{
try {
$command = $this->commands->get($commandName);
assert(is_object($command));
return $command;
} catch (NotFoundExceptionInterface $e) {
throw new MissingHandlerException(
sprintf('No handler found for command "%s"', $commandName),
$e->getCode(),
$e,
);
}
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\DependencyInjection\Compiler;
use phpDocumentor\Guides\NodeRenderers\TemplateNodeRenderer;
use phpDocumentor\Guides\TemplateRenderer;
use RuntimeException;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use function is_array;
use function sprintf;
final class NodeRendererPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
$definitions = [];
if (is_array($container->getParameter('phpdoc.guides.node_templates')) === false) {
throw new RuntimeException('phpdoc.guides.node_templates must be an array');
}
foreach ($container->getParameter('phpdoc.guides.node_templates') as $nodeTemplate) {
$definition = new Definition(
TemplateNodeRenderer::class,
[
'$renderer' => new Reference(TemplateRenderer::class),
'$template' => $nodeTemplate['file'],
'$nodeClass' => $nodeTemplate['node'],
],
);
$definition->addTag('phpdoc.guides.noderenderer.' . $nodeTemplate['format']);
$definitions[sprintf('phpdoc.guides.noderenderer.%s.%s', $nodeTemplate['format'], $nodeTemplate['node'])] = $definition;
}
$container->addDefinitions($definitions);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
final class ParserRulesPass implements CompilerPassInterface
{
use PriorityTaggedServiceTrait;
public function process(ContainerBuilder $container): void
{
$body = $container->findDefinition('phpdoc.guides.parser.rst.body_elements');
$structual = $container->findDefinition('phpdoc.guides.parser.rst.structural_elements');
foreach ($this->findAndSortTaggedServices('phpdoc.guides.parser.rst.structural_element', $container) as $reference) {
$structual->addMethodCall('push', [$reference]);
}
foreach ($this->findAndSortTaggedServices('phpdoc.guides.parser.rst.body_element', $container) as $reference) {
$body->addMethodCall('push', [$reference]);
//TODO: remove this call to $structual, body elements should not be part of it once subparser is removed
$structual->addMethodCall('push', [$reference]);
}
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\DependencyInjection\Compiler;
use phpDocumentor\Guides\NodeRenderers\DefaultNodeRenderer;
use phpDocumentor\Guides\NodeRenderers\DelegatingNodeRenderer;
use phpDocumentor\Guides\NodeRenderers\InMemoryNodeRendererFactory;
use phpDocumentor\Guides\NodeRenderers\PreRenderers\PreNodeRendererFactory;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use function sprintf;
use function Symfony\Component\DependencyInjection\Loader\Configurator\tagged_iterator;
final class RendererPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
$definitions = [];
foreach ($container->findTaggedServiceIds('phpdoc.renderer.typerenderer') as $id => $tags) {
foreach ($tags as $tag) {
if (isset($tag['noderender_tag']) === false) {
continue;
}
$definitions[sprintf('phpdoc.guides.noderenderer.factory.%s', $tag['format'])] = $this->createNodeRendererFactory($tag);
$definitions[sprintf('phpdoc.guides.noderenderer.prefactory.%s', $tag['format'])] = $this->createPreNodeRendererFactory($tag);
$definitions[sprintf('phpdoc.guides.noderenderer.delegating.%s', $tag['format'])] = $this->createDelegatingNodeRender($tag);
$definitions[sprintf('phpdoc.guides.noderenderer.default.%s', $tag['format'])] = (new Definition(DefaultNodeRenderer::class))->setAutowired(true)
->addMethodCall('setNodeRendererFactory', [new Reference(sprintf('phpdoc.guides.noderenderer.factory.%s', $tag['format']))])
->addTag(sprintf('phpdoc.guides.noderenderer.%s', $tag['format']));
}
}
$container->addDefinitions($definitions);
}
/** @param array{format: string} $tag */
private function createDelegatingNodeRender(array $tag): Definition
{
return (new Definition(DelegatingNodeRenderer::class))
->addTag('phpdoc.guides.output_node_renderer', ['format' => $tag['format']])
->addMethodCall('setNodeRendererFactory', [new Reference(sprintf('phpdoc.guides.noderenderer.factory.%s', $tag['format']))]);
}
/** @param array{format: string, noderender_tag: string} $tag */
private function createNodeRendererFactory(array $tag): Definition
{
return new Definition(
InMemoryNodeRendererFactory::class,
[
'$nodeRenderers' => tagged_iterator($tag['noderender_tag']),
'$defaultNodeRenderer' => new Reference(sprintf('phpdoc.guides.noderenderer.default.%s', $tag['format'])),
],
);
}
/** @param array{format: string, noderender_tag: string} $tag */
private function createPreNodeRendererFactory(array $tag): Definition
{
return (new Definition(
PreNodeRendererFactory::class,
[
'$innerFactory' => new Reference('.inner'),
'$preRenderers' => tagged_iterator('phpdoc.guides.prerenderer'),
],
))
->setDecoratedService(sprintf('phpdoc.guides.noderenderer.factory.%s', $tag['format']));
}
}

View File

@@ -0,0 +1,521 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\DependencyInjection;
use phpDocumentor\FileSystem\Finder\Exclude;
use phpDocumentor\Guides\Compiler\NodeTransformers\RawNodeEscapeTransformer;
use phpDocumentor\Guides\DependencyInjection\Compiler\NodeRendererPass;
use phpDocumentor\Guides\DependencyInjection\Compiler\ParserRulesPass;
use phpDocumentor\Guides\DependencyInjection\Compiler\RendererPass;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Settings\ProjectSettings;
use phpDocumentor\Guides\Settings\SettingsManager;
use phpDocumentor\Guides\Twig\Theme\ThemeConfig;
use phpDocumentor\Guides\Twig\Theme\ThemeManager;
use Psr\Log\LogLevel;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
use function array_keys;
use function array_map;
use function array_merge;
use function array_values;
use function assert;
use function dirname;
use function is_array;
use function is_int;
use function is_string;
use function pathinfo;
use function trim;
use function var_export;
final class GuidesExtension extends Extension implements CompilerPassInterface, ConfigurationInterface, PrependExtensionInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('guides');
$rootNode = $treeBuilder->getRootNode();
assert($rootNode instanceof ArrayNodeDefinition);
$rootNode
->fixXmlConfig('template')
->fixXmlConfig('inventory', 'inventories')
->children()
->arrayNode('project')
->children()
->scalarNode('title')->end()
->scalarNode('version')
->beforeNormalization()
->always(
// We need to revert the phpize call in XmlUtils. Version is always a string!
static function ($value) {
if (!is_int($value) && !is_string($value)) {
return var_export($value, true);
}
if (is_string($value)) {
return trim($value, "'");
}
return $value;
},
)
->end()
->end()
->scalarNode('release')
->beforeNormalization()
->always(
// We need to revert the phpize call in XmlUtils. Version is always a string!
static function ($value) {
if (!is_int($value) && !is_string($value)) {
return var_export($value, true);
}
if (is_string($value)) {
return trim($value, "'");
}
return $value;
},
)
->end()
->end()
->scalarNode('copyright')->end()
->end()
->end()
->arrayNode('inventories')
->arrayPrototype()
->children()
->scalarNode('id')
->isRequired()
->end()
->scalarNode('url')
->isRequired()
->end()
->end()
->end()
->end()
->scalarNode('theme')->end()
->scalarNode('input')->end()
->scalarNode('input_file')->end()
->scalarNode('index_name')->end()
->arrayNode('exclude')
->addDefaultsIfNotSet()
->fixXmlConfig('path')
->children()
->booleanNode('hidden')->defaultTrue()->end()
->booleanNode('symlinks')->defaultTrue()->end()
->append($this->paths())
->end()
->end()
->scalarNode('output')->end()
->scalarNode('input_format')->end()
->arrayNode('output_format')
->defaultValue(['html', 'interlink'])
->beforeNormalization()
->ifString()
->then(static function ($value) {
return [$value];
})
->end()
->scalarPrototype()->end()
->end()
->arrayNode('ignored_domain')
->defaultValue([])
->beforeNormalization()
->ifString()
->then(static function ($value) {
return [$value];
})
->end()
->scalarPrototype()->end()
->end()
->scalarNode('log_path')->end()
->scalarNode('fail_on_log')->end()
->scalarNode('fail_on_error')->end()
->scalarNode('show_progress')->end()
->scalarNode('links_are_relative')->end()
->scalarNode('max_menu_depth')->end()
->scalarNode('automatic_menu')->end()
->arrayNode('base_template_paths')
->defaultValue([])
->scalarPrototype()->end()
->end()
->arrayNode('templates')
->arrayPrototype()
->children()
->scalarNode('node')
->isRequired()
->end()
->scalarNode('file')
->isRequired()
->end()
->scalarNode('format')
->defaultValue('html')
->end()
->end()
->end()
->end()
->arrayNode('raw_node')
->fixXmlConfig('sanitizer')
->children()
->booleanNode('escape')->defaultValue(false)->end()
->scalarNode('sanitizer_name')->end()
->arrayNode('sanitizers')
->defaultValue([])
->useAttributeAsKey('name')
->arrayPrototype()
->fixXmlConfig('allow_element')
->fixXmlConfig('drop_element')
->fixXmlConfig('block_element')
->fixXmlConfig('allow_attribute')
->fixXmlConfig('drop_attribute')
->children()
->booleanNode('allow_safe_elements')->defaultValue(true)->end()
->booleanNode('allow_static_elements')->defaultValue(true)->end()
->arrayNode('allow_elements')
->normalizeKeys(false)
->useAttributeAsKey('name')
->variablePrototype()
->beforeNormalization()
->ifArray()->then(static fn ($n) => $n['attribute'] ?? $n)
->end()
->validate()
->ifTrue(static fn ($n): bool => !is_string($n) && !is_array($n))
->thenInvalid('The value must be either a string or an array of strings.')
->end()
->end()
->end()
->arrayNode('block_elements')
->beforeNormalization()->castToArray()->end()
->scalarPrototype()->end()
->end()
->arrayNode('drop_elements')
->beforeNormalization()->castToArray()->end()
->scalarPrototype()->end()
->end()
->arrayNode('allow_attributes')
->normalizeKeys(false)
->useAttributeAsKey('name')
->variablePrototype()
->beforeNormalization()
->ifArray()->then(static fn ($n) => $n['element'] ?? $n)
->end()
->end()
->end()
->arrayNode('drop_attributes')
->normalizeKeys(false)
->useAttributeAsKey('name')
->variablePrototype()
->beforeNormalization()
->ifArray()->then(static fn ($n) => $n['element'] ?? $n)
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->scalarNode('default_code_language')->defaultValue('')->end()
->arrayNode('themes')
->defaultValue([])
->arrayPrototype()
->beforeNormalization()
->ifTrue(static fn ($v) => !is_array($v) || !isset($v['templates']))
->then(static fn (string $v) => ['templates' => (array) $v])
->end()
->children()
->scalarNode('extends')->end()
->arrayNode('templates')
->isRequired()
->cannotBeEmpty()
->scalarPrototype()->end()
->end()
->end()
->end()
->end()
->end();
return $treeBuilder;
}
/** @param mixed[] $configs */
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);
$loader = new PhpFileLoader(
$container,
new FileLocator(dirname(__DIR__, 2) . '/resources/config'),
);
$loader->load('command_bus.php');
$loader->load('guides.php');
$projectSettings = new ProjectSettings();
if (isset($config['project'])) {
if (isset($config['project']['version'])) {
$projectSettings->setVersion((string) $config['project']['version']);
}
if (isset($config['project']['title'])) {
$projectSettings->setTitle((string) $config['project']['title']);
}
if (isset($config['project']['release'])) {
$projectSettings->setRelease((string) $config['project']['release']);
}
if (isset($config['project']['copyright'])) {
$projectSettings->setCopyright((string) $config['project']['copyright']);
}
}
if (isset($config['inventories'])) {
$projectSettings->setInventories($config['inventories']);
}
if (isset($config['theme'])) {
$projectSettings->setTheme((string) $config['theme']);
}
if (isset($config['input'])) {
$projectSettings->setInput((string) $config['input']);
}
if (isset($config['input_file']) && $config['input_file'] !== '') {
$inputFile = (string) $config['input_file'];
$pathInfo = pathinfo($inputFile);
$projectSettings->setInputFile($pathInfo['filename']);
if (!empty($pathInfo['extension'])) {
$projectSettings->setInputFormat($pathInfo['extension']);
}
}
if (isset($config['index_name']) && $config['index_name'] !== '') {
$projectSettings->setIndexName((string) $config['index_name']);
}
if (isset($config['output'])) {
$projectSettings->setOutput((string) $config['output']);
}
if (isset($config['input_format'])) {
$projectSettings->setInputFormat((string) $config['input_format']);
}
if (isset($config['output_format']) && is_array($config['output_format'])) {
$projectSettings->setOutputFormats($config['output_format']);
}
if (isset($config['ignored_domain']) && is_array($config['ignored_domain'])) {
$projectSettings->setIgnoredDomains($config['ignored_domain']);
}
if (isset($config['links_are_relative'])) {
$projectSettings->setLinksRelative((bool) $config['links_are_relative']);
}
if (isset($config['show_progress'])) {
$projectSettings->setShowProgressBar((bool) $config['show_progress']);
}
if (isset($config['fail_on_error'])) {
$projectSettings->setFailOnError(LogLevel::ERROR);
}
if (isset($config['fail_on_log'])) {
$projectSettings->setFailOnError(LogLevel::WARNING);
}
if (isset($config['max_menu_depth'])) {
$projectSettings->setMaxMenuDepth((int) $config['max_menu_depth']);
}
if (isset($config['automatic_menu'])) {
$projectSettings->setAutomaticMenu((bool) $config['automatic_menu']);
}
if (isset($config['default_code_language'])) {
$projectSettings->setDefaultCodeLanguage((string) $config['default_code_language']);
}
$projectSettings->setExcludes(
new Exclude(
$config['exclude']['paths'],
$config['exclude']['hidden'],
$config['exclude']['symlinks'],
),
);
$container->getDefinition(SettingsManager::class)
->addMethodCall('setProjectSettings', [$projectSettings]);
$config['base_template_paths'][] = dirname(__DIR__, 2) . '/resources/template/html';
$config['base_template_paths'][] = dirname(__DIR__, 2) . '/resources/template/tex';
$container->setParameter('phpdoc.guides.base_template_paths', $config['base_template_paths']);
$container->setParameter('phpdoc.guides.node_templates', $config['templates']);
$container->setParameter('phpdoc.guides.inventories', $config['inventories']);
$container->setParameter('phpdoc.guides.raw_node.escape', $config['raw_node']['escape'] ?? false);
if ($config['raw_node'] ?? false) {
$this->configureSanitizers($config['raw_node'], $container);
}
foreach ($config['themes'] as $themeName => $themeConfig) {
$container->getDefinition(ThemeManager::class)
->addMethodCall('registerTheme', [new ThemeConfig($themeName, $themeConfig['templates'], $themeConfig['extends'] ?? null)]);
}
}
/** @param array<string> $defaultValue */
private function paths(array $defaultValue = []): ArrayNodeDefinition
{
$treebuilder = new TreeBuilder('paths');
return $treebuilder->getRootNode()
->beforeNormalization()
->castToArray()
->end()
->defaultValue($defaultValue)
->prototype('scalar')
->end();
}
public function process(ContainerBuilder $container): void
{
(new ParserRulesPass())->process($container);
(new NodeRendererPass())->process($container);
(new RendererPass())->process($container);
}
/** @param mixed[] $config */
public function getConfiguration(array $config, ContainerBuilder $container): ConfigurationInterface
{
return $this;
}
public function prepend(ContainerBuilder $container): void
{
$container->prependExtensionConfig(
'guides',
[
'templates' => array_merge(
templateArray(
require dirname(__DIR__, 2) . '/resources/template/html/template.php',
'html',
),
templateArray(
require dirname(__DIR__, 2) . '/resources/template/tex/template.php',
'tex',
),
),
],
);
}
/** @param array<string, mixed> $rawNodeConfig */
private function configureSanitizers(array $rawNodeConfig, ContainerBuilder $container): void
{
if ($rawNodeConfig['sanitizer_name'] ?? false) {
$container->getDefinition(RawNodeEscapeTransformer::class)
->setArgument('$htmlSanitizerConfig', new Reference('phpdoc.guides.raw_node.sanitizer.' . $rawNodeConfig['sanitizer_name']));
}
if (!is_array($rawNodeConfig['sanitizers'] ?? false)) {
return;
}
foreach ($rawNodeConfig['sanitizers'] as $sanitizerName => $sanitizerConfig) {
$def = $container->register('phpdoc.guides.raw_node.sanitizer.' . $sanitizerName, HtmlSanitizerConfig::class);
// Base
if ($sanitizerConfig['allow_safe_elements']) {
$def->addMethodCall('allowSafeElements', [], true);
}
if ($sanitizerConfig['allow_static_elements']) {
$def->addMethodCall('allowStaticElements', [], true);
}
// Configures elements
foreach ($sanitizerConfig['allow_elements'] as $element => $attributes) {
$def->addMethodCall('allowElement', [$element, $attributes], true);
}
foreach ($sanitizerConfig['block_elements'] as $element) {
$def->addMethodCall('blockElement', [$element], true);
}
foreach ($sanitizerConfig['drop_elements'] as $element) {
$def->addMethodCall('dropElement', [$element], true);
}
// Configures attributes
foreach ($sanitizerConfig['allow_attributes'] as $attribute => $elements) {
$def->addMethodCall('allowAttribute', [$attribute, $elements], true);
}
foreach ($sanitizerConfig['drop_attributes'] as $attribute => $elements) {
$def->addMethodCall('dropAttribute', [$attribute, $elements], true);
}
}
}
}
/**
* Helper function to configure multiple templates.
*
* This function is used to configure the templates in the configuration file.
*
* @param array<class-string<Node>, string> $input
*
* @return array<array-key, array{node: class-string<Node>, file: string, format: string}>
*/
function templateArray(array $input, string $format = 'html'): array
{
return array_map(
static fn ($node, $template) => template($node, $template, $format),
array_keys($input),
array_values($input),
);
}
/**
* Helper function to configure templates.
*
* This function is used to configure the templates in the configuration file.
*
* @param class-string<Node> $node
*
* @return array{node: class-string<Node>, file: string, format: string}
*/
function template(string $node, string $template, string $format = 'html'): array
{
return [
'node' => $node,
'file' => $template,
'format' => $format,
];
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\DependencyInjection;
use Monolog\Handler\TestHandler;
use Monolog\Logger;
use phpDocumentor\Guides\Compiler\Compiler;
use phpDocumentor\Guides\Parser;
use Psr\Clock\ClockInterface;
use Symfony\Component\Clock\MockClock;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Reference;
final class TestExtension extends Extension implements CompilerPassInterface
{
/** @param array<mixed> $configs */
public function load(array $configs, ContainerBuilder $container): void
{
}
public function process(ContainerBuilder $container): void
{
$container->getDefinition(Parser::class)->setPublic(true);
$container->getDefinition(Compiler::class)->setPublic(true);
$container->getDefinition('phpdoc.guides.output_node_renderer')->setPublic(true);
$clockDefinition = new Definition(MockClock::class, ['2023-01-01 12:00:00']);
$container->setDefinition(ClockInterface::class, $clockDefinition);
$container->register(TestHandler::class, TestHandler::class)->setPublic(true);
$container->getDefinition(Logger::class)
->addMethodCall('pushHandler', [new Reference(TestHandler::class)]);
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Event;
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Nodes\Node;
final class ModifyDocumentEntryAdditionalData
{
/** @param array<string, Node> $additionalData */
public function __construct(
private array $additionalData,
private readonly DocumentNode $documentNode,
private readonly CompilerContextInterface $compilerContext,
) {
}
/** @return array<string, Node> */
public function getAdditionalData(): array
{
return $this->additionalData;
}
/** @param array<string, Node> $additionalData */
public function setAdditionalData(array $additionalData): ModifyDocumentEntryAdditionalData
{
$this->additionalData = $additionalData;
return $this;
}
public function getDocumentNode(): DocumentNode
{
return $this->documentNode;
}
public function getCompilerContext(): CompilerContextInterface
{
return $this->compilerContext;
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Event;
use phpDocumentor\Guides\Files;
use phpDocumentor\Guides\Handlers\ParseDirectoryCommand;
/**
* This event is called after all files have been collected for parsing
* But before the actual parsing begins.
*
* It can be used to manipulate the files to be parsed.
*/
final class PostCollectFilesForParsingEvent
{
public function __construct(
private readonly ParseDirectoryCommand $command,
private Files $files,
) {
}
public function getCommand(): ParseDirectoryCommand
{
return $this->command;
}
public function getFiles(): Files
{
return $this->files;
}
public function setFiles(Files $files): void
{
$this->files = $files;
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Event;
use phpDocumentor\Guides\Nodes\DocumentNode;
/**
* This event is called after the parsing of each document is completed by the responsible extension.
*
* It can for example be used to display a progress bar.
*/
final class PostParseDocument
{
public function __construct(
private readonly string $fileName,
private DocumentNode|null $documentNode,
private readonly string $originalFile,
) {
}
public function getDocumentNode(): DocumentNode|null
{
return $this->documentNode;
}
public function setDocumentNode(DocumentNode|null $documentNode): void
{
$this->documentNode = $documentNode;
}
public function getFileName(): string
{
return $this->fileName;
}
public function getOriginalFileName(): string
{
return $this->originalFile;
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Event;
use phpDocumentor\Guides\Handlers\ParseDirectoryCommand;
use phpDocumentor\Guides\Nodes\DocumentNode;
/**
* This event is dispatched right after the overall parsing process is
* finished, Before the compiler passes, including the node transformers
* are called.
*/
final class PostParseProcess
{
/** @param DocumentNode[] $documents */
public function __construct(
private readonly ParseDirectoryCommand $parseDirectoryCommand,
private readonly array $documents,
) {
}
public function getParseDirectoryCommand(): ParseDirectoryCommand
{
return $this->parseDirectoryCommand;
}
/** @return DocumentNode[] */
public function getDocuments(): array
{
return $this->documents;
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Event;
use phpDocumentor\Guides\Nodes\ProjectNode;
use phpDocumentor\Guides\Settings\ProjectSettings;
final class PostProjectNodeCreated
{
public function __construct(
private ProjectNode $projectNode,
private ProjectSettings $settings,
) {
}
public function getProjectNode(): ProjectNode
{
return $this->projectNode;
}
public function setProjectNode(ProjectNode $projectNode): void
{
$this->projectNode = $projectNode;
}
public function getSettings(): ProjectSettings
{
return $this->settings;
}
public function setSettings(ProjectSettings $settings): void
{
$this->settings = $settings;
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Event;
use phpDocumentor\Guides\Handlers\RenderDocumentCommand;
use phpDocumentor\Guides\NodeRenderers\NodeRenderer;
use phpDocumentor\Guides\Nodes\DocumentNode;
/**
* This event is called after the rendering of each document.
*
* It can for example be used to display a progress bar or to post-process the rendered documents one by one.
*/
final class PostRenderDocument
{
/** @param NodeRenderer<DocumentNode> $renderer */
public function __construct(
private readonly NodeRenderer $renderer,
private readonly RenderDocumentCommand $command,
) {
}
/** @return NodeRenderer<DocumentNode> */
public function getRenderer(): NodeRenderer
{
return $this->renderer;
}
public function getCommand(): RenderDocumentCommand
{
return $this->command;
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Event;
use phpDocumentor\Guides\Handlers\RenderCommand;
/**
* This event is called once after each rendering method after all documents have been rendered.
*
* It can for example be used to copy assets into the target directory after rendering.
*/
final class PostRenderProcess
{
public function __construct(private readonly RenderCommand $command)
{
}
public function getCommand(): RenderCommand
{
return $this->command;
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Event;
use phpDocumentor\Guides\Parser;
/**
* This event is called before the parsing of each document is passed to the responsible extension.
*
* It can be used to manipulate the content passed to the parser by calling PreParseDocument::setContents
*/
final class PreParseDocument
{
public function __construct(private readonly Parser $parser, private readonly string $fileName, private string $contents)
{
}
public function getParser(): Parser
{
return $this->parser;
}
public function setContents(string $contents): void
{
$this->contents = $contents;
}
public function getContents(): string
{
return $this->contents;
}
public function getFileName(): string
{
return $this->fileName;
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Event;
use phpDocumentor\Guides\Handlers\ParseDirectoryCommand;
/**
* This event is dispatched right before the overall parsing process is
* started.
*
* It can be used to modify the ParseDirectoryCommand, so it could be used to alter the
* directory to be parsed or the file system to be used.
*/
final class PreParseProcess
{
public function __construct(
private ParseDirectoryCommand $parseDirectoryCommand,
) {
}
public function getParseDirectoryCommand(): ParseDirectoryCommand
{
return $this->parseDirectoryCommand;
}
public function setParseDirectoryCommand(ParseDirectoryCommand $parseDirectoryCommand): PreParseProcess
{
$this->parseDirectoryCommand = $parseDirectoryCommand;
return $this;
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Event;
use phpDocumentor\Guides\Handlers\RenderDocumentCommand;
use phpDocumentor\Guides\NodeRenderers\NodeRenderer;
use phpDocumentor\Guides\Nodes\DocumentNode;
/**
* This event is called before the rendering of each document.
*/
final class PreRenderDocument
{
/** @param NodeRenderer<DocumentNode> $renderer */
public function __construct(private readonly NodeRenderer $renderer, private readonly RenderDocumentCommand $command)
{
}
/** @return NodeRenderer<DocumentNode> */
public function getRenderer(): NodeRenderer
{
return $this->renderer;
}
public function getCommand(): RenderDocumentCommand
{
return $this->command;
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Event;
use phpDocumentor\Guides\Handlers\RenderCommand;
/**
* This event is called once before each rendering method after all documents have been parsed and
* all compiler passes (including node transformers have been called.)
*
* It can be used to exit the rendering process before anything was rendered. A third party extension could then
* take over the rendering with its own means.
*/
final class PreRenderProcess
{
private bool $exitRendering = false;
public function __construct(
private readonly RenderCommand $command,
private readonly int $steps = 1,
) {
}
public function getCommand(): RenderCommand
{
return $this->command;
}
public function isExitRendering(): bool
{
return $this->exitRendering;
}
public function setExitRendering(bool $exitRendering): PreRenderProcess
{
$this->exitRendering = $exitRendering;
return $this;
}
public function getSteps(): int
{
return $this->steps;
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\EventListener;
use phpDocumentor\Guides\Event\PostProjectNodeCreated;
use phpDocumentor\Guides\Settings\ComposerSettingsLoader;
use function dirname;
use function file_exists;
use function getcwd;
use function is_string;
final class LoadSettingsFromComposer
{
public function __construct(private readonly ComposerSettingsLoader $composerSettingsLoader)
{
}
public function __invoke(PostProjectNodeCreated $event): void
{
$workDir = getcwd();
if ($workDir === false) {
return;
}
$composerjson = $this->findComposerJson($workDir);
if (!is_string($composerjson)) {
return;
}
$projectNode = $event->getProjectNode();
$settings = $event->getSettings();
$this->composerSettingsLoader->loadSettings($projectNode, $settings, $composerjson);
}
private function findComposerJson(string $currentDir): string|null
{
// Navigate up the directory structure until finding the composer.json file
while (!file_exists($currentDir . '/composer.json') && $currentDir !== '/') {
$currentDir = dirname($currentDir);
}
// If found, return the path to the composer.json file; otherwise, return null
return file_exists($currentDir . '/composer.json') ? $currentDir . '/composer.json' : null;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Exception;
use Exception;
final class DocumentEntryNotFound extends Exception
{
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Exception;
use Exception;
final class DuplicateLinkAnchorException extends Exception
{
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Exception;
use Exception;
final class InvalidTableStructure extends Exception
{
}

View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides;
use Doctrine\Deprecations\Deprecation;
use Flyfinder\Specification\AndSpecification;
use Flyfinder\Specification\HasExtension;
use Flyfinder\Specification\InPath;
use Flyfinder\Specification\NotSpecification;
use Flyfinder\Specification\SpecificationInterface;
use InvalidArgumentException;
use League\Flysystem\FilesystemInterface;
use phpDocumentor\FileSystem\FileSystem;
use phpDocumentor\FileSystem\Finder\Exclude;
use phpDocumentor\FileSystem\Finder\SpecificationFactory;
use phpDocumentor\FileSystem\Finder\SpecificationFactoryInterface;
use phpDocumentor\FileSystem\Path;
use function sprintf;
use function strlen;
use function substr;
use function trim;
final class FileCollector
{
/** @var string[][] */
private array $fileInfos = [];
private SpecificationFactoryInterface $specificationFactory;
public function __construct(SpecificationFactoryInterface|null $specificationFactory = null)
{
$this->specificationFactory = $specificationFactory ?? new SpecificationFactory();
}
/**
* Scans a directory recursively looking for all files to parse.
*
* This takes into account the presence of cached & fresh MetaEntry
* objects, and avoids adding files to the parse queue that have
* not changed and whose direct dependencies have not changed.
*
* @param SpecificationInterface|Exclude|null $excludedSpecification specification that is used to exclude specific files/directories.
* Passing a {@see SpecificationInterface} is deprecated, use {@see Exclude} instead.
*/
public function collect(FilesystemInterface|FileSystem $filesystem, string $directory, string $extension, SpecificationInterface|Exclude|null $excludedSpecification = null): Files
{
if ($excludedSpecification instanceof SpecificationInterface) {
Deprecation::triggerIfCalledFromOutside(
'phpdocumentor/guides',
'https://github.com/phpDocumentor/guides/issues/1209',
'Passing %s to %s::collect() is deprecated, use %s instead.',
$excludedSpecification::class,
self::class,
Exclude::class,
);
}
$directory = trim($directory, '/');
$specification = $this->getSpecification($excludedSpecification, $directory, $extension);
/** @var array<array<string>> $files */
$files = $filesystem->find($specification);
// completely populate the splFileInfos property
$this->fileInfos = [];
foreach ($files as $fileInfo) {
$dirname = $fileInfo['dirname'];
if (strlen($directory) > 0) {
// Make paths relative to the provided source folder
$dirname = substr($fileInfo['dirname'], strlen($directory) + 1) ?: '';
}
$documentPath = $this->getFilenameFromFile($fileInfo['filename'], $dirname);
$this->fileInfos[$documentPath] = $fileInfo;
}
$parseQueue = new Files();
foreach ($this->fileInfos as $filename => $_fileInfo) {
if (!$this->doesFileRequireParsing((string) $filename)) {
continue;
}
$parseQueue->add((string) $filename);
}
return $parseQueue;
}
private function doesFileRequireParsing(string $filename): bool
{
if (!isset($this->fileInfos[$filename])) {
throw new InvalidArgumentException(
sprintf('No file info found for "%s" - file does not exist.', $filename),
);
}
return true;
}
/**
* Converts foo/bar.rst to foo/bar (the document filename)
*/
private function getFilenameFromFile(string $filename, string $dirname): string
{
$directory = $dirname ? $dirname . '/' : '';
return $directory . $filename;
}
private function getSpecification(Exclude|SpecificationInterface|null $excludedSpecification, string $directory, string $extension): SpecificationInterface
{
if ($excludedSpecification instanceof Exclude) {
if ($directory === '') {
$directory = new Path('./');
}
return $this->specificationFactory->create([$directory], $excludedSpecification, [$extension]);
}
$specification = new AndSpecification(new InPath(new \Flyfinder\Path($directory)), new HasExtension([$extension]));
if ($excludedSpecification) {
$specification = new AndSpecification($specification, new NotSpecification($excludedSpecification));
}
return $specification;
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides;
use ArrayIterator;
use Countable;
use Iterator;
use IteratorAggregate;
use function array_key_exists;
use function count;
use function sort;
use const SORT_FLAG_CASE;
use const SORT_NATURAL;
/** @implements IteratorAggregate<string> */
final class Files implements IteratorAggregate, Countable
{
/** @var string[] */
private array $files = [];
public function add(string $filename): void
{
if (array_key_exists($filename, $this->files)) {
return;
}
$this->files[$filename] = $filename;
}
/** @return Iterator<string> */
public function getIterator(): Iterator
{
sort($this->files, SORT_NATURAL | SORT_FLAG_CASE);
return new ArrayIterator($this->files);
}
public function count(): int
{
return count($this->files);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Handlers;
use phpDocumentor\Guides\Compiler\CompilerContext;
use phpDocumentor\Guides\Nodes\DocumentNode;
final class CompileDocumentsCommand
{
/** @param DocumentNode[] $documents */
public function __construct(
private readonly array $documents,
private readonly CompilerContext $compilerContext,
) {
}
/** @return DocumentNode[] */
public function getDocuments(): array
{
return $this->documents;
}
public function getCompilerContext(): CompilerContext
{
return $this->compilerContext;
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Handlers;
use phpDocumentor\Guides\Compiler\Compiler;
use phpDocumentor\Guides\Nodes\DocumentNode;
final class CompileDocumentsHandler
{
public function __construct(private readonly Compiler $compiler)
{
}
/** @return DocumentNode[] */
public function handle(CompileDocumentsCommand $command): array
{
return $this->compiler->run($command->getDocuments(), $command->getCompilerContext());
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Handlers;
final class LoadCacheCommand
{
public function __construct(private readonly string $cacheDirectory, private readonly bool $useCaching = true)
{
}
public function getCacheDirectory(): string
{
return $this->cacheDirectory;
}
public function useCaching(): bool
{
return $this->useCaching;
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Handlers;
final class LoadCacheHandler
{
public function handle(LoadCacheCommand $command): void
{
if (!$command->useCaching()) {
return;
}
// TODO:: Load Cache
}
}

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Handlers;
use Doctrine\Deprecations\Deprecation;
use Flyfinder\Specification\SpecificationInterface;
use League\Flysystem\FilesystemInterface;
use phpDocumentor\FileSystem\FileSystem;
use phpDocumentor\FileSystem\Finder\Exclude;
use phpDocumentor\Guides\Nodes\ProjectNode;
final class ParseDirectoryCommand
{
private readonly SpecificationInterface|null $excludedSpecification;
private readonly Exclude|null $exclude;
public function __construct(
private readonly FilesystemInterface|FileSystem $origin,
private readonly string $directory,
private readonly string $inputFormat,
private readonly ProjectNode $projectNode,
SpecificationInterface|Exclude|null $excludedSpecification = null,
) {
if ($excludedSpecification instanceof SpecificationInterface) {
Deprecation::trigger(
'phpdocumentor/guides',
'https://github.com/phpDocumentor/guides/issues/1209',
'Passing %s to %s is deprecated, use %s instead.',
$excludedSpecification::class,
self::class,
Exclude::class,
);
$this->excludedSpecification = $excludedSpecification;
$this->exclude = null;
} else {
$this->exclude = $excludedSpecification;
$this->excludedSpecification = null;
}
}
public function getOrigin(): FilesystemInterface|FileSystem
{
return $this->origin;
}
public function getDirectory(): string
{
return $this->directory;
}
public function getInputFormat(): string
{
return $this->inputFormat;
}
public function getProjectNode(): ProjectNode
{
return $this->projectNode;
}
/** @deprecated Specification definition on parse directory is deprecated. Use {@see self::getExclude()} instead. */
public function getExcludedSpecification(): SpecificationInterface|null
{
Deprecation::triggerIfCalledFromOutside(
'phpdocumentor/guides',
'https://github.com/phpDocumentor/guides/issues/1209',
'Specification definition on parse directory is deprecated. Use getExclude() instead.',
);
return $this->excludedSpecification;
}
public function getExclude(): Exclude
{
return $this->exclude ?? new Exclude();
}
public function hasExclude(): bool
{
return isset($this->exclude);
}
/** @internal Used by {@see ParseDirectoryHandler} to dispatch without triggering the deprecation on {@see self::getExcludedSpecification()}. */
public function hasExcludedSpecification(): bool
{
return isset($this->excludedSpecification);
}
}

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Handlers;
use InvalidArgumentException;
use League\Flysystem\FilesystemInterface;
use League\Tactician\CommandBus;
use phpDocumentor\FileSystem\FileSystem;
use phpDocumentor\Guides\Event\PostCollectFilesForParsingEvent;
use phpDocumentor\Guides\Event\PostParseProcess;
use phpDocumentor\Guides\Event\PreParseProcess;
use phpDocumentor\Guides\FileCollector;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Settings\ProjectSettings;
use phpDocumentor\Guides\Settings\SettingsManager;
use Psr\EventDispatcher\EventDispatcherInterface;
use function array_map;
use function assert;
use function explode;
use function implode;
use function sprintf;
final class ParseDirectoryHandler
{
private SettingsManager $settingsManager;
public function __construct(
private readonly FileCollector $fileCollector,
private readonly CommandBus $commandBus,
private readonly EventDispatcherInterface $eventDispatcher,
SettingsManager|null $settingsManager = null,
) {
// if for backward compatibility reasons no settings manager was passed, use the defaults
$this->settingsManager = $settingsManager ?? new SettingsManager(new ProjectSettings());
}
/** @return DocumentNode[] */
public function handle(ParseDirectoryCommand $command): array
{
$preParseProcessEvent = $this->eventDispatcher->dispatch(
new PreParseProcess($command),
);
assert($preParseProcessEvent instanceof PreParseProcess);
$command = $preParseProcessEvent->getParseDirectoryCommand();
$origin = $command->getOrigin();
$currentDirectory = $command->getDirectory();
$extension = $command->getInputFormat();
$indexName = $this->getDirectoryIndexFile(
$origin,
$currentDirectory,
$extension,
);
$files = $this->fileCollector->collect(
$origin,
$currentDirectory,
$extension,
match (true) {
$command->hasExclude() => $command->getExclude(),
$command->hasExcludedSpecification() => $command->getExcludedSpecification(),
default => null,
},
);
$postCollectFilesForParsingEvent = $this->eventDispatcher->dispatch(
new PostCollectFilesForParsingEvent($command, $files),
);
assert($postCollectFilesForParsingEvent instanceof PostCollectFilesForParsingEvent);
/** @var DocumentNode[] $documents */
$documents = [];
foreach ($postCollectFilesForParsingEvent->getFiles() as $file) {
$documents[] = $this->commandBus->handle(
new ParseFileCommand($origin, $currentDirectory, $file, $extension, 1, $command->getProjectNode(), $indexName === $file),
);
}
$postCollectFilesForParsingEvent = $this->eventDispatcher->dispatch(
new PostParseProcess($command, $documents),
);
assert($postCollectFilesForParsingEvent instanceof PostParseProcess);
return $documents;
}
private function getDirectoryIndexFile(
FilesystemInterface|FileSystem $filesystem,
string $directory,
string $sourceFormat,
): string {
$extension = $sourceFormat;
// On macOS filesystems, a file-check against "index.rst"
// using $filesystem->has() would return TRUE, when in fact
// a file might be stored as "Index.rst". Thus, at this point
// we fetch the whole directory list and compare the contents
// with if the INDEX_FILE_NAMES entry matches. This ensures
// that we get the file with exactly the casing that is returned
// from the filesystem.
$contentFromFilesystem = $filesystem->listContents($directory);
$hashedContentFromFilesystem = [];
foreach ($contentFromFilesystem as $itemFromFilesystem) {
$hashedContentFromFilesystem[$itemFromFilesystem['basename']] = true;
}
$indexFileNames = array_map('trim', explode(',', $this->settingsManager->getProjectSettings()->getIndexName()));
$indexNamesNotFound = [];
foreach ($indexFileNames as $indexName) {
$fullIndexFilename = sprintf('%s.%s', $indexName, $extension);
if (isset($hashedContentFromFilesystem[$fullIndexFilename])) {
return $indexName;
}
$indexNamesNotFound[] = $fullIndexFilename;
}
throw new InvalidArgumentException(
sprintf('Could not find an index file "%s", expected file names: %s', $directory, implode(', ', $indexNamesNotFound)),
);
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Handlers;
use League\Flysystem\FilesystemInterface;
use phpDocumentor\FileSystem\FileSystem;
use phpDocumentor\Guides\Nodes\ProjectNode;
final class ParseFileCommand
{
public function __construct(
private readonly FilesystemInterface|FileSystem $origin,
private readonly string $directory,
private readonly string $file,
private readonly string $extension,
private readonly int $initialHeaderLevel,
private readonly ProjectNode $projectNode,
private readonly bool $isRoot,
) {
}
public function getOrigin(): FilesystemInterface|FileSystem
{
return $this->origin;
}
public function getDirectory(): string
{
return $this->directory;
}
public function getFile(): string
{
return $this->file;
}
public function getExtension(): string
{
return $this->extension;
}
public function getInitialHeaderLevel(): int
{
return $this->initialHeaderLevel;
}
public function getProjectNode(): ProjectNode
{
return $this->projectNode;
}
public function isRoot(): bool
{
return $this->isRoot;
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Handlers;
use InvalidArgumentException;
use League\Flysystem\FilesystemInterface;
use phpDocumentor\FileSystem\FileSystem;
use phpDocumentor\Guides\Event\PostParseDocument;
use phpDocumentor\Guides\Event\PreParseDocument;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Nodes\ProjectNode;
use phpDocumentor\Guides\Parser;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use RuntimeException;
use function assert;
use function ltrim;
use function sprintf;
use function trim;
final class ParseFileHandler
{
public function __construct(
private readonly LoggerInterface $logger,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly Parser $parser,
) {
}
public function handle(ParseFileCommand $command): DocumentNode|null
{
$this->logger->info(sprintf('Parsing %s', $command->getFile()));
return $this->createDocument(
$command->getOrigin(),
$command->getDirectory(),
$command->getFile(),
$command->getExtension(),
$command->getInitialHeaderLevel(),
$command->getProjectNode(),
$command->isRoot(),
);
}
private function getFileContents(FilesystemInterface|FileSystem $origin, string $file): string
{
if (!$origin->has($file)) {
throw new InvalidArgumentException(sprintf('File at path %s does not exist', $file));
}
$contents = $origin->read($file);
if ($contents === false) {
throw new InvalidArgumentException(sprintf('Could not load file from path %s', $file));
}
return $contents;
}
private function createDocument(
FilesystemInterface|FileSystem $origin,
string $documentFolder,
string $fileName,
string $extension,
int $initialHeaderLevel,
ProjectNode $projectNode,
bool $isRoot,
): DocumentNode|null {
$path = $this->buildPathOnFileSystem($fileName, $documentFolder, $extension);
$fileContents = $this->getFileContents($origin, $path);
$this->parser->prepare(
$origin,
$documentFolder,
$fileName,
$projectNode,
$initialHeaderLevel,
);
$preParseDocumentEvent = $this->eventDispatcher->dispatch(
new PreParseDocument($this->parser, $path, $fileContents),
);
assert($preParseDocumentEvent instanceof PreParseDocument);
$document = null;
try {
$document = $this->parser->parse($preParseDocumentEvent->getContents(), $extension)->withIsRoot($isRoot);
} catch (RuntimeException $e) {
$this->logger->error(
sprintf('Unable to parse %s, input format was not recognized', $path),
['error' => $e],
);
}
$event = $this->eventDispatcher->dispatch(new PostParseDocument($fileName, $document, $path));
assert($event instanceof PostParseDocument);
return $event->getDocumentNode();
}
private function buildPathOnFileSystem(string $file, string $currentDirectory, string $extension): string
{
return ltrim(sprintf('%s/%s.%s', trim($currentDirectory, '/'), $file, $extension), '/');
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Handlers;
final class PersistCacheCommand
{
public function __construct(private readonly string $cacheDirectory, private readonly bool $useCache = false)
{
}
public function getCacheDirectory(): string
{
return $this->cacheDirectory;
}
public function useCache(): bool
{
return $this->useCache;
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Handlers;
final class PersistCacheHandler
{
public function handle(PersistCacheCommand $command): void
{
if (!$command->useCache()) {
return;
}
// TODO: Introduce Caching
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Handlers;
use League\Flysystem\FilesystemInterface;
use phpDocumentor\FileSystem\FileSystem;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Nodes\ProjectNode;
use phpDocumentor\Guides\Renderer\DocumentListIterator;
final class RenderCommand
{
private DocumentListIterator $documentIterator;
/** @param DocumentNode[] $documentArray */
public function __construct(
private readonly string $outputFormat,
private readonly array $documentArray,
private readonly FilesystemInterface|FileSystem $origin,
private readonly FilesystemInterface|FileSystem $destination,
private readonly ProjectNode $projectNode,
private readonly string $destinationPath = '/',
) {
$this->documentIterator = DocumentListIterator::create(
$this->projectNode->getRootDocumentEntry(),
$this->documentArray,
);
}
public function getOutputFormat(): string
{
return $this->outputFormat;
}
/** @return DocumentNode[] $documentArray */
public function getDocumentArray(): array
{
return $this->documentArray;
}
public function getDocumentIterator(): DocumentListIterator
{
return $this->documentIterator;
}
public function getOrigin(): FilesystemInterface|FileSystem
{
return $this->origin;
}
public function getDestination(): FilesystemInterface|FileSystem
{
return $this->destination;
}
public function getDestinationPath(): string
{
return $this->destinationPath;
}
public function getProjectNode(): ProjectNode
{
return $this->projectNode;
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Handlers;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\RenderContext;
final class RenderDocumentCommand
{
public function __construct(
private readonly DocumentNode $document,
private readonly RenderContext $renderContext,
) {
}
public function getDocument(): DocumentNode
{
return $this->document;
}
public function getContext(): RenderContext
{
return $this->renderContext;
}
public function getFileDestination(): string
{
return $this->renderContext->getDestinationPath() . '/' . $this->renderContext->getCurrentFileName() . '.' . $this->renderContext->getOutputFormat();
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Handlers;
use phpDocumentor\Guides\Event\PostRenderDocument;
use phpDocumentor\Guides\Event\PreRenderDocument;
use phpDocumentor\Guides\NodeRenderers\NodeRenderer;
use phpDocumentor\Guides\Nodes\DocumentNode;
use Psr\EventDispatcher\EventDispatcherInterface;
use function assert;
final class RenderDocumentHandler
{
/** @param NodeRenderer<DocumentNode> $renderer */
public function __construct(
private readonly NodeRenderer $renderer,
private readonly EventDispatcherInterface $eventDispatcher,
) {
}
public function handle(RenderDocumentCommand $command): void
{
$preRenderDocumentEvent = $this->eventDispatcher->dispatch(
new PreRenderDocument($this->renderer, $command),
);
assert($preRenderDocumentEvent instanceof PreRenderDocument);
$command->getContext()->getDestination()->put(
$command->getFileDestination(),
$this->renderer->render(
$command->getDocument(),
$command->getContext(),
),
);
$postRenderDocumentEvent = $this->eventDispatcher->dispatch(
new PostRenderDocument($this->renderer, $command),
);
assert($postRenderDocumentEvent instanceof PostRenderDocument);
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Handlers;
use phpDocumentor\Guides\Event\PostRenderProcess;
use phpDocumentor\Guides\Event\PreRenderProcess;
use phpDocumentor\Guides\Renderer\TypeRendererFactory;
use Psr\EventDispatcher\EventDispatcherInterface;
use function assert;
final class RenderHandler
{
public function __construct(
private readonly TypeRendererFactory $renderSetFactory,
private readonly EventDispatcherInterface $eventDispatcher,
) {
}
public function handle(RenderCommand $command): void
{
$preRenderProcessEvent = $this->eventDispatcher->dispatch(
new PreRenderProcess($command),
);
assert($preRenderProcessEvent instanceof PreRenderProcess);
if ($preRenderProcessEvent->isExitRendering()) {
return;
}
$renderSet = $this->renderSetFactory->getRenderSet($command->getOutputFormat());
$renderSet->render($command);
$postRenderProcessEvent = $this->eventDispatcher->dispatch(
new PostRenderProcess($command),
);
assert($postRenderProcessEvent instanceof PostRenderProcess);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides;
use phpDocumentor\Guides\Nodes\DocumentNode;
interface MarkupLanguageParser
{
public function supports(string $inputFormat): bool;
public function getParserContext(): ParserContext;
public function parse(ParserContext $parserContext, string $contents): DocumentNode;
public function getDocument(): DocumentNode;
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Meta;
class AnnotationTarget extends InternalTarget
{
public function __construct(string $documentPath, string $anchorName, private readonly string $name)
{
parent::__construct($documentPath, $anchorName);
}
public function getName(): string
{
return $this->name;
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Meta;
final class CitationTarget extends AnnotationTarget
{
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Meta;
final class ExternalTarget implements Target
{
public function __construct(
private readonly string $url,
private readonly string|null $title = null,
) {
}
public function getUrl(): string
{
return $this->url;
}
public function getTitle(): string|null
{
return $this->title;
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Meta;
final class FootnoteTarget extends AnnotationTarget
{
public function __construct(string $documentPath, string $anchorName, string $name, private int $number)
{
parent::__construct($documentPath, $anchorName, $name);
}
public function getNumber(): int
{
return $this->number;
}
public function setNumber(int $number): void
{
$this->number = $number;
$this->anchorName = 'footnote-' . $number;
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Meta;
use phpDocumentor\Guides\Nodes\SectionNode;
class InternalTarget implements Target
{
private string $url;
public function __construct(
private readonly string $documentPath,
protected string $anchorName,
private readonly string|null $title = null,
private readonly string $linkType = SectionNode::STD_LABEL,
private readonly string $prefix = '',
) {
}
public function getDocumentPath(): string
{
return $this->documentPath;
}
public function getAnchor(): string
{
return $this->anchorName;
}
public function setAnchorName(string $anchorName): void
{
$this->anchorName = $anchorName;
}
public function getTitle(): string|null
{
return $this->title;
}
public function getUrl(): string
{
return $this->url;
}
public function setUrl(string $url): InternalTarget
{
$this->url = $url;
return $this;
}
public function getLinkType(): string
{
return $this->linkType;
}
public function getPrefix(): string
{
return $this->prefix;
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\Meta;
interface Target
{
public function getUrl(): string;
public function getTitle(): string|null;
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\NodeRenderers;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\RenderContext;
use Psr\Log\LoggerInterface;
use function assert;
use function is_array;
use function is_string;
use function sprintf;
/** @implements NodeRenderer<Node> */
final class DefaultNodeRenderer implements NodeRenderer, NodeRendererFactoryAware
{
private NodeRendererFactory|null $nodeRendererFactory = null;
public function __construct(private readonly LoggerInterface $logger)
{
}
public function setNodeRendererFactory(NodeRendererFactory $nodeRendererFactory): void
{
$this->nodeRendererFactory = $nodeRendererFactory;
}
public function render(Node $node, RenderContext $renderContext): string
{
$value = $node->getValue();
if ($value instanceof Node) {
assert($this->nodeRendererFactory !== null);
return $this->nodeRendererFactory->get($value)->render($value, $renderContext);
}
if (is_array($value)) {
$returnValue = '';
foreach ($value as $child) {
if ($child instanceof Node) {
$returnValue .= $this->render($child, $renderContext);
} else {
$this->logger->error(
sprintf('The default renderer cannot be applied to node %s', $node::class),
$renderContext->getLoggerInformation(),
);
}
}
return $returnValue;
}
if (is_string($value)) {
return $value;
}
$this->logger->error(
sprintf('The default renderer cannot be applied to node %s', $node::class),
$renderContext->getLoggerInformation(),
);
return '';
}
public function supports(string $nodeFqcn): bool
{
return true;
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\NodeRenderers;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\RenderContext;
/** @implements NodeRenderer<Node> */
final class DelegatingNodeRenderer implements NodeRenderer, NodeRendererFactoryAware
{
private NodeRendererFactory $nodeRendererFactory;
public function setNodeRendererFactory(NodeRendererFactory $nodeRendererFactory): void
{
if (isset($this->nodeRendererFactory)) {
return;
}
$this->nodeRendererFactory = $nodeRendererFactory;
}
public function supports(string $nodeFqcn): bool
{
return true;
}
public function render(Node $node, RenderContext $renderContext): string
{
return $this->nodeRendererFactory->get($node)->render($node, $renderContext);
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\NodeRenderers\Html;
use InvalidArgumentException;
use phpDocumentor\Guides\NodeRenderers\NodeRenderer;
use phpDocumentor\Guides\Nodes\AdmonitionNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\RenderContext;
use phpDocumentor\Guides\TemplateRenderer;
use function implode;
use function is_a;
/** @implements NodeRenderer<AdmonitionNode> */
class AdmonitionNodeRenderer implements NodeRenderer
{
public function __construct(private readonly TemplateRenderer $renderer)
{
}
public function supports(string $nodeFqcn): bool
{
return $nodeFqcn === AdmonitionNode::class || is_a($nodeFqcn, AdmonitionNode::class, true);
}
public function render(Node $node, RenderContext $renderContext): string
{
if ($node instanceof AdmonitionNode === false) {
throw new InvalidArgumentException('Node must be an instance of ' . AdmonitionNode::class);
}
$classes = $node->getClasses();
return $this->renderer->renderTemplate(
$renderContext,
'body/admonition.html.twig',
[
'name' => $node->getName(),
'text' => $node->getText(),
'title' => $node->getTitle(),
'isTitled' => $node->isTitled(),
'class' => implode(' ', $classes),
'node' => $node->getValue(),
],
);
}
}

View File

@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\NodeRenderers\Html;
use phpDocumentor\Guides\NodeRenderers\NodeRenderer;
use phpDocumentor\Guides\Nodes\BreadCrumbNode;
use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode;
use phpDocumentor\Guides\Nodes\Menu\InternalMenuEntryNode;
use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\TitleNode;
use phpDocumentor\Guides\RenderContext;
use phpDocumentor\Guides\TemplateRenderer;
use function array_reverse;
use function assert;
use function is_a;
/**
* @template T as Node
* @implements NodeRenderer<BreadCrumbNode>
*/
final class BreadCrumbNodeRenderer implements NodeRenderer
{
private string $template = 'body/menu/breadcrumb.html.twig';
public function __construct(
private readonly TemplateRenderer $renderer,
) {
}
public function supports(string $nodeFqcn): bool
{
return $nodeFqcn === BreadCrumbNode::class || is_a($nodeFqcn, BreadCrumbNode::class, true);
}
/** @param T $node */
public function render(Node $node, RenderContext $renderContext): string
{
assert($node instanceof BreadCrumbNode);
$documentEntry = $renderContext->getCurrentDocumentEntry();
$data = [
'node' => $node,
'rootline' => $documentEntry === null ? [] :
$this->buildBreadcrumb(
$node,
$renderContext,
$documentEntry,
[],
$this->getBreadcrumbMaxLevel($node, $renderContext, $documentEntry, 0),
true,
),
];
return $this->renderer->renderTemplate(
$renderContext,
$this->template,
$data,
);
}
private function getBreadcrumbMaxLevel(
BreadCrumbNode $node,
RenderContext $renderContext,
DocumentEntryNode $documentEntry,
int $level,
): int {
if ($documentEntry->getParent() === null) {
if ($documentEntry->isRoot()) {
return $level;
}
// Current document has no parent but is not the root, add the overall root to the breadcrumb
$entries = $renderContext->getProjectNode()->getAllDocumentEntries();
foreach ($entries as $entry) {
if ($entry->isRoot() && $entry->getParent() === null) {
return $this->getBreadcrumbMaxLevel($node, $renderContext, $entry, ++$level);
}
}
return $level;
}
return $this->getBreadcrumbMaxLevel($node, $renderContext, $documentEntry->getParent(), ++$level);
}
/**
* @param MenuEntryNode[] $currentBreadcrumb
*
* @return MenuEntryNode[]
*/
private function buildBreadcrumb(
BreadCrumbNode $node,
RenderContext $renderContext,
DocumentEntryNode $documentEntry,
array $currentBreadcrumb,
int $level,
bool $isCurrent,
): array {
$title = $documentEntry->getTitle();
$navigationTitle = $documentEntry->getAdditionalData('navigationTitle');
if ($navigationTitle instanceof TitleNode) {
$title = $navigationTitle;
}
$entry = new InternalMenuEntryNode(
$documentEntry->getFile(),
$title,
[],
false,
$level,
'',
true,
$isCurrent,
);
$currentBreadcrumb[] = $entry;
if ($documentEntry->getParent() === null) {
if ($documentEntry->isRoot()) {
return array_reverse($currentBreadcrumb);
}
// Current document has no parent but is not the root, add the overall root to the breadcrumb
$entries = $renderContext->getProjectNode()->getAllDocumentEntries();
foreach ($entries as $entry) {
if ($entry->isRoot() && $entry->getParent() === null) {
return $this->buildBreadcrumb($node, $renderContext, $entry, $currentBreadcrumb, --$level, false);
}
}
return array_reverse($currentBreadcrumb);
}
return $this->buildBreadcrumb($node, $renderContext, $documentEntry->getParent(), $currentBreadcrumb, --$level, false);
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\NodeRenderers\Html;
use phpDocumentor\Guides\NodeRenderers\NodeRenderer;
use phpDocumentor\Guides\Nodes\DocumentNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\RenderContext;
use phpDocumentor\Guides\TemplateRenderer;
use function assert;
use function is_a;
/**
* @template T as Node
* @implements NodeRenderer<DocumentNode>
*/
final class DocumentNodeRenderer implements NodeRenderer
{
private string $template = 'structure/document.html.twig';
public function __construct(
private readonly TemplateRenderer $renderer,
) {
}
public function supports(string $nodeFqcn): bool
{
return $nodeFqcn === DocumentNode::class || is_a($nodeFqcn, DocumentNode::class, true);
}
/** @param T $node */
public function render(Node $node, RenderContext $renderContext): string
{
assert($node instanceof DocumentNode);
$data = [
'node' => $node,
'title' => $node->getPageTitle(),
'parts' => $node->getDocumentPartNodes(),
];
return $this->renderer->renderTemplate(
$renderContext,
$this->template,
$data,
);
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\NodeRenderers\Html;
use phpDocumentor\Guides\NodeRenderers\NodeRenderer;
use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\RenderContext;
use phpDocumentor\Guides\Renderer\UrlGenerator\UrlGeneratorInterface;
use phpDocumentor\Guides\TemplateRenderer;
use function is_a;
/** @implements NodeRenderer<MenuEntryNode> */
final class MenuEntryRenderer implements NodeRenderer
{
public function __construct(
private readonly TemplateRenderer $renderer,
private readonly UrlGeneratorInterface $urlGenerator,
) {
}
public function supports(string $nodeFqcn): bool
{
return $nodeFqcn === MenuEntryNode::class || is_a($nodeFqcn, MenuEntryNode::class, true);
}
public function render(Node $node, RenderContext $renderContext): string
{
$url = $this->urlGenerator->generateCanonicalOutputUrl($renderContext, $node->getUrl(), $node->getValue()?->getId());
return $this->renderer->renderTemplate(
$renderContext,
'body/menu/menu-item.html.twig',
[
'url' => $url,
'node' => $node,
],
);
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\NodeRenderers\Html;
use phpDocumentor\Guides\NodeRenderers\NodeRenderer;
use phpDocumentor\Guides\Nodes\Menu\ContentMenuNode;
use phpDocumentor\Guides\Nodes\Menu\MenuNode;
use phpDocumentor\Guides\Nodes\Menu\TocNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\RenderContext;
use phpDocumentor\Guides\TemplateRenderer;
use Webmozart\Assert\Assert;
use function is_a;
/** @implements NodeRenderer<MenuNode> */
final class MenuNodeRenderer implements NodeRenderer
{
public function __construct(private readonly TemplateRenderer $renderer)
{
}
public function render(Node $node, RenderContext $renderContext): string
{
Assert::isInstanceOf($node, MenuNode::class);
if ($node->getOption('hidden', false)) {
return '';
}
return $this->renderer->renderTemplate(
$renderContext,
$this->getTemplate($node),
['node' => $node],
);
}
private function getTemplate(Node $node): string
{
if ($node instanceof TocNode) {
return 'body/menu/table-of-content.html.twig';
}
if ($node instanceof ContentMenuNode) {
return 'body/menu/content-menu.html.twig';
}
return 'body/menu/menu.html.twig';
}
public function supports(string $nodeFqcn): bool
{
return $nodeFqcn === MenuNode::class || is_a($nodeFqcn, MenuNode::class, true);
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\NodeRenderers\Html;
use phpDocumentor\Guides\NodeRenderers\NodeRenderer;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\TableNode;
use phpDocumentor\Guides\RenderContext;
use phpDocumentor\Guides\TemplateRenderer;
use function is_a;
/** @implements NodeRenderer<TableNode> */
final class TableNodeRenderer implements NodeRenderer
{
public function __construct(private readonly TemplateRenderer $renderer)
{
}
public function render(Node $node, RenderContext $renderContext): string
{
$headers = $node->getHeaders();
$rows = $node->getData();
return $this->renderer->renderTemplate(
$renderContext,
'body/table.html.twig',
[
'tableNode' => $node,
'tableHeaderRows' => $headers,
'tableRows' => $rows,
],
);
}
public function supports(string $nodeFqcn): bool
{
return $nodeFqcn === TableNode::class || is_a($nodeFqcn, TableNode::class, true);
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\NodeRenderers;
use phpDocumentor\Guides\Nodes\Node;
final class InMemoryNodeRendererFactory implements NodeRendererFactory
{
/** @var array<class-string<Node>, NodeRenderer<Node>> */
private array $cache = [];
/**
* @param iterable<NodeRenderer<Node>> $nodeRenderers
* @param NodeRenderer<Node> $defaultNodeRenderer
*/
public function __construct(private readonly iterable $nodeRenderers, private readonly NodeRenderer $defaultNodeRenderer)
{
foreach ($nodeRenderers as $nodeRenderer) {
if (!$nodeRenderer instanceof NodeRendererFactoryAware) {
continue;
}
$nodeRenderer->setNodeRendererFactory($this);
}
if (!$defaultNodeRenderer instanceof NodeRendererFactoryAware) {
return;
}
$defaultNodeRenderer->setNodeRendererFactory($this);
}
public function get(Node $node): NodeRenderer
{
$nodeFqcn = $node::class;
if (isset($this->cache[$nodeFqcn])) {
return $this->cache[$nodeFqcn];
}
foreach ($this->nodeRenderers as $nodeRenderer) {
if ($nodeRenderer->supports($nodeFqcn)) {
return $this->cache[$nodeFqcn] = $nodeRenderer;
}
}
return $this->cache[$nodeFqcn] = $this->defaultNodeRenderer;
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\NodeRenderers\LaTeX;
use InvalidArgumentException;
use phpDocumentor\Guides\NodeRenderers\NodeRenderer;
use phpDocumentor\Guides\NodeRenderers\NodeRendererFactory;
use phpDocumentor\Guides\NodeRenderers\NodeRendererFactoryAware;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\SpanNode;
use phpDocumentor\Guides\Nodes\TableNode;
use phpDocumentor\Guides\RenderContext;
use function assert;
use function count;
use function implode;
use function is_a;
use function max;
/** @implements NodeRenderer<TableNode> */
final class TableNodeRenderer implements NodeRenderer, NodeRendererFactoryAware
{
private NodeRendererFactory|null $nodeRendererFactory = null;
public function setNodeRendererFactory(NodeRendererFactory $nodeRendererFactory): void
{
$this->nodeRendererFactory = $nodeRendererFactory;
}
public function render(Node $node, RenderContext $renderContext): string
{
if ($node instanceof TableNode === false) {
throw new InvalidArgumentException('Invalid node presented');
}
$cols = 0;
$rows = [];
foreach ($node->getData() as $row) {
$rowTex = '';
$cols = max($cols, count($row->getColumns()));
assert($this->nodeRendererFactory !== null);
foreach ($row->getColumns() as $n => $col) {
assert($col instanceof SpanNode);
$rowTex .= $this->nodeRendererFactory->get($col)->render($col, $renderContext);
if ((int) $n + 1 >= count($row->getColumns())) {
continue;
}
$rowTex .= ' & ';
}
$rowTex .= ' \\\\' . "\n";
$rows[] = $rowTex;
}
$aligns = [];
for ($i = 0; $i < $cols; $i++) {
$aligns[] = 'l';
}
$aligns = '|' . implode('|', $aligns) . '|';
$rows = "\\hline\n" . implode("\\hline\n", $rows) . "\\hline\n";
return "\\begin{tabular}{" . $aligns . "}\n" . $rows . "\n\\end{tabular}\n";
}
public function supports(string $nodeFqcn): bool
{
return $nodeFqcn === TableNode::class || is_a($nodeFqcn, TableNode::class, true);
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\NodeRenderers\LaTeX;
use InvalidArgumentException;
use phpDocumentor\Guides\NodeRenderers\NodeRenderer;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\Nodes\TitleNode;
use phpDocumentor\Guides\RenderContext;
use phpDocumentor\Guides\TemplateRenderer;
use function is_a;
/** @implements NodeRenderer<TitleNode> */
final class TitleNodeRenderer implements NodeRenderer
{
public function __construct(private readonly TemplateRenderer $renderer)
{
}
public function render(Node $node, RenderContext $renderContext): string
{
if ($node instanceof TitleNode === false) {
throw new InvalidArgumentException('Invalid node presented');
}
$type = 'chapter';
if ($node->getLevel() > 1) {
$type = 'section';
for ($i = 2; $i < $node->getLevel(); $i++) {
$type = 'sub' . $type;
}
}
return $this->renderer->renderTemplate(
$renderContext,
'page/header/title.tex.twig',
[
'type' => $type,
'titleNode' => $node,
],
);
}
public function supports(string $nodeFqcn): bool
{
return $nodeFqcn === TitleNode::class || is_a($nodeFqcn, TitleNode::class, true);
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\NodeRenderers\LaTeX;
use InvalidArgumentException;
use phpDocumentor\Guides\NodeRenderers\NodeRenderer;
use phpDocumentor\Guides\Nodes\Menu\TocNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\RenderContext;
use phpDocumentor\Guides\TemplateRenderer;
use function is_a;
/** @implements NodeRenderer<TocNode> */
final class TocNodeRenderer implements NodeRenderer
{
public function __construct(private readonly TemplateRenderer $renderer)
{
}
public function render(Node $node, RenderContext $renderContext): string
{
if ($node instanceof TocNode === false) {
throw new InvalidArgumentException('Invalid node presented');
}
return $this->renderer->renderTemplate(
$renderContext,
'toc.tex.twig',
['tocNode' => $node],
);
}
public function supports(string $nodeFqcn): bool
{
return $nodeFqcn === TocNode::class || is_a($nodeFqcn, TocNode::class, true);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\NodeRenderers;
use phpDocumentor\Guides\Nodes\Node;
final class LazyNodeRendererFactory implements NodeRendererFactory
{
/** @var callable */
private $factory;
private NodeRendererFactory|null $innerFactory = null;
public function __construct(callable $factory)
{
$this->factory = $factory;
}
public function get(Node $node): NodeRenderer
{
if ($this->innerFactory === null) {
$this->innerFactory = ($this->factory)();
}
return $this->innerFactory->get($node);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\NodeRenderers;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\RenderContext;
/** @template T of Node */
interface NodeRenderer
{
/** @param class-string<Node> $nodeFqcn */
public function supports(string $nodeFqcn): bool;
/** @param T $node */
public function render(Node $node, RenderContext $renderContext): string;
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\NodeRenderers;
use phpDocumentor\Guides\Nodes\Node;
interface NodeRendererFactory
{
/** @return NodeRenderer<Node> */
public function get(Node $node): NodeRenderer;
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://phpdoc.org
*/
namespace phpDocumentor\Guides\NodeRenderers;
interface NodeRendererFactoryAware
{
public function setNodeRendererFactory(NodeRendererFactory $nodeRendererFactory): void;
}

Some files were not shown because too many files have changed in this diff Show More