diff --git a/packages/guides-restructured-text/resources/config/guides-restructured-text.php b/packages/guides-restructured-text/resources/config/guides-restructured-text.php index 91033f6fb..27a46b862 100644 --- a/packages/guides-restructured-text/resources/config/guides-restructured-text.php +++ b/packages/guides-restructured-text/resources/config/guides-restructured-text.php @@ -4,6 +4,7 @@ use phpDocumentor\Guides\Graphs\Directives\UmlDirective; use phpDocumentor\Guides\ReferenceResolvers\DocumentNameResolverInterface; +use phpDocumentor\Guides\RestructuredText\Compiler\Passes\DirectiveProcessPass; use phpDocumentor\Guides\RestructuredText\Directives\AdmonitionDirective; use phpDocumentor\Guides\RestructuredText\Directives\AttentionDirective; use phpDocumentor\Guides\RestructuredText\Directives\BaseDirective; @@ -277,6 +278,7 @@ ->tag('phpdoc.guides.parser.rst.body_element', ['priority' => ParagraphRule::PRIORITY + 1]) ->set(DirectiveRule::class) ->arg('$directives', tagged_iterator('phpdoc.guides.directive')) + ->arg('$startingRule', service(DirectiveContentRule::class)) ->tag('phpdoc.guides.parser.rst.body_element', ['priority' => DirectiveRule::PRIORITY]) ->set(CommentRule::class) ->tag('phpdoc.guides.parser.rst.body_element', ['priority' => CommentRule::PRIORITY]) @@ -374,6 +376,10 @@ ->set(GlobSearcher::class) ->set(ToctreeBuilder::class) ->set(InlineMarkupRule::class) + + ->set(DirectiveProcessPass::class) + ->arg('$directives', tagged_iterator('phpdoc.guides.directive')) + ->tag('phpdoc.guides.compiler.nodeTransformers') ->set(DefaultCodeNodeOptionMapper::class) ->alias(CodeNodeOptionMapper::class, DefaultCodeNodeOptionMapper::class); }; diff --git a/packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php b/packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php new file mode 100644 index 000000000..a2922e61b --- /dev/null +++ b/packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php @@ -0,0 +1,86 @@ + */ +final class DirectiveProcessPass implements ReverseNodeTransformer +{ + /** @var array */ + private array $directives; + + /** @param iterable $directives */ + public function __construct( + private readonly LoggerInterface $logger, + private readonly GeneralDirective $generalDirective, + iterable $directives = [], + ) { + foreach ($directives as $directive) { + $this->registerDirective($directive); + } + } + + private function registerDirective(DirectiveHandler $directive): void + { + $this->directives[strtolower($directive->getName())] = $directive; + foreach ($directive->getAliases() as $alias) { + $this->directives[strtolower($alias)] = $directive; + } + } + + public function enterNode(Node $node, CompilerContext $compilerContext): Node + { + return $node; + } + + public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null + { + $newNode = $this->getDirectiveHandler($node->getDirective())->createNode($node); + if ($newNode === null) { + return null; + } + + $newNode->setClasses($node->getClasses()); + + return $newNode; + } + + private function getDirectiveHandler(Directive $directive): DirectiveHandler + { + return $this->directives[strtolower($directive->getName())] ?? $this->generalDirective; + } + + public function supports(Node $node): bool + { + return $node instanceof DirectiveNode; + } + + public function getPriority(): int + { + return 100; + } +} diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/AbstractAdmonitionDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/AbstractAdmonitionDirective.php index 89e0f6dc5..eeeb93c0f 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/AbstractAdmonitionDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/AbstractAdmonitionDirective.php @@ -13,10 +13,12 @@ namespace phpDocumentor\Guides\RestructuredText\Directives; +use Doctrine\Deprecations\Deprecation; use phpDocumentor\Guides\Nodes\AdmonitionNode; use phpDocumentor\Guides\Nodes\CollectionNode; use phpDocumentor\Guides\Nodes\Node; use phpDocumentor\Guides\Nodes\ParagraphNode; +use phpDocumentor\Guides\RestructuredText\Nodes\DirectiveNode; use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; use phpDocumentor\Guides\RestructuredText\Parser\Directive; use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; @@ -39,22 +41,26 @@ final protected function processSub( CollectionNode $collectionNode, Directive $directive, ): Node|null { - $children = $collectionNode->getChildren(); + return $this->createNode( + new DirectiveNode( + $directive, + $collectionNode->getChildren(), + ), + ); + } - if ($directive->getDataNode() !== null) { - array_unshift($children, new ParagraphNode([$directive->getDataNode()])); + public function createNode(DirectiveNode $directiveNode): Node|null + { + $children = $directiveNode->getChildren(); + if ($directiveNode->getDirective()->getDataNode() !== null) { + array_unshift($children, new ParagraphNode([$directiveNode->getDirective()->getDataNode()])); } return new AdmonitionNode( - $this->name, + $directiveNode->getDirective()->getName(), null, $this->text, $children, ); } - - final public function getName(): string - { - return $this->name; - } } diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/AbstractVersionChangeDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/AbstractVersionChangeDirective.php index be90f7e5b..8a39f61c6 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/AbstractVersionChangeDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/AbstractVersionChangeDirective.php @@ -13,8 +13,10 @@ namespace phpDocumentor\Guides\RestructuredText\Directives; +use Doctrine\Deprecations\Deprecation; use phpDocumentor\Guides\Nodes\CollectionNode; use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\RestructuredText\Nodes\DirectiveNode; use phpDocumentor\Guides\RestructuredText\Nodes\VersionChangeNode; use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; use phpDocumentor\Guides\RestructuredText\Parser\Directive; @@ -37,16 +39,39 @@ final protected function processSub( CollectionNode $collectionNode, Directive $directive, ): Node|null { - return new VersionChangeNode( - $this->type, - $this->label, - $directive->getData(), - $collectionNode->getChildren(), + return $this->createNode( + new DirectiveNode( + $directive, + $collectionNode->getChildren(), + ), ); } - final public function getName(): string + public function getName(): string { - return $this->type; + try { + return parent::getName(); + } catch (\LogicException) { + Deprecation::trigger( + 'phpdocumentor/guides-restructured-text', + 'TODO: link', + sprintf( + 'Directives without attributes are deprecated, consult the documentation for more information on how to update your directives. Directive: %s', + $this->type, + ), + ); + + return $this->type; + } + } + + public function createNode(DirectiveNode $directiveNode): Node|null + { + return new VersionChangeNode( + $this->type, + $this->label, + $directiveNode->getDirective()->getData(), + $directiveNode->getChildren(), + ); } } diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/AdmonitionDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/AdmonitionDirective.php index 220daf89b..d3e9e0006 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/AdmonitionDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/AdmonitionDirective.php @@ -16,6 +16,7 @@ use phpDocumentor\Guides\Nodes\AdmonitionNode; use phpDocumentor\Guides\Nodes\CollectionNode; use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\RestructuredText\Nodes\DirectiveNode; use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; use phpDocumentor\Guides\RestructuredText\Parser\Directive; @@ -34,13 +35,9 @@ * * @see https://docutils.sourceforge.io/docs/ref/rst/directives.html#generic-admonition */ +#[Attributes\Directive(name: 'admonition')] final class AdmonitionDirective extends SubDirective { - public function getName(): string - { - return 'admonition'; - } - /** {@inheritDoc} * * @param Directive $directive @@ -50,22 +47,32 @@ protected function processSub( CollectionNode $collectionNode, Directive $directive, ): Node|null { + return $this->createNode( + new DirectiveNode( + $directive, + $collectionNode->getChildren(), + ), + ); + } + + public function createNode(DirectiveNode $directiveNode): Node|null + { // The title argument is required per the RST spec. // Skip rendering if no title is provided. - if ($directive->getData() === '') { + if ($directiveNode->getDirective()->getData() === '') { return null; } $name = trim( - preg_replace('/[^0-9a-zA-Z]+/', '-', strtolower($directive->getData())) ?? '', + preg_replace('/[^0-9a-zA-Z]+/', '-', strtolower($directiveNode->getDirective()->getData())) ?? '', '-', ); return new AdmonitionNode( $name, - $directive->getDataNode(), - $directive->getData(), - $collectionNode->getChildren(), + $directiveNode->getDirective()->getDataNode(), + $directiveNode->getDirective()->getData(), + $directiveNode->getChildren(), true, ); } diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/AttentionDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/AttentionDirective.php index 3a93f8f0e..856f5d9ac 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/AttentionDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/AttentionDirective.php @@ -26,6 +26,7 @@ * This is an attention admonition. * ``` */ +#[Attributes\Directive(name: 'attention')] final class AttentionDirective extends AbstractAdmonitionDirective { public function __construct(protected Rule $startingRule) diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Directive.php b/packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Directive.php new file mode 100644 index 000000000..07513c24e --- /dev/null +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Directive.php @@ -0,0 +1,27 @@ + Cache of Option attributes indexed by option name */ + private array $optionAttributeCache; + + private string $name; + + /** @var string[] */ + private array $aliases; + /** * Get the directive name */ - abstract public function getName(): string; + public function getName(): string + { + if (isset($this->name)) { + return $this->name; + } + + $reflection = new ReflectionClass($this); + $attributes = $reflection->getAttributes(Attributes\Directive::class); + + if (count($attributes) === 0) { + throw new LogicException('Directive class must have a Directive attribute'); + } + + $this->name = $attributes[0]->newInstance()->name; + + return $this->name; + } /** * Allow a directive to be registered under multiple names. @@ -50,7 +82,35 @@ abstract public function getName(): string; */ public function getAliases(): array { - return []; + if (isset($this->aliases)) { + return $this->aliases; + } + + $reflection = new ReflectionClass($this); + $attributes = $reflection->getAttributes(Attributes\Directive::class); + $this->aliases = []; + if (count($attributes) !== 0) { + $this->aliases = $attributes[0]->newInstance()->aliases; + } + + return $this->aliases; + } + + /** + * Returns whether this directive has been upgraded to a new version. + * + * In the new version of directives, the processing is done during the compile phase. + * This method only exists to allow for backward compatibility with directives that + * were written before the upgrade. + * + * @internal + */ + final public function isUpgraded(): bool + { + $reflection = new ReflectionClass($this); + $attributes = $reflection->getAttributes(Attributes\Directive::class); + + return count($attributes) === 1; } /** @@ -85,6 +145,11 @@ public function processNode( return new GenericNode($directive->getVariable(), $directive->getData()); } + public function createNode(DirectiveNode $directiveNode): Node|null + { + return null; + } + /** * @param DirectiveOption[] $options * @@ -94,4 +159,84 @@ protected function optionsToArray(array $options): array { return array_map(static fn (DirectiveOption $option): bool|float|int|string|null => $option->getValue(), $options); } + + /** + * Gets an option value from a directive based on attribute configuration. + * + * Looks up the option in the directive and returns its value converted to the + * appropriate type based on the Option attribute defined on this directive class. + * If the option is not present in the directive, returns the default value from the attribute. + * + * @param Directive $directive The directive containing the options + * @param string $optionName The name of the option to retrieve + * + * @return mixed The option value converted to the appropriate type, or the default value + */ + final protected function readOption(Directive $directive, string $optionName): mixed + { + $optionAttribute = $this->findOptionAttribute($optionName); + + return $this->getOptionValue($directive, $optionAttribute); + } + + /** @return array */ + final protected function readAllOptions(Directive $directive): array + { + $this->initialize(); + + return array_map( + fn (Option $option) => $this->getOptionValue($directive, $option), + $this->optionAttributeCache, + ); + } + + private function getOptionValue(Directive $directive, Option|null $option): mixed + { + if ($option === null) { + return null; + } + + if (!$directive->hasOption($option->name)) { + return $option->default; + } + + $directiveOption = $directive->getOption($option->name); + $value = $directiveOption->getValue(); + + return match ($option->type) { + OptionType::Integer => (int) $value, + OptionType::Boolean => $value === null || filter_var($value, FILTER_VALIDATE_BOOL), + OptionType::String => (string) $value, + OptionType::Array => (array) $value, + }; + } + + /** + * Finds the Option attribute for the given option name on the current class. + * + * @param string $optionName The option name to look for + * + * @return Option|null The Option attribute if found, null otherwise + */ + private function findOptionAttribute(string $optionName): Option|null + { + $this->initialize(); + + return $this->optionAttributeCache[$optionName] ?? null; + } + + private function initialize(): void + { + if (isset($this->optionAttributeCache)) { + return; + } + + $reflection = new ReflectionClass($this); + $attributes = $reflection->getAttributes(Option::class); + $this->optionAttributeCache = []; + foreach ($attributes as $attribute) { + $option = $attribute->newInstance(); + $this->optionAttributeCache[$option->name] = $option; + } + } } diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/BreadcrumbDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/BreadcrumbDirective.php index 8d0e130eb..e66a34930 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/BreadcrumbDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/BreadcrumbDirective.php @@ -15,6 +15,7 @@ use phpDocumentor\Guides\Nodes\BreadCrumbNode; use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\RestructuredText\Nodes\DirectiveNode; use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; use phpDocumentor\Guides\RestructuredText\Parser\Directive; @@ -30,17 +31,11 @@ * .. breadcrumb:: * ``` */ +#[Attributes\Directive(name: 'breadcrumb')] final class BreadcrumbDirective extends BaseDirective { - public function getName(): string + public function createNode(DirectiveNode $directiveNode): Node|null { - return 'breadcrumb'; - } - - public function processNode( - BlockContext $blockContext, - Directive $directive, - ): Node { return new BreadCrumbNode(); } } diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/CautionDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/CautionDirective.php index 45a2ac18a..a27c31480 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/CautionDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/CautionDirective.php @@ -26,6 +26,7 @@ * This is a caution admonition. * ``` */ +#[Attributes\Directive(name: 'caution')] final class CautionDirective extends AbstractAdmonitionDirective { public function __construct(protected Rule $startingRule) diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/ConfvalDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/ConfvalDirective.php index 64d36df2f..a741291e6 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/ConfvalDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/ConfvalDirective.php @@ -16,6 +16,7 @@ use phpDocumentor\Guides\Nodes\CollectionNode; use phpDocumentor\Guides\Nodes\Node; use phpDocumentor\Guides\ReferenceResolvers\AnchorNormalizer; +use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option; use phpDocumentor\Guides\RestructuredText\Nodes\ConfvalNode; use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; use phpDocumentor\Guides\RestructuredText\Parser\Directive; @@ -32,6 +33,11 @@ * * https://sphinx-toolbox.readthedocs.io/en/stable/extensions/confval.html */ +#[Option(name: 'name', description: 'Id of the configuration value, used for linking to it.')] +#[Option(name: 'type', description: 'Type of the configuration value, e.g. "string", "int", etc.')] +#[Option(name: 'required', type: OptionType::Boolean, default: false, description: 'Whether the configuration value is required or not.')] +#[Option(name: 'default', description: 'Default value of the configuration value, if any.')] +#[Option(name: 'noindex', type: OptionType::Boolean, default: false, description: 'Whether the configuration value should not be indexed.')] final class ConfvalDirective extends SubDirective { public const NAME = 'confval'; @@ -80,16 +86,16 @@ protected function processSub( } if ($directive->hasOption('type')) { - $type = $this->inlineParser->parse($directive->getOptionString('type'), $blockContext); + $type = $this->inlineParser->parse($this->readOption($directive, 'type'), $blockContext); } - $required = $directive->getOptionBool('required'); + $required = $this->readOption($directive, 'required'); if ($directive->hasOption('default')) { - $default = $this->inlineParser->parse($directive->getOptionString('default'), $blockContext); + $default = $this->inlineParser->parse($this->readOption($directive, 'default'), $blockContext); } - $noindex = $directive->getOptionBool('noindex'); + $noindex = $this->readOption($directive, 'noindex'); foreach ($directive->getOptions() as $option) { if (in_array($option->getName(), ['type', 'required', 'default', 'noindex', 'name'], true)) { diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/ContentsDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/ContentsDirective.php index 201208e3d..7905fbb45 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/ContentsDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/ContentsDirective.php @@ -17,6 +17,7 @@ use phpDocumentor\Guides\Nodes\Menu\SectionMenuEntryNode; use phpDocumentor\Guides\Nodes\Node; use phpDocumentor\Guides\ReferenceResolvers\DocumentNameResolverInterface; +use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option; use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; use phpDocumentor\Guides\RestructuredText\Parser\Directive; @@ -25,6 +26,8 @@ * * Displays a table of content of the current page */ +#[Option(name: 'local', type: OptionType::Boolean, description: 'If set, the table of contents will only include sections that are local to the current document.', default: false)] +#[Option(name: 'depth', description: 'The maximum depth of the table of contents.')] final class ContentsDirective extends BaseDirective { public function __construct( @@ -51,6 +54,6 @@ public function process( return (new ContentMenuNode([new SectionMenuEntryNode($absoluteUrl)])) ->withOptions($this->optionsToArray($options)) ->withCaption($directive->getDataNode()) - ->withLocal($directive->hasOption('local')); + ->withLocal($this->readOption($directive, 'local')); } } diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/DangerDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/DangerDirective.php index 51e4a0cec..2bb35647e 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/DangerDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/DangerDirective.php @@ -26,6 +26,7 @@ * This is a danger admonition. * ``` */ +#[Attributes\Directive(name: 'danger')] class DangerDirective extends AbstractAdmonitionDirective { public function __construct(protected Rule $startingRule) diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/DeprecatedDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/DeprecatedDirective.php index 80354d6c6..27be1d91c 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/DeprecatedDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/DeprecatedDirective.php @@ -15,6 +15,7 @@ use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; +#[Attributes\Directive(name: 'deprecated')] final class DeprecatedDirective extends AbstractVersionChangeDirective { public function __construct(protected Rule $startingRule) diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/DocumentBlockDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/DocumentBlockDirective.php index b61d74e09..e501f9eab 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/DocumentBlockDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/DocumentBlockDirective.php @@ -16,9 +16,11 @@ use phpDocumentor\Guides\Nodes\CollectionNode; use phpDocumentor\Guides\Nodes\DocumentBlockNode; use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option; use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; use phpDocumentor\Guides\RestructuredText\Parser\Directive; +#[Option(name: 'identifier', description: 'The identifier of the document block')] final class DocumentBlockDirective extends SubDirective { public function getName(): string @@ -39,7 +41,7 @@ protected function processSub( return new DocumentBlockNode( $collectionNode->getChildren(), - $identifier, + $this->readOption($directive, 'identifier'), ); } } diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/ErrorDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/ErrorDirective.php index faa5d1e21..bd9e98898 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/ErrorDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/ErrorDirective.php @@ -26,6 +26,7 @@ * This is an error admonition. * ``` */ +#[Attributes\Directive(name: 'error')] final class ErrorDirective extends AbstractAdmonitionDirective { public function __construct(protected Rule $startingRule) diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/FigureDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/FigureDirective.php index 0e18dae88..04f9b9ed5 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/FigureDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/FigureDirective.php @@ -18,6 +18,7 @@ use phpDocumentor\Guides\Nodes\ImageNode; use phpDocumentor\Guides\Nodes\Node; use phpDocumentor\Guides\ReferenceResolvers\DocumentNameResolverInterface; +use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option; use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; use phpDocumentor\Guides\RestructuredText\Parser\Directive; use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; @@ -33,6 +34,14 @@ * * Here is an awesome caption */ +#[Option(name: 'width', description: 'Width of the image in pixels')] +#[Option(name: 'height', description: 'Height of the image in pixels')] +#[Option(name: 'alt', description: 'Alternative text for the image')] +#[Option(name: 'scale', description: 'Scale of the image, e.g. 0.5 for half size')] +#[Option(name: 'target', description: 'Target for the image, e.g. a link to the image')] +#[Option(name: 'class', description: 'CSS class to apply to the image')] +#[Option(name: 'name', description: 'Name of the image, used for references')] +#[Option(name: 'align', description: 'Alignment of the image, e.g. left, right, center')] final class FigureDirective extends SubDirective { public function __construct( diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/HintDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/HintDirective.php index 1298506f1..2d2904a2b 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/HintDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/HintDirective.php @@ -26,6 +26,7 @@ * This is a hint admonition. * ``` */ +#[Attributes\Directive(name: 'hint')] final class HintDirective extends AbstractAdmonitionDirective { public function __construct(protected Rule $startingRule) diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/ImageDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/ImageDirective.php index c1252a38d..e6d2c17fd 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/ImageDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/ImageDirective.php @@ -20,6 +20,7 @@ use phpDocumentor\Guides\Nodes\Inline\ReferenceNode; use phpDocumentor\Guides\Nodes\Node; use phpDocumentor\Guides\ReferenceResolvers\DocumentNameResolverInterface; +use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option; use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; use phpDocumentor\Guides\RestructuredText\Parser\Directive; @@ -37,6 +38,14 @@ * :width: 100 * :title: An image */ +#[Option(name: 'width', description: 'Width of the image in pixels')] +#[Option(name: 'height', description: 'Height of the image in pixels')] +#[Option(name: 'alt', description: 'Alternative text for the image')] +#[Option(name: 'scale', description: 'Scale of the image, e.g. 0.5 for half size')] +#[Option(name: 'target', description: 'Target for the image, e.g. a link to the image')] +#[Option(name: 'class', description: 'CSS class to apply to the image')] +#[Option(name: 'name', description: 'Name of the image, used for references')] +#[Option(name: 'align', description: 'Alignment of the image, e.g. left, right, center')] final class ImageDirective extends BaseDirective { /** @see https://regex101.com/r/9dUrzu/3 */ @@ -67,8 +76,11 @@ public function processNode( ), ); if ($directive->hasOption('target')) { - $targetReference = (string) $directive->getOption('target')->getValue(); - $node->setTarget($this->resolveLinkTarget($targetReference)); + $node->setTarget( + $this->resolveLinkTarget( + $this->readOption($directive, 'target'), + ), + ); } return $node; diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/ImportantDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/ImportantDirective.php index 2a3fb96b6..a0cfb5b16 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/ImportantDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/ImportantDirective.php @@ -26,6 +26,7 @@ * This is a important admonition. * ``` */ +#[Attributes\Directive(name: 'important')] final class ImportantDirective extends AbstractAdmonitionDirective { public function __construct(protected Rule $startingRule) diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/IncludeDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/IncludeDirective.php index 123397189..2718aee26 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/IncludeDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/IncludeDirective.php @@ -17,6 +17,7 @@ use phpDocumentor\Guides\Nodes\CollectionNode; use phpDocumentor\Guides\Nodes\LiteralBlockNode; use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option; use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; use phpDocumentor\Guides\RestructuredText\Parser\Directive; use phpDocumentor\Guides\RestructuredText\Parser\Productions\DocumentRule; @@ -27,6 +28,8 @@ use function sprintf; use function str_replace; +#[Option(name: 'literal', description: 'If set, the contents will be rendered as a literal block.')] +#[Option(name: 'code', description: 'If set, the contents will be rendered as a code block with the specified language.')] final class IncludeDirective extends BaseDirective { public function __construct(private readonly DocumentRule $startingRule) diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/NoteDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/NoteDirective.php index 953386a21..66f2449ef 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/NoteDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/NoteDirective.php @@ -26,6 +26,7 @@ * This is a note admonition. * ``` */ +#[Attributes\Directive(name: 'note')] final class NoteDirective extends AbstractAdmonitionDirective { public function __construct(protected Rule $startingRule) diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/OptionType.php b/packages/guides-restructured-text/src/RestructuredText/Directives/OptionType.php new file mode 100644 index 000000000..76d956b72 --- /dev/null +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/OptionType.php @@ -0,0 +1,22 @@ +createNode(new DirectiveNode($directive)); + } + + public function createNode(DirectiveNode $directiveNode): EmbeddedFrame + { $node = new EmbeddedFrame( - 'https://www.youtube-nocookie.com/embed/' . $directive->getData(), + 'https://www.youtube-nocookie.com/embed/' . $directiveNode->getDirective()->getData(), ); - return $node->withOptions( - array_filter( - [ - 'width' => $directive->getOption('width')->getValue() ?? 560, - 'title' => $directive->getOption('title')->getValue(), - 'height' => $directive->getOption('height')->getValue() ?? 315, - 'allow' => $directive->getOption('allow')->getValue() ?? 'encrypted-media; picture-in-picture; web-share', - 'allowfullscreen' => (bool) ($directive->getOption('allowfullscreen')->getValue() ?? true), - ], - ), - ); + return $node->withOptions($this->readAllOptions($directiveNode->getDirective())); } } diff --git a/packages/guides-restructured-text/src/RestructuredText/Nodes/DirectiveNode.php b/packages/guides-restructured-text/src/RestructuredText/Nodes/DirectiveNode.php new file mode 100644 index 000000000..b9b692aa2 --- /dev/null +++ b/packages/guides-restructured-text/src/RestructuredText/Nodes/DirectiveNode.php @@ -0,0 +1,32 @@ + */ +final class DirectiveNode extends CompoundNode +{ + public function __construct(private readonly Directive $directive, array $children = []) + { + parent::__construct($children); + } + + public function getDirective(): Directive + { + return $this->directive; + } +} diff --git a/packages/guides-restructured-text/src/RestructuredText/Parser/Directive.php b/packages/guides-restructured-text/src/RestructuredText/Parser/Directive.php index a0ccb9b46..e237e65bf 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Parser/Directive.php +++ b/packages/guides-restructured-text/src/RestructuredText/Parser/Directive.php @@ -42,7 +42,7 @@ public function getVariable(): string public function getName(): string { - return $this->name; + return strtolower($this->name); } public function getData(): string @@ -60,7 +60,7 @@ public function addOption(DirectiveOption $value): void { $this->options[$value->getName()] = $value; } - + public function hasOption(string $name): bool { return isset($this->options[$name]); diff --git a/packages/guides-restructured-text/src/RestructuredText/Parser/Productions/DirectiveContentRule.php b/packages/guides-restructured-text/src/RestructuredText/Parser/Productions/DirectiveContentRule.php index 9498de862..cb33455da 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Parser/Productions/DirectiveContentRule.php +++ b/packages/guides-restructured-text/src/RestructuredText/Parser/Productions/DirectiveContentRule.php @@ -18,7 +18,7 @@ use phpDocumentor\Guides\Nodes\Node; use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; -/** @implements Rule */ +/** @implements Rule */ final class DirectiveContentRule implements Rule { public function __construct(private readonly RuleContainer $bodyElements) @@ -32,7 +32,7 @@ public function applies(BlockContext $blockContext): bool public function apply(BlockContext $blockContext, CompoundNode|null $on = null): Node|null { - $node = new CollectionNode([]); + $node = $on ?? new CollectionNode([]); $documentIterator = $blockContext->getDocumentIterator(); // We explicitly do not use foreach, but rather the cursors of the DocumentIterator // this is done because we are transitioning to a method where a Substate can take the current @@ -40,7 +40,7 @@ public function apply(BlockContext $blockContext, CompoundNode|null $on = null): while ($documentIterator->valid()) { $this->bodyElements->apply($blockContext, $node); } - + return $node; } } diff --git a/packages/guides-restructured-text/src/RestructuredText/Parser/Productions/DirectiveRule.php b/packages/guides-restructured-text/src/RestructuredText/Parser/Productions/DirectiveRule.php index 178eb004a..bf131276b 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Parser/Productions/DirectiveRule.php +++ b/packages/guides-restructured-text/src/RestructuredText/Parser/Productions/DirectiveRule.php @@ -18,6 +18,7 @@ use phpDocumentor\Guides\Nodes\Node; use phpDocumentor\Guides\RestructuredText\Directives\BaseDirective as DirectiveHandler; use phpDocumentor\Guides\RestructuredText\Directives\GeneralDirective; +use phpDocumentor\Guides\RestructuredText\Nodes\DirectiveNode; use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; use phpDocumentor\Guides\RestructuredText\Parser\Buffer; use phpDocumentor\Guides\RestructuredText\Parser\Directive; @@ -48,12 +49,16 @@ final class DirectiveRule implements Rule /** @var array */ private array $directives; - /** @param iterable $directives */ + /** + * @param iterable $directives + * @param Rule|null $startingRule + */ public function __construct( private readonly InlineMarkupRule $inlineMarkupRule, private readonly LoggerInterface $logger, private readonly GeneralDirective $generalDirective, iterable $directives = [], + private readonly Rule|null $startingRule = null, ) { foreach ($directives as $directive) { $this->registerDirective($directive); @@ -84,12 +89,30 @@ public function apply(BlockContext $blockContext, CompoundNode|null $on = null): } $this->parseDirectiveContent($directive, $blockContext); + $this->interpretDirectiveOptions($documentIterator, $directive); $directiveHandler = $this->getDirectiveHandler($directive); - - $this->interpretDirectiveOptions($documentIterator, $directive); $buffer = $this->collectDirectiveContents($documentIterator); + if ($this->startingRule !== null && $directiveHandler->isUpgraded()) { + $node = $this->startingRule->apply( + new BlockContext($blockContext->getDocumentParserContext(), $buffer->getLinesString(), true, $documentIterator->key()), + new DirectiveNode($directive), + ); + + if ($node === null) { + return null; + } + + if ($directive->getVariable() === '') { + return $node; + } + + $blockContext->getDocumentParserContext()->getDocument()->addVariable($directive->getVariable(), $node); + + return null; + } + // Processing the Directive, the handler is responsible for adding the right Nodes to the document. try { $node = $directiveHandler->process( diff --git a/packages/guides/src/Compiler/DocumentNodeTraverser.php b/packages/guides/src/Compiler/DocumentNodeTraverser.php index 0e9286941..9d6158aab 100644 --- a/packages/guides/src/Compiler/DocumentNodeTraverser.php +++ b/packages/guides/src/Compiler/DocumentNodeTraverser.php @@ -50,6 +50,12 @@ private function traverseForTransformer( TreeNode $shadowNode, CompilerContext $compilerContext, ): void { + if ($transformer instanceof ReverseNodeTransformer) { + foreach ($shadowNode->getChildren() as $shadowChild) { + $this->traverseForTransformer($transformer, $shadowChild, $compilerContext->withShadowTree($shadowChild)); + } + } + $node = $shadowNode->getNode(); $supports = $transformer->supports($node); @@ -60,8 +66,10 @@ private function traverseForTransformer( } } - foreach ($shadowNode->getChildren() as $shadowChild) { - $this->traverseForTransformer($transformer, $shadowChild, $compilerContext->withShadowTree($shadowChild)); + if ($transformer instanceof ReverseNodeTransformer === false) { + foreach ($shadowNode->getChildren() as $shadowChild) { + $this->traverseForTransformer($transformer, $shadowChild, $compilerContext->withShadowTree($shadowChild)); + } } if (!$supports) { diff --git a/packages/guides/src/Compiler/NodeTransformers/VariableInlineNodeTransformer.php b/packages/guides/src/Compiler/NodeTransformers/VariableInlineNodeTransformer.php index 7d2a0c6b5..0bbd81f03 100644 --- a/packages/guides/src/Compiler/NodeTransformers/VariableInlineNodeTransformer.php +++ b/packages/guides/src/Compiler/NodeTransformers/VariableInlineNodeTransformer.php @@ -48,15 +48,15 @@ public function leaveNode(Node $node, CompilerContextInterface $compilerContext) $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 $nodeReplacement; } + $this->logger->warning( + 'No replacement was found for variable |' . $node->getValue() . '|', + $compilerContext->getLoggerInformation(), + ); + $node->setChild(new PlainTextInlineNode('|' . $node->getValue() . '|')); + return $node; } diff --git a/packages/guides/src/Compiler/ReverseNodeTransformer.php b/packages/guides/src/Compiler/ReverseNodeTransformer.php new file mode 100644 index 000000000..cb9ed571d --- /dev/null +++ b/packages/guides/src/Compiler/ReverseNodeTransformer.php @@ -0,0 +1,15 @@ +getContainer()->get(Compiler::class); assert($compiler instanceof Compiler); $projectNode = new ProjectNode(); - $compiler->run([$document], new CompilerContext($projectNode)); + [$document] = $compiler->run([$document], new CompilerContext($projectNode)); $inputFilesystem = FlySystemAdapter::createFromFileSystem(new Filesystem(new InMemoryFilesystemAdapter())); $inputFilesystem->put('img/test-image.jpg', 'Some image');