From f44010f1b27ac54b7348862915c2af7f60439dc3 Mon Sep 17 00:00:00 2001 From: Jaapio Date: Tue, 17 Jun 2025 22:37:02 +0200 Subject: [PATCH 1/5] First iteration on option attributes The new option attribute makes it easier to validate directives. An extra advantage is that we can use the new attributes to document the directive options. --- .../Directives/Attributes/Option.php | 22 +++++ .../Directives/BaseDirective.php | 87 +++++++++++++++++++ .../Directives/ConfvalDirective.php | 14 ++- .../Directives/ContentsDirective.php | 5 +- .../Directives/CsvTableDirective.php | 1 + .../Directives/DocumentBlockDirective.php | 4 +- .../Directives/FigureDirective.php | 9 ++ .../Directives/ImageDirective.php | 16 +++- .../Directives/IncludeDirective.php | 3 + .../Directives/OptionType.php | 13 +++ .../Directives/YoutubeDirective.php | 18 ++-- 11 files changed, 173 insertions(+), 19 deletions(-) create mode 100644 packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Option.php create mode 100644 packages/guides-restructured-text/src/RestructuredText/Directives/OptionType.php diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Option.php b/packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Option.php new file mode 100644 index 000000000..ab9b7e313 --- /dev/null +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Option.php @@ -0,0 +1,22 @@ + Cache of Option attributes indexed by option name */ + private array $optionAttributeCache; + /** * Get the directive name */ @@ -94,4 +98,87 @@ 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); + } + + 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, + default => $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 + { + $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/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/CsvTableDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/CsvTableDirective.php index cccb25a8d..8350d37fa 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/CsvTableDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/CsvTableDirective.php @@ -20,6 +20,7 @@ use phpDocumentor\Guides\Nodes\Table\TableColumn; use phpDocumentor\Guides\Nodes\Table\TableRow; use phpDocumentor\Guides\Nodes\TableNode; +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\RuleContainer; 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/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/ImageDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/ImageDirective.php index c1252a38d..ecdee1109 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/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/OptionType.php b/packages/guides-restructured-text/src/RestructuredText/Directives/OptionType.php new file mode 100644 index 000000000..1f0f57e5d --- /dev/null +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/OptionType.php @@ -0,0 +1,13 @@ +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($directive)); } } From 938b39609b770e50bbb8b64f1d8683d738081af7 Mon Sep 17 00:00:00 2001 From: Jaapio Date: Tue, 24 Jun 2025 20:44:04 +0200 Subject: [PATCH 2/5] Redirect processing in compiler pass --- .../config/guides-restructured-text.php | 4 ++ .../Compiler/Passes/DirectiveProcessPass.php | 64 +++++++++++++++++++ .../Directives/Attributes/Directive.php | 18 ++++++ .../Directives/BaseDirective.php | 56 +++++++++++++++- .../Directives/NoteDirective.php | 1 + .../Directives/WarningDirective.php | 1 + .../Directives/YoutubeDirective.php | 8 +++ .../RestructuredText/Nodes/DirectiveNode.php | 22 +++++++ .../Parser/Productions/DirectiveRule.php | 8 ++- 9 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php create mode 100644 packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Directive.php create mode 100644 packages/guides-restructured-text/src/RestructuredText/Nodes/DirectiveNode.php 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..838e2b1da 100644 --- a/packages/guides-restructured-text/resources/config/guides-restructured-text.php +++ b/packages/guides-restructured-text/resources/config/guides-restructured-text.php @@ -374,6 +374,10 @@ ->set(GlobSearcher::class) ->set(ToctreeBuilder::class) ->set(InlineMarkupRule::class) + + ->set(\phpDocumentor\Guides\RestructuredText\Compiler\Passes\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..7d051576a --- /dev/null +++ b/packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php @@ -0,0 +1,64 @@ + */ + 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 + { + return $this->getDirectiveHandler($node->getDirective())->createNode($node->getDirective()); + } + + 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 PHP_INT_MAX; + } +} 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..2366f17e0 --- /dev/null +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Directive.php @@ -0,0 +1,18 @@ + Cache of Option attributes indexed by option name */ private array $optionAttributeCache; + private string $name; + + 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. @@ -54,7 +73,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; } /** @@ -89,6 +136,11 @@ public function processNode( return new GenericNode($directive->getVariable(), $directive->getData()); } + public function createNode(Directive $directive): Node|null + { + return null; + } + /** * @param DirectiveOption[] $options * 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/WarningDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/WarningDirective.php index b3bbdb8c5..ee9a3a8a1 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/WarningDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/WarningDirective.php @@ -26,6 +26,7 @@ * This is a warning admonition. * ``` */ +#[Attributes\Directive(name: 'warning')] final class WarningDirective extends AbstractAdmonitionDirective { public function __construct(protected Rule $startingRule) diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/YoutubeDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/YoutubeDirective.php index 16b578a9c..b44348973 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/YoutubeDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/YoutubeDirective.php @@ -14,6 +14,8 @@ namespace phpDocumentor\Guides\RestructuredText\Directives; use phpDocumentor\Guides\Nodes\EmbeddedFrame; +use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\RestructuredText\Directives\Attributes; use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option; use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; use phpDocumentor\Guides\RestructuredText\Parser\Directive; @@ -37,6 +39,7 @@ * - string allow The allow attribute of the iframe, default is 'encrypted-media; picture-in-picture; web-share' * - bool allowfullscreen Whether the video should be allowed to go fullscreen, default is true */ +#[Attributes\Directive(name: 'youtube')] #[Option('width', type: OptionType::Integer, default: 560, description: 'Width of the video')] #[Option('title', type: OptionType::String, description: 'Title of the video')] #[Option('height', type: OptionType::Integer, default: 315, description: 'Height of the video')] @@ -53,6 +56,11 @@ public function process( BlockContext $blockContext, Directive $directive, ): EmbeddedFrame { + return $this->createNode($directive); + } + + public function createNode(Directive $directive): EmbeddedFrame + { $node = new EmbeddedFrame( 'https://www.youtube-nocookie.com/embed/' . $directive->getData(), ); 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..3a9a44f96 --- /dev/null +++ b/packages/guides-restructured-text/src/RestructuredText/Nodes/DirectiveNode.php @@ -0,0 +1,22 @@ + */ +final class DirectiveNode extends AbstractNode +{ + public function __construct(private Directive $directive) + { + + } + + public function getDirective(): Directive + { + return $this->directive; + } +} 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..ab7637189 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; @@ -84,10 +85,15 @@ public function apply(BlockContext $blockContext, CompoundNode|null $on = null): } $this->parseDirectiveContent($directive, $blockContext); + $this->interpretDirectiveOptions($documentIterator, $directive); $directiveHandler = $this->getDirectiveHandler($directive); + if ($directiveHandler->isUpgraded()) { + //What do we do with the content of the directive? + // Child nodes need to be parsed. and the content needs to be collected as a string. + return new DirectiveNode($directive); + } - $this->interpretDirectiveOptions($documentIterator, $directive); $buffer = $this->collectDirectiveContents($documentIterator); // Processing the Directive, the handler is responsible for adding the right Nodes to the document. From 1823455ccbec6288509584a5821bbc4c5f01aa6f Mon Sep 17 00:00:00 2001 From: Jaapio Date: Sun, 1 Mar 2026 20:49:23 +0100 Subject: [PATCH 3/5] Add support for directive content. --- .../config/guides-restructured-text.php | 4 ++- .../Compiler/Passes/DirectiveProcessPass.php | 25 +++++++++++++-- .../AbstractAdmonitionDirective.php | 15 +++++++-- .../Directives/AdmonitionDirective.php | 6 +--- .../Directives/AttentionDirective.php | 1 + .../Directives/Attributes/Directive.php | 11 ++++++- .../Directives/Attributes/Option.php | 10 +++++- .../Directives/BaseDirective.php | 32 +++++++++++-------- .../Directives/CautionDirective.php | 1 + .../Directives/CsvTableDirective.php | 1 - .../Directives/DangerDirective.php | 1 + .../Directives/ErrorDirective.php | 1 + .../Directives/HintDirective.php | 1 + .../Directives/ImageDirective.php | 2 +- .../Directives/ImportantDirective.php | 1 + .../Directives/OptionType.php | 9 ++++++ .../Directives/SeeAlsoDirective.php | 2 ++ .../Directives/TipDirective.php | 1 + .../Directives/YoutubeDirective.php | 18 +++-------- .../RestructuredText/Nodes/DirectiveNode.php | 20 +++++++++--- .../Productions/DirectiveContentRule.php | 6 ++-- .../Parser/Productions/DirectiveRule.php | 19 +++++++---- tests/Functional/FunctionalTest.php | 2 +- 23 files changed, 133 insertions(+), 56 deletions(-) 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 838e2b1da..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]) @@ -375,7 +377,7 @@ ->set(ToctreeBuilder::class) ->set(InlineMarkupRule::class) - ->set(\phpDocumentor\Guides\RestructuredText\Compiler\Passes\DirectiveProcessPass::class) + ->set(DirectiveProcessPass::class) ->arg('$directives', tagged_iterator('phpdoc.guides.directive')) ->tag('phpdoc.guides.compiler.nodeTransformers') ->set(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 index 7d051576a..40946d33f 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php +++ b/packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php @@ -2,6 +2,15 @@ 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\RestructuredText\Compiler\Passes; use phpDocumentor\Guides\Compiler\CompilerContext; @@ -13,7 +22,12 @@ use phpDocumentor\Guides\RestructuredText\Parser\Directive; use Psr\Log\LoggerInterface; -class DirectiveProcessPass implements NodeTransformer +use function strtolower; + +use const PHP_INT_MAX; + +/** @implements NodeTransformer */ +final class DirectiveProcessPass implements NodeTransformer { /** @var array */ private array $directives; @@ -44,7 +58,14 @@ public function enterNode(Node $node, CompilerContext $compilerContext): Node public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null { - return $this->getDirectiveHandler($node->getDirective())->createNode($node->getDirective()); + $newNode = $this->getDirectiveHandler($node->getDirective())->createNode($node); + if ($newNode === null) { + return null; + } + + $newNode->setClasses($node->getClasses()); + + return $newNode; } private function getDirectiveHandler(Directive $directive): DirectiveHandler diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/AbstractAdmonitionDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/AbstractAdmonitionDirective.php index 89e0f6dc5..cbf901266 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/AbstractAdmonitionDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/AbstractAdmonitionDirective.php @@ -17,6 +17,7 @@ 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; @@ -53,8 +54,18 @@ final protected function processSub( ); } - final public function getName(): string + public function createNode(DirectiveNode $directiveNode): Node|null { - return $this->name; + $children = $directiveNode->getChildren(); + if ($directiveNode->getDirective()->getDataNode() !== null) { + array_unshift($children, new ParagraphNode([$directiveNode->getDirective()->getDataNode()])); + } + + return new AdmonitionNode( + $directiveNode->getDirective()->getName(), + null, + $this->text, + $children, + ); } } diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/AdmonitionDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/AdmonitionDirective.php index 220daf89b..48f4c8cb0 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/AdmonitionDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/AdmonitionDirective.php @@ -34,13 +34,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 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 index 2366f17e0..07513c24e 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Directive.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Directive.php @@ -2,6 +2,15 @@ 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\RestructuredText\Directives\Attributes; use Attribute; @@ -9,10 +18,10 @@ #[Attribute(Attribute::TARGET_CLASS)] final class Directive { + /** @param string[] $aliases */ public function __construct( public readonly string $name, public readonly array $aliases = [], ) { - } } diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Option.php b/packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Option.php index ab9b7e313..5b3af1286 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Option.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Option.php @@ -2,6 +2,15 @@ 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\RestructuredText\Directives\Attributes; use Attribute; @@ -19,4 +28,3 @@ public function __construct( ) { } } - diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php index ceee22baf..2d6ecf45e 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php @@ -13,14 +13,21 @@ namespace phpDocumentor\Guides\RestructuredText\Directives; +use LogicException; use phpDocumentor\Guides\Nodes\GenericNode; use phpDocumentor\Guides\Nodes\Node; use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option; +use phpDocumentor\Guides\RestructuredText\Nodes\DirectiveNode; use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; use phpDocumentor\Guides\RestructuredText\Parser\Directive; use phpDocumentor\Guides\RestructuredText\Parser\DirectiveOption; +use ReflectionClass; use function array_map; +use function count; +use function filter_var; + +use const FILTER_VALIDATE_BOOL; /** * A directive is like a function you can call or apply to a block @@ -37,11 +44,12 @@ */ abstract class BaseDirective { - /** @var array Cache of Option attributes indexed by option name */ + /** @var array Cache of Option attributes indexed by option name */ private array $optionAttributeCache; private string $name; + /** @var string[] */ private array $aliases; /** @@ -53,14 +61,15 @@ public function getName(): string return $this->name; } - $reflection = new \ReflectionClass($this); + $reflection = new ReflectionClass($this); $attributes = $reflection->getAttributes(Attributes\Directive::class); if (count($attributes) === 0) { - throw new \LogicException('Directive class must have a Directive attribute'); + throw new LogicException('Directive class must have a Directive attribute'); } $this->name = $attributes[0]->newInstance()->name; + return $this->name; } @@ -77,7 +86,7 @@ public function getAliases(): array return $this->aliases; } - $reflection = new \ReflectionClass($this); + $reflection = new ReflectionClass($this); $attributes = $reflection->getAttributes(Attributes\Directive::class); $this->aliases = []; if (count($attributes) !== 0) { @@ -98,7 +107,7 @@ public function getAliases(): array */ final public function isUpgraded(): bool { - $reflection = new \ReflectionClass($this); + $reflection = new ReflectionClass($this); $attributes = $reflection->getAttributes(Attributes\Directive::class); return count($attributes) === 1; @@ -136,7 +145,7 @@ public function processNode( return new GenericNode($directive->getVariable(), $directive->getData()); } - public function createNode(Directive $directive): Node|null + public function createNode(DirectiveNode $directiveNode): Node|null { return null; } @@ -170,13 +179,14 @@ final protected function readOption(Directive $directive, string $optionName): m 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 + $this->optionAttributeCache, ); } @@ -198,11 +208,9 @@ private function getOptionValue(Directive $directive, Option|null $option): mixe OptionType::Boolean => $value === null || filter_var($value, FILTER_VALIDATE_BOOL), OptionType::String => (string) $value, OptionType::Array => (array) $value, - default => $value, }; } - /** * Finds the Option attribute for the given option name on the current class. * @@ -210,7 +218,7 @@ private function getOptionValue(Directive $directive, Option|null $option): mixe * * @return Option|null The Option attribute if found, null otherwise */ - private function findOptionAttribute(string $optionName): ?Option + private function findOptionAttribute(string $optionName): Option|null { $this->initialize(); @@ -223,7 +231,7 @@ private function initialize(): void return; } - $reflection = new \ReflectionClass($this); + $reflection = new ReflectionClass($this); $attributes = $reflection->getAttributes(Option::class); $this->optionAttributeCache = []; foreach ($attributes as $attribute) { @@ -232,5 +240,3 @@ private function initialize(): void } } } - - 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/CsvTableDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/CsvTableDirective.php index 8350d37fa..cccb25a8d 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/CsvTableDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/CsvTableDirective.php @@ -20,7 +20,6 @@ use phpDocumentor\Guides\Nodes\Table\TableColumn; use phpDocumentor\Guides\Nodes\Table\TableRow; use phpDocumentor\Guides\Nodes\TableNode; -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\RuleContainer; 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/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/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 ecdee1109..e6d2c17fd 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/ImageDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/ImageDirective.php @@ -78,7 +78,7 @@ public function processNode( if ($directive->hasOption('target')) { $node->setTarget( $this->resolveLinkTarget( - $this->readOption($directive, 'target') + $this->readOption($directive, 'target'), ), ); } 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/OptionType.php b/packages/guides-restructured-text/src/RestructuredText/Directives/OptionType.php index 1f0f57e5d..76d956b72 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/OptionType.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/OptionType.php @@ -2,6 +2,15 @@ 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\RestructuredText\Directives; enum OptionType diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/SeeAlsoDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/SeeAlsoDirective.php index 6dd643aba..688f9f78e 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/SeeAlsoDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/SeeAlsoDirective.php @@ -13,6 +13,7 @@ namespace phpDocumentor\Guides\RestructuredText\Directives; +use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Directive; use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; /** @@ -26,6 +27,7 @@ * This is a seealso admonition. * ``` */ +#[Directive(name: 'seealso')] final class SeeAlsoDirective extends AbstractAdmonitionDirective { public function __construct(protected Rule $startingRule) diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/TipDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/TipDirective.php index 17dd2552d..03a43852e 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/TipDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/TipDirective.php @@ -26,6 +26,7 @@ * This is a tip admonition. * ``` */ +#[Attributes\Directive(name: 'tip')] final class TipDirective extends AbstractAdmonitionDirective { public function __construct(protected Rule $startingRule) diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/YoutubeDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/YoutubeDirective.php index b44348973..ac5fa1d37 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/YoutubeDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/YoutubeDirective.php @@ -14,14 +14,11 @@ namespace phpDocumentor\Guides\RestructuredText\Directives; use phpDocumentor\Guides\Nodes\EmbeddedFrame; -use phpDocumentor\Guides\Nodes\Node; -use phpDocumentor\Guides\RestructuredText\Directives\Attributes; use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option; +use phpDocumentor\Guides\RestructuredText\Nodes\DirectiveNode; use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; use phpDocumentor\Guides\RestructuredText\Parser\Directive; -use function array_filter; - /** * This directive is used to embed a youtube video in the document. * @@ -47,24 +44,19 @@ #[Option('allowfullscreen', type: OptionType::Boolean, default: true, description: 'Whether the video should be allowed to go fullscreen')] final class YoutubeDirective extends BaseDirective { - public function getName(): string - { - return 'youtube'; - } - public function process( BlockContext $blockContext, Directive $directive, ): EmbeddedFrame { - return $this->createNode($directive); + return $this->createNode(new DirectiveNode($directive)); } - public function createNode(Directive $directive): EmbeddedFrame + 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($this->readAllOptions($directive)); + 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 index 3a9a44f96..ea66b7e36 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Nodes/DirectiveNode.php +++ b/packages/guides-restructured-text/src/RestructuredText/Nodes/DirectiveNode.php @@ -2,17 +2,27 @@ 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\RestructuredText\Nodes; -use phpDocumentor\Guides\Nodes\AbstractNode; +use phpDocumentor\Guides\Nodes\CompoundNode; +use phpDocumentor\Guides\Nodes\Node; use phpDocumentor\Guides\RestructuredText\Parser\Directive; -/** @extends AbstractNode */ -final class DirectiveNode extends AbstractNode +/** @extends CompoundNode */ +final class DirectiveNode extends CompoundNode { - public function __construct(private Directive $directive) + public function __construct(private readonly Directive $directive) { - + parent::__construct(); } public function getDirective(): Directive 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 ab7637189..49cf6ba9f 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Parser/Productions/DirectiveRule.php +++ b/packages/guides-restructured-text/src/RestructuredText/Parser/Productions/DirectiveRule.php @@ -49,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); @@ -88,14 +92,15 @@ public function apply(BlockContext $blockContext, CompoundNode|null $on = null): $this->interpretDirectiveOptions($documentIterator, $directive); $directiveHandler = $this->getDirectiveHandler($directive); - if ($directiveHandler->isUpgraded()) { - //What do we do with the content of the directive? - // Child nodes need to be parsed. and the content needs to be collected as a string. - return new DirectiveNode($directive); - } - $buffer = $this->collectDirectiveContents($documentIterator); + if ($this->startingRule !== null && $directiveHandler->isUpgraded()) { + return $this->startingRule->apply( + new BlockContext($blockContext->getDocumentParserContext(), $buffer->getLinesString(), true, $documentIterator->key()), + new DirectiveNode($directive), + ); + } + // Processing the Directive, the handler is responsible for adding the right Nodes to the document. try { $node = $directiveHandler->process( diff --git a/tests/Functional/FunctionalTest.php b/tests/Functional/FunctionalTest.php index bcac5797f..2b9186b4e 100644 --- a/tests/Functional/FunctionalTest.php +++ b/tests/Functional/FunctionalTest.php @@ -113,7 +113,7 @@ public function testFunctional( $compiler = $this->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'); From b8863d313c25e57f1f416eb16b7697bcdfdf42df Mon Sep 17 00:00:00 2001 From: Jaapio Date: Mon, 9 Mar 2026 21:55:01 +0100 Subject: [PATCH 4/5] Add reverse node transformer The reverse node transformer allows us to transform children of nodes before the node itself is transformed. This is needed for directives which may contain child nodes that need processing. --- .../Compiler/Passes/DirectiveProcessPass.php | 3 +- .../AbstractAdmonitionDirective.php | 26 ++++--------- .../AbstractVersionChangeDirective.php | 39 +++++++++++++++---- .../Directives/AdmonitionDirective.php | 21 +++++++--- .../Directives/BreadcrumbDirective.php | 11 ++---- .../Directives/DeprecatedDirective.php | 1 + .../Directives/VersionAddedDirective.php | 1 + .../Directives/VersionChangedDirective.php | 1 + .../RestructuredText/Nodes/DirectiveNode.php | 4 +- .../src/RestructuredText/Parser/Directive.php | 4 +- .../src/Compiler/DocumentNodeTraverser.php | 12 +++++- .../src/Compiler/ReverseNodeTransformer.php | 15 +++++++ 12 files changed, 93 insertions(+), 45 deletions(-) create mode 100644 packages/guides/src/Compiler/ReverseNodeTransformer.php diff --git a/packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php b/packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php index 40946d33f..080d32cd4 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php +++ b/packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php @@ -15,6 +15,7 @@ use phpDocumentor\Guides\Compiler\CompilerContext; use phpDocumentor\Guides\Compiler\NodeTransformer; +use phpDocumentor\Guides\Compiler\ReverseNodeTransformer; use phpDocumentor\Guides\Nodes\Node; use phpDocumentor\Guides\RestructuredText\Directives\BaseDirective as DirectiveHandler; use phpDocumentor\Guides\RestructuredText\Directives\GeneralDirective; @@ -27,7 +28,7 @@ use const PHP_INT_MAX; /** @implements NodeTransformer */ -final class DirectiveProcessPass implements NodeTransformer +final class DirectiveProcessPass implements ReverseNodeTransformer { /** @var array */ private array $directives; diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/AbstractAdmonitionDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/AbstractAdmonitionDirective.php index cbf901266..606b188c4 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/AbstractAdmonitionDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/AbstractAdmonitionDirective.php @@ -13,6 +13,7 @@ namespace phpDocumentor\Guides\RestructuredText\Directives; +use Doctrine\Deprecations\Deprecation; use phpDocumentor\Guides\Nodes\AdmonitionNode; use phpDocumentor\Guides\Nodes\CollectionNode; use phpDocumentor\Guides\Nodes\Node; @@ -40,32 +41,21 @@ final protected function processSub( CollectionNode $collectionNode, Directive $directive, ): Node|null { - $children = $collectionNode->getChildren(); - - if ($directive->getDataNode() !== null) { - array_unshift($children, new ParagraphNode([$directive->getDataNode()])); - } - - return new AdmonitionNode( - $this->name, - null, - $this->text, - $children, + return $this->createNode( + new DirectiveNode( + $directive, + $collectionNode->getChildren(), + ), ); } 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( $directiveNode->getDirective()->getName(), - null, + $directiveNode->getDirective()->getDataNode(), $this->text, - $children, + $directiveNode->getChildren(), ); } } 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 48f4c8cb0..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; @@ -46,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/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/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/VersionAddedDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/VersionAddedDirective.php index c9ae440f1..564f3fe71 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/VersionAddedDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/VersionAddedDirective.php @@ -15,6 +15,7 @@ use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; +#[Attributes\Directive(name: 'versionadded')] final class VersionAddedDirective extends AbstractVersionChangeDirective { public function __construct(protected Rule $startingRule) diff --git a/packages/guides-restructured-text/src/RestructuredText/Directives/VersionChangedDirective.php b/packages/guides-restructured-text/src/RestructuredText/Directives/VersionChangedDirective.php index bc041e48c..9703071b4 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/VersionChangedDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/VersionChangedDirective.php @@ -15,6 +15,7 @@ use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; +#[Attributes\Directive(name: 'versionchanged')] final class VersionChangedDirective extends AbstractVersionChangeDirective { public function __construct(protected Rule $startingRule) diff --git a/packages/guides-restructured-text/src/RestructuredText/Nodes/DirectiveNode.php b/packages/guides-restructured-text/src/RestructuredText/Nodes/DirectiveNode.php index ea66b7e36..b9b692aa2 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Nodes/DirectiveNode.php +++ b/packages/guides-restructured-text/src/RestructuredText/Nodes/DirectiveNode.php @@ -20,9 +20,9 @@ /** @extends CompoundNode */ final class DirectiveNode extends CompoundNode { - public function __construct(private readonly Directive $directive) + public function __construct(private readonly Directive $directive, array $children = []) { - parent::__construct(); + parent::__construct($children); } public function getDirective(): 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/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/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 @@ + Date: Sat, 28 Mar 2026 21:45:17 +0100 Subject: [PATCH 5/5] Improve new directive handling --- .../Compiler/Passes/DirectiveProcessPass.php | 2 +- .../Directives/AbstractAdmonitionDirective.php | 9 +++++++-- .../Parser/Productions/DirectiveRule.php | 14 +++++++++++++- .../VariableInlineNodeTransformer.php | 14 +++++++------- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php b/packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php index 080d32cd4..a2922e61b 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php +++ b/packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php @@ -81,6 +81,6 @@ public function supports(Node $node): bool public function getPriority(): int { - return PHP_INT_MAX; + 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 606b188c4..eeeb93c0f 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Directives/AbstractAdmonitionDirective.php +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/AbstractAdmonitionDirective.php @@ -51,11 +51,16 @@ final protected function processSub( 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( $directiveNode->getDirective()->getName(), - $directiveNode->getDirective()->getDataNode(), + null, $this->text, - $directiveNode->getChildren(), + $children, ); } } 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 49cf6ba9f..bf131276b 100644 --- a/packages/guides-restructured-text/src/RestructuredText/Parser/Productions/DirectiveRule.php +++ b/packages/guides-restructured-text/src/RestructuredText/Parser/Productions/DirectiveRule.php @@ -95,10 +95,22 @@ public function apply(BlockContext $blockContext, CompoundNode|null $on = null): $buffer = $this->collectDirectiveContents($documentIterator); if ($this->startingRule !== null && $directiveHandler->isUpgraded()) { - return $this->startingRule->apply( + $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. 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; }