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 6b81f0566..74233c92a 100644 --- a/packages/guides-restructured-text/resources/config/guides-restructured-text.php +++ b/packages/guides-restructured-text/resources/config/guides-restructured-text.php @@ -372,6 +372,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. @@ -50,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; } /** @@ -85,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 * @@ -94,4 +150,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 f747d1a96..3e1256bae 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/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..1f0f57e5d --- /dev/null +++ b/packages/guides-restructured-text/src/RestructuredText/Directives/OptionType.php @@ -0,0 +1,13 @@ +createNode($directive); + } + + public function createNode(Directive $directive): EmbeddedFrame + { $node = new EmbeddedFrame( 'https://www.youtube-nocookie.com/embed/' . $directive->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)); } } 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.