Skip to content

Commit 9804699

Browse files
committed
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.
1 parent 6a309ad commit 9804699

File tree

11 files changed

+173
-19
lines changed

11 files changed

+173
-19
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpDocumentor\Guides\RestructuredText\Directives\Attributes;
6+
7+
use Attribute;
8+
use phpDocumentor\Guides\RestructuredText\Directives\OptionType;
9+
10+
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
11+
final class Option
12+
{
13+
public function __construct(
14+
public readonly string $name,
15+
public readonly OptionType $type = OptionType::String,
16+
public readonly mixed $default = null,
17+
public readonly string $description = '',
18+
public readonly string|null $example = null,
19+
) {
20+
}
21+
}
22+

packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use phpDocumentor\Guides\Nodes\GenericNode;
1717
use phpDocumentor\Guides\Nodes\Node;
18+
use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option;
1819
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
1920
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
2021
use phpDocumentor\Guides\RestructuredText\Parser\DirectiveOption;
@@ -36,6 +37,9 @@
3637
*/
3738
abstract class BaseDirective
3839
{
40+
/** @var array<string, Option|null> Cache of Option attributes indexed by option name */
41+
private array $optionAttributeCache;
42+
3943
/**
4044
* Get the directive name
4145
*/
@@ -94,4 +98,87 @@ protected function optionsToArray(array $options): array
9498
{
9599
return array_map(static fn (DirectiveOption $option): bool|float|int|string|null => $option->getValue(), $options);
96100
}
101+
102+
/**
103+
* Gets an option value from a directive based on attribute configuration.
104+
*
105+
* Looks up the option in the directive and returns its value converted to the
106+
* appropriate type based on the Option attribute defined on this directive class.
107+
* If the option is not present in the directive, returns the default value from the attribute.
108+
*
109+
* @param Directive $directive The directive containing the options
110+
* @param string $optionName The name of the option to retrieve
111+
*
112+
* @return mixed The option value converted to the appropriate type, or the default value
113+
*/
114+
final protected function readOption(Directive $directive, string $optionName): mixed
115+
{
116+
$optionAttribute = $this->findOptionAttribute($optionName);
117+
118+
return $this->getOptionValue($directive, $optionAttribute);
119+
}
120+
121+
final protected function readAllOptions(Directive $directive): array
122+
{
123+
$this->initialize();
124+
125+
return array_map(
126+
fn (Option $option) => $this->getOptionValue($directive, $option),
127+
$this->optionAttributeCache
128+
);
129+
}
130+
131+
private function getOptionValue(Directive $directive, Option|null $option): mixed
132+
{
133+
if ($option === null) {
134+
return null;
135+
}
136+
137+
if (!$directive->hasOption($option->name)) {
138+
return $option->default;
139+
}
140+
141+
$directiveOption = $directive->getOption($option->name);
142+
$value = $directiveOption->getValue();
143+
144+
return match ($option->type) {
145+
OptionType::Integer => (int) $value,
146+
OptionType::Boolean => $value === null || filter_var($value, FILTER_VALIDATE_BOOL),
147+
OptionType::String => (string) $value,
148+
OptionType::Array => (array) $value,
149+
default => $value,
150+
};
151+
}
152+
153+
154+
/**
155+
* Finds the Option attribute for the given option name on the current class.
156+
*
157+
* @param string $optionName The option name to look for
158+
*
159+
* @return Option|null The Option attribute if found, null otherwise
160+
*/
161+
private function findOptionAttribute(string $optionName): ?Option
162+
{
163+
$this->initialize();
164+
165+
return $this->optionAttributeCache[$optionName] ?? null;
166+
}
167+
168+
private function initialize(): void
169+
{
170+
if (isset($this->optionAttributeCache)) {
171+
return;
172+
}
173+
174+
$reflection = new \ReflectionClass($this);
175+
$attributes = $reflection->getAttributes(Option::class);
176+
$this->optionAttributeCache = [];
177+
foreach ($attributes as $attribute) {
178+
$option = $attribute->newInstance();
179+
$this->optionAttributeCache[$option->name] = $option;
180+
}
181+
}
97182
}
183+
184+

packages/guides-restructured-text/src/RestructuredText/Directives/ConfvalDirective.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use phpDocumentor\Guides\Nodes\CollectionNode;
1717
use phpDocumentor\Guides\Nodes\Node;
1818
use phpDocumentor\Guides\ReferenceResolvers\AnchorNormalizer;
19+
use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option;
1920
use phpDocumentor\Guides\RestructuredText\Nodes\ConfvalNode;
2021
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
2122
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
@@ -32,6 +33,11 @@
3233
*
3334
* https://sphinx-toolbox.readthedocs.io/en/stable/extensions/confval.html
3435
*/
36+
#[Option(name: 'name', description: 'Id of the configuration value, used for linking to it.')]
37+
#[Option(name: 'type', description: 'Type of the configuration value, e.g. "string", "int", etc.')]
38+
#[Option(name: 'required', type: OptionType::Boolean, default: false, description: 'Whether the configuration value is required or not.')]
39+
#[Option(name: 'default', description: 'Default value of the configuration value, if any.')]
40+
#[Option(name: 'noindex', type: OptionType::Boolean, default: false, description: 'Whether the configuration value should not be indexed.')]
3541
final class ConfvalDirective extends SubDirective
3642
{
3743
public const NAME = 'confval';
@@ -80,16 +86,16 @@ protected function processSub(
8086
}
8187

8288
if ($directive->hasOption('type')) {
83-
$type = $this->inlineParser->parse($directive->getOptionString('type'), $blockContext);
89+
$type = $this->inlineParser->parse($this->readOption($directive, 'type'), $blockContext);
8490
}
8591

86-
$required = $directive->getOptionBool('required');
92+
$required = $this->readOption($directive, 'required');
8793

8894
if ($directive->hasOption('default')) {
89-
$default = $this->inlineParser->parse($directive->getOptionString('default'), $blockContext);
95+
$default = $this->inlineParser->parse($this->readOption($directive, 'default'), $blockContext);
9096
}
9197

92-
$noindex = $directive->getOptionBool('noindex');
98+
$noindex = $this->readOption($directive, 'noindex');
9399

94100
foreach ($directive->getOptions() as $option) {
95101
if (in_array($option->getName(), ['type', 'required', 'default', 'noindex', 'name'], true)) {

packages/guides-restructured-text/src/RestructuredText/Directives/ContentsDirective.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use phpDocumentor\Guides\Nodes\Menu\SectionMenuEntryNode;
1818
use phpDocumentor\Guides\Nodes\Node;
1919
use phpDocumentor\Guides\ReferenceResolvers\DocumentNameResolverInterface;
20+
use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option;
2021
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
2122
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
2223

@@ -25,6 +26,8 @@
2526
*
2627
* Displays a table of content of the current page
2728
*/
29+
#[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)]
30+
#[Option(name: 'depth', description: 'The maximum depth of the table of contents.')]
2831
final class ContentsDirective extends BaseDirective
2932
{
3033
public function __construct(
@@ -51,6 +54,6 @@ public function process(
5154
return (new ContentMenuNode([new SectionMenuEntryNode($absoluteUrl)]))
5255
->withOptions($this->optionsToArray($options))
5356
->withCaption($directive->getDataNode())
54-
->withLocal($directive->hasOption('local'));
57+
->withLocal($this->readOption($directive, 'local'));
5558
}
5659
}

packages/guides-restructured-text/src/RestructuredText/Directives/CsvTableDirective.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use phpDocumentor\Guides\Nodes\Table\TableColumn;
2121
use phpDocumentor\Guides\Nodes\Table\TableRow;
2222
use phpDocumentor\Guides\Nodes\TableNode;
23+
use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option;
2324
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
2425
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
2526
use phpDocumentor\Guides\RestructuredText\Parser\Productions\RuleContainer;

packages/guides-restructured-text/src/RestructuredText/Directives/DocumentBlockDirective.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
use phpDocumentor\Guides\Nodes\CollectionNode;
1717
use phpDocumentor\Guides\Nodes\DocumentBlockNode;
1818
use phpDocumentor\Guides\Nodes\Node;
19+
use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option;
1920
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
2021
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
2122

23+
#[Option(name: 'identifier', description: 'The identifier of the document block')]
2224
final class DocumentBlockDirective extends SubDirective
2325
{
2426
public function getName(): string
@@ -39,7 +41,7 @@ protected function processSub(
3941

4042
return new DocumentBlockNode(
4143
$collectionNode->getChildren(),
42-
$identifier,
44+
$this->readOption($directive, 'identifier'),
4345
);
4446
}
4547
}

packages/guides-restructured-text/src/RestructuredText/Directives/FigureDirective.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use phpDocumentor\Guides\Nodes\ImageNode;
1919
use phpDocumentor\Guides\Nodes\Node;
2020
use phpDocumentor\Guides\ReferenceResolvers\DocumentNameResolverInterface;
21+
use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option;
2122
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
2223
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
2324
use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule;
@@ -33,6 +34,14 @@
3334
*
3435
* Here is an awesome caption
3536
*/
37+
#[Option(name: 'width', description: 'Width of the image in pixels')]
38+
#[Option(name: 'height', description: 'Height of the image in pixels')]
39+
#[Option(name: 'alt', description: 'Alternative text for the image')]
40+
#[Option(name: 'scale', description: 'Scale of the image, e.g. 0.5 for half size')]
41+
#[Option(name: 'target', description: 'Target for the image, e.g. a link to the image')]
42+
#[Option(name: 'class', description: 'CSS class to apply to the image')]
43+
#[Option(name: 'name', description: 'Name of the image, used for references')]
44+
#[Option(name: 'align', description: 'Alignment of the image, e.g. left, right, center')]
3645
final class FigureDirective extends SubDirective
3746
{
3847
public function __construct(

packages/guides-restructured-text/src/RestructuredText/Directives/ImageDirective.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use phpDocumentor\Guides\Nodes\Inline\ReferenceNode;
2121
use phpDocumentor\Guides\Nodes\Node;
2222
use phpDocumentor\Guides\ReferenceResolvers\DocumentNameResolverInterface;
23+
use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option;
2324
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
2425
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
2526

@@ -37,6 +38,14 @@
3738
* :width: 100
3839
* :title: An image
3940
*/
41+
#[Option(name: 'width', description: 'Width of the image in pixels')]
42+
#[Option(name: 'height', description: 'Height of the image in pixels')]
43+
#[Option(name: 'alt', description: 'Alternative text for the image')]
44+
#[Option(name: 'scale', description: 'Scale of the image, e.g. 0.5 for half size')]
45+
#[Option(name: 'target', description: 'Target for the image, e.g. a link to the image')]
46+
#[Option(name: 'class', description: 'CSS class to apply to the image')]
47+
#[Option(name: 'name', description: 'Name of the image, used for references')]
48+
#[Option(name: 'align', description: 'Alignment of the image, e.g. left, right, center')]
4049
final class ImageDirective extends BaseDirective
4150
{
4251
/** @see https://regex101.com/r/9dUrzu/3 */
@@ -67,8 +76,11 @@ public function processNode(
6776
),
6877
);
6978
if ($directive->hasOption('target')) {
70-
$targetReference = (string) $directive->getOption('target')->getValue();
71-
$node->setTarget($this->resolveLinkTarget($targetReference));
79+
$node->setTarget(
80+
$this->resolveLinkTarget(
81+
$this->readOption($directive, 'target')
82+
),
83+
);
7284
}
7385

7486
return $node;

packages/guides-restructured-text/src/RestructuredText/Directives/IncludeDirective.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use phpDocumentor\Guides\Nodes\CollectionNode;
1818
use phpDocumentor\Guides\Nodes\LiteralBlockNode;
1919
use phpDocumentor\Guides\Nodes\Node;
20+
use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option;
2021
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
2122
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
2223
use phpDocumentor\Guides\RestructuredText\Parser\Productions\DocumentRule;
@@ -27,6 +28,8 @@
2728
use function sprintf;
2829
use function str_replace;
2930

31+
#[Option(name: 'literal', description: 'If set, the contents will be rendered as a literal block.')]
32+
#[Option(name: 'code', description: 'If set, the contents will be rendered as a code block with the specified language.')]
3033
final class IncludeDirective extends BaseDirective
3134
{
3235
public function __construct(private readonly DocumentRule $startingRule)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpDocumentor\Guides\RestructuredText\Directives;
6+
7+
enum OptionType
8+
{
9+
case String;
10+
case Integer;
11+
case Boolean;
12+
case Array;
13+
}

packages/guides-restructured-text/src/RestructuredText/Directives/YoutubeDirective.php

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace phpDocumentor\Guides\RestructuredText\Directives;
1515

1616
use phpDocumentor\Guides\Nodes\EmbeddedFrame;
17+
use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option;
1718
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
1819
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
1920

@@ -36,6 +37,11 @@
3637
* - string allow The allow attribute of the iframe, default is 'encrypted-media; picture-in-picture; web-share'
3738
* - bool allowfullscreen Whether the video should be allowed to go fullscreen, default is true
3839
*/
40+
#[Option('width', type: OptionType::Integer, default: 560, description: 'Width of the video')]
41+
#[Option('title', type: OptionType::String, description: 'Title of the video')]
42+
#[Option('height', type: OptionType::Integer, default: 315, description: 'Height of the video')]
43+
#[Option('allow', type: OptionType::String, default: 'encrypted-media; picture-in-picture; web-share', description: 'Allow attribute of the iframe')]
44+
#[Option('allowfullscreen', type: OptionType::Boolean, default: true, description: 'Whether the video should be allowed to go fullscreen')]
3945
final class YoutubeDirective extends BaseDirective
4046
{
4147
public function getName(): string
@@ -51,16 +57,6 @@ public function process(
5157
'https://www.youtube-nocookie.com/embed/' . $directive->getData(),
5258
);
5359

54-
return $node->withOptions(
55-
array_filter(
56-
[
57-
'width' => $directive->getOption('width')->getValue() ?? 560,
58-
'title' => $directive->getOption('title')->getValue(),
59-
'height' => $directive->getOption('height')->getValue() ?? 315,
60-
'allow' => $directive->getOption('allow')->getValue() ?? 'encrypted-media; picture-in-picture; web-share',
61-
'allowfullscreen' => (bool) ($directive->getOption('allowfullscreen')->getValue() ?? true),
62-
],
63-
),
64-
);
60+
return $node->withOptions($this->readAllOptions($directive));
6561
}
6662
}

0 commit comments

Comments
 (0)