Skip to content

First iteration on option attributes #1231

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace phpDocumentor\Guides\RestructuredText\Directives\Attributes;

use Attribute;
use phpDocumentor\Guides\RestructuredText\Directives\OptionType;

#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class Option
{
public function __construct(
public readonly string $name,
public readonly OptionType $type = OptionType::String,
public readonly mixed $default = null,
public readonly string $description = '',
public readonly string|null $example = null,
) {
}
}

Check failure on line 21 in packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Option.php

View workflow job for this annotation

GitHub Actions / Coding Standards

Expected 1 blank line at end of file; 2 found

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use phpDocumentor\Guides\Nodes\GenericNode;
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\DirectiveOption;
Expand All @@ -36,6 +37,9 @@
*/
abstract class BaseDirective
{
/** @var array<string, Option|null> Cache of Option attributes indexed by option name */
private array $optionAttributeCache;

/**
* Get the directive name
*/
Expand Down Expand Up @@ -94,4 +98,87 @@
{
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

Check failure on line 121 in packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php

View workflow job for this annotation

GitHub Actions / Coding Standards

Method \phpDocumentor\Guides\RestructuredText\Directives\BaseDirective::readAllOptions() does not have @return annotation for its traversable return value.

Check failure on line 121 in packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Method phpDocumentor\Guides\RestructuredText\Directives\BaseDirective::readAllOptions() return type has no value type specified in iterable type array.
{
$this->initialize();

return array_map(
fn (Option $option) => $this->getOptionValue($directive, $option),

Check failure on line 126 in packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Parameter #1 $callback of function array_map expects (callable(phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option|null): mixed)|null, Closure(phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option): mixed given.
$this->optionAttributeCache

Check failure on line 127 in packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php

View workflow job for this annotation

GitHub Actions / Coding Standards

Multi-line function calls must have a trailing comma after the last parameter.
);
}

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),

Check failure on line 146 in packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php

View workflow job for this annotation

GitHub Actions / Coding Standards

Constant FILTER_VALIDATE_BOOL should not be referenced via a fallback global name, but via a use statement.

Check failure on line 146 in packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php

View workflow job for this annotation

GitHub Actions / Coding Standards

Function filter_var() should not be referenced via a fallback global name, but via a use statement.
OptionType::String => (string) $value,
OptionType::Array => (array) $value,

Check failure on line 148 in packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Match arm comparison between phpDocumentor\Guides\RestructuredText\Directives\OptionType::Array and phpDocumentor\Guides\RestructuredText\Directives\OptionType::Array is always true.
default => $value,

Check failure on line 149 in packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Match arm is unreachable because previous comparison is always true.
};
}

Check failure on line 151 in packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php

View workflow job for this annotation

GitHub Actions / Coding Standards

Expected 1 blank line after function; 2 found


/**
* 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

Check failure on line 161 in packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php

View workflow job for this annotation

GitHub Actions / Coding Standards

Usage of short nullable type hint in "?Option" is disallowed.
{
$this->initialize();

return $this->optionAttributeCache[$optionName] ?? null;
}

private function initialize(): void
{
if (isset($this->optionAttributeCache)) {
return;
}

$reflection = new \ReflectionClass($this);

Check failure on line 174 in packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php

View workflow job for this annotation

GitHub Actions / Coding Standards

Class \ReflectionClass should not be referenced via a fully qualified name, but via a use statement.
$attributes = $reflection->getAttributes(Option::class);
$this->optionAttributeCache = [];
foreach ($attributes as $attribute) {
$option = $attribute->newInstance();
$this->optionAttributeCache[$option->name] = $option;
}
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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';
Expand Down Expand Up @@ -80,16 +86,16 @@
}

if ($directive->hasOption('type')) {
$type = $this->inlineParser->parse($directive->getOptionString('type'), $blockContext);
$type = $this->inlineParser->parse($this->readOption($directive, 'type'), $blockContext);

Check failure on line 89 in packages/guides-restructured-text/src/RestructuredText/Directives/ConfvalDirective.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Parameter #1 $content of method phpDocumentor\Guides\RestructuredText\Parser\InlineParser::parse() expects string, mixed given.
}

$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);

Check failure on line 95 in packages/guides-restructured-text/src/RestructuredText/Directives/ConfvalDirective.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Parameter #1 $content of method phpDocumentor\Guides\RestructuredText\Parser\InlineParser::parse() expects string, mixed given.
}

$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)) {
Expand All @@ -99,6 +105,6 @@
$additionalOptions[$option->getName()] = $this->inlineParser->parse($option->toString(), $blockContext);
}

return new ConfvalNode($id, $directive->getData(), $type, $required, $default, $additionalOptions, $collectionNode->getChildren(), $noindex);

Check failure on line 108 in packages/guides-restructured-text/src/RestructuredText/Directives/ConfvalDirective.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Parameter #8 $noindex of class phpDocumentor\Guides\RestructuredText\Nodes\ConfvalNode constructor expects bool, mixed given.

Check failure on line 108 in packages/guides-restructured-text/src/RestructuredText/Directives/ConfvalDirective.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Parameter #4 $required of class phpDocumentor\Guides\RestructuredText\Nodes\ConfvalNode constructor expects bool, mixed given.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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(
Expand All @@ -51,6 +54,6 @@
return (new ContentMenuNode([new SectionMenuEntryNode($absoluteUrl)]))
->withOptions($this->optionsToArray($options))
->withCaption($directive->getDataNode())
->withLocal($directive->hasOption('local'));
->withLocal($this->readOption($directive, 'local'));

Check failure on line 57 in packages/guides-restructured-text/src/RestructuredText/Directives/ContentsDirective.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Parameter #1 $local of method phpDocumentor\Guides\Nodes\Menu\ContentMenuNode::withLocal() expects bool, mixed given.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,7 +41,7 @@

return new DocumentBlockNode(
$collectionNode->getChildren(),
$identifier,
$this->readOption($directive, 'identifier'),

Check failure on line 44 in packages/guides-restructured-text/src/RestructuredText/Directives/DocumentBlockDirective.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Parameter #2 $identifier of class phpDocumentor\Guides\Nodes\DocumentBlockNode constructor expects string, mixed given.
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 */
Expand Down Expand Up @@ -67,8 +76,11 @@
),
);
if ($directive->hasOption('target')) {
$targetReference = (string) $directive->getOption('target')->getValue();
$node->setTarget($this->resolveLinkTarget($targetReference));
$node->setTarget(
$this->resolveLinkTarget(
$this->readOption($directive, 'target')

Check failure on line 81 in packages/guides-restructured-text/src/RestructuredText/Directives/ImageDirective.php

View workflow job for this annotation

GitHub Actions / Coding Standards

Multi-line function calls must have a trailing comma after the last parameter.
),
);
}

return $node;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace phpDocumentor\Guides\RestructuredText\Directives;

enum OptionType
{
case String;
case Integer;
case Boolean;
case Array;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
namespace phpDocumentor\Guides\RestructuredText\Directives;

use phpDocumentor\Guides\Nodes\EmbeddedFrame;
use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option;
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
use phpDocumentor\Guides\RestructuredText\Parser\Directive;

use function array_filter;

Check failure on line 21 in packages/guides-restructured-text/src/RestructuredText/Directives/YoutubeDirective.php

View workflow job for this annotation

GitHub Actions / Coding Standards

Type array_filter is not used in this file.

/**
* This directive is used to embed a youtube video in the document.
Expand All @@ -36,6 +37,11 @@
* - 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
*/
#[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')]
#[Option('allow', type: OptionType::String, default: 'encrypted-media; picture-in-picture; web-share', description: 'Allow attribute of the iframe')]
#[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
Expand All @@ -51,16 +57,6 @@
'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));
}
}