8000 [Console] Add ReStructuredText descriptor · symfony/symfony@f3ab66e · GitHub
[go: up one dir, main page]

Skip to content

Commit f3ab66e

Browse files
danepowellfabpot
authored andcommitted
[Console] Add ReStructuredText descriptor
1 parent e7e59fb commit f3ab66e

32 files changed

+1056
-2
lines changed

src/Symfony/Component/Console/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Remove `exit` call in `Application` signal handlers. Commands will no longer be automatically interrupted after receiving signal other than `SIGUSR1` or `SIGUSR2`
88
* Add `ProgressBar::setPlaceholderFormatter` to set a placeholder attached to a instance, instead of being global.
9+
* Add `ReStructuredTextDescriptor`
910

1011
6.2
1112
---
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Descriptor;
13+
14+
use Symfony\Component\Console\Application;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Helper\Helper;
17+
use Symfony\Component\Console\Input\InputArgument;
18+
use Symfony\Component\Console\Input\InputDefinition;
19+
use Symfony\Component\Console\Input\InputOption;
20+
use Symfony\Component\Console\Output\OutputInterface;
21+
use Symfony\Component\String\UnicodeString;
22+
23+
class ReStructuredTextDescriptor extends Descriptor
24+
{
25+
// <h1>
26+
private string $partChar = '=';
27+
// <h2>
28+
private string $chapterChar = '-';
29+
// <h3>
30+
private string $sectionChar = '~';
31+
// <h4>
32+
private string $subsectionChar = '.';
33+
// <h5>
34+
private string $subsubsectionChar = '^';
35+
// <h6>
36+
private string $paragraphsChar = '"';
37+
38+
private array $visibleNamespaces = [];
39+
40+
public function describe(OutputInterface $output, object $object, array $options = []): void
41+
{
42+
$decorated = $output->isDecorated();
43+
$output->setDecorated(false);
44+
45+
parent::describe($output, $object, $options);
46+
47+
$output->setDecorated($decorated);
48+
}
49+
50+
/**
51+
* Override parent method to set $decorated = true.
52+
*/
53+
protected function write(string $content, bool $decorated = true): void
54+
{
55+
parent::write($content, $decorated);
56+
}
57+
58+
protected function describeInputArgument(InputArgument $argument, array $options = []): void
59+
{
60+
$this->write(
61+
$argument->getName() ?: '<none>'."\n".str_repeat($this->paragraphsChar, Helper::width($argument->getName()))."\n\n"
62+
.($argument->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $argument->getDescription())."\n\n" : '')
63+
.'- **Is required**: '.($argument->isRequired() ? 'yes' : 'no')."\n"
64+
.'- **Is array**: '.($argument->isArray() ? 'yes' : 'no')."\n"
65+
.'- **Default**: ``'.str_replace("\n", '', var_export($argument->getDefault(), true)).'``'
66+
);
67+
}
68+
69+
protected function describeInputOption(InputOption $option, array $options = []): void
70+
{
71+
$name = '\-\-'.$option->getName();
72+
if ($option->isNegatable()) {
73+
$name .= '|\-\-no-'.$option->getName();
74+
}
75+
if ($option->getShortcut()) {
76+
$name .= '|-'.str_replace('|', '|-', $option->getShortcut());
77+
}
78+
79+
$optionDescription = $option->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n\n", $option->getDescription())."\n\n" : '';
80+
$optionDescription = (new UnicodeString($optionDescription))->ascii();
81+
$this->write(
82+
$name."\n".str_repeat($this->paragraphsChar, Helper::width($name))."\n\n"
83+
.$optionDescription
84+
.'- **Accept value**: '.($option->acceptValue() ? 'yes' : 'no')."\n"
85+
.'- **Is value required**: '.($option->isValueRequired() ? 'yes' : 'no')."\n"
86+
.'- **Is multiple**: '.($option->isArray() ? 'yes' : 'no')."\n"
87+
.'- **Is negatable**: '.($option->isNegatable() ? 'yes' : 'no')."\n"
88+
.'- **Default**: ``'.str_replace("\n", '', var_export($option->getDefault(), true)).'``'."\n"
89+
);
90+
}
91+
92+
protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
93+
{
94+
if ($showArguments = ((bool) $definition->getArguments())) {
95+
$this->write("Arguments\n".str_repeat($this->subsubsectionChar, 9))."\n\n";
96+
foreach ($definition->getArguments() as $argument) {
97+
$this->write("\n\n");
98+
$this->describeInputArgument($argument);
99+
}
100+
}
101+
102+
if ($nonDefaultOptions = $this->getNonDefaultOptions($definition)) {
103+
if ($showArguments) {
104+
$this->write("\n\n");
105+
}
106+
107+
$this->write("Options\n".str_repeat($this->subsubsectionChar, 7)."\n\n");
108+
foreach ($nonDefaultOptions as $option) {
109+
$this->describeInputOption($option);
110+
$this->write("\n");
111+
}
112+
}
113+
}
114+
115+
protected function describeCommand(Command $command, array $options = []): void
116+
{
117+
if ($options['short'] ?? false) {
118+
$this->write(
119+
'``'.$command->getName()."``\n"
120+
.str_repeat($this->subsectionChar, Helper::width($command->getName()))."\n\n"
121+
.($command->getDescription() ? $command->getDescription()."\n\n" : '')
122+
."Usage\n".str_repeat($this->paragraphsChar, 5)."\n\n"
123+
.array_reduce($command->getAliases(), static fn ($carry, $usage) => $carry.'- ``'.$usage.'``'."\n")
124+
);
125+
126+
return;
127+
}
128+
129+
$command->mergeApplicationDefinition(false);
130+
131+
foreach ($command->getAliases() as $alias) {
132+
$this->write('.. _'.$alias.":\n\n");
133+
}
134+
$this->write(
135+
$command->getName()."\n"
136+
.str_repeat($this->subsectionChar, Helper::width($command->getName()))."\n\n"
137+
.($command->getDescription() ? $command->getDescription()."\n\n" : '')
138+
."Usage\n".str_repeat($this->subsubsectionChar, 5)."\n\n"
139+
.array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), static fn ($carry, $usage) => $carry.'- ``'.$usage.'``'."\n")
140+
);
141+
142+
if ($help = $command->getProcessedHelp()) {
143+
$this->write("\n");
144+
$this->write($help);
145+
}
146+
147+
$definition = $command->getDefinition();
148+
if ($definition->getOptions() || $definition->getArguments()) {
149+
$this->write("\n\n");
150+
$this->describeInputDefinition($definition);
151+
}
152+
}
153+
154+
protected function describeApplication(Application $application, array $options = []): void
155+
{
156+
$description = new ApplicationDescription($application, $options['namespace'] ?? null);
157+
$title = $this->getApplicationTitle($application);
158+
159+
$this->write($title."\n".str_repeat($this->partChar, Helper::width($title)));
160+
$this->createTableOfContents($description, $application);
161+
$this->describeCommands($application, $options);
162+
}
163+
164+
private function getApplicationTitle(Application $application): string
165+
{
166+
if ('UNKNOWN' === $application->getName()) {
167+
return 'Console Tool';
168+
}
169+
if ('UNKNOWN' !== $application->getVersion()) {
170+
return sprintf('%s %s', $application->getName(), $application->getVersion());
171+
}
172+
173+
return $application->getName();
174+
}
175+
176+
private function describeCommands($application, array $options): void
177+
{
178+
$title = 'Commands';
179+
$this->write("\n\n$title\n".str_repeat($this->chapterChar, Helper::width($title))."\n\n");
180+
foreach ($this->visibleNamespaces as $namespace) {
181+
if ('_global' === $namespace) {
182+
$commands = $application->all('');
183+
$this->write('Global'."\n".str_repeat($this->sectionChar, Helper::width('Global'))."\n\n");
184+
} else {
185+
$commands = $application->all($namespace);
186+
$this->write($namespace."\n".str_repeat($this->sectionChar, Helper::width($namespace))."\n\n");
187+
}
188+
189+
foreach ($this->removeAliasesAndHiddenCommands($commands) as $command) {
190+
$this->describeCommand($command, $options);
191+
$this->write("\n\n");
192+
}
193+
}
194+
}
195+
196+
private function createTableOfContents(ApplicationDescription $description, Application $application): void
197+
{
198+
$this->setVisibleNamespaces($description);
199+
$chapterTitle = 'Table of Contents';
200+
$this->write("\n\n$chapterTitle\n".str_repeat($this->chapterChar, Helper::width($chapterTitle))."\n\n");
201+
foreach ($this->visibleNamespaces as $namespace) {
202+
if ('_global' === $namespace) {
203+
$commands = $application->all('');
204+
} else {
205+
$commands = $application->all($namespace);
206+
$this->write("\n\n");
207+
$this->write($namespace."\n".str_repeat($this->sectionChar, Helper::width($namespace))."\n\n");
208+
}
209+
$commands = $this->removeAliasesAndHiddenCommands($commands);
210+
211+
$this->write("\n\n");
212+
$this->write(implode("\n", array_map(static fn ($commandName) => sprintf('- `%s`_', $commandName), array_keys($commands))));
213+
}
214+
}
215+
216+
private function getNonDefaultOptions(InputDefinition $definition): array
217+
{
218+
$globalOptions = [
219+
'help',
220+
'quiet',
221+
'verbose',
222+
'version',
223+
'ansi',
224+
'no-interaction',
225+
];
226+
$nonDefaultOptions = [];
227+
foreach ($definition->getOptions() as $option) {
228+
// Skip global options.
229+
if (!\in_array($option->getName(), $globalOptions)) {
230+
$nonDefaultOptions[] = $option;
231+
}
232+
}
233+
234+
return $nonDefaultOptions;
235+
}
236+
237+
private function setVisibleNamespaces(ApplicationDescription $description): void
238+
{
239+
$commands = $description->getCommands();
240+
foreach ($description->getNamespaces() as $namespace) {
241+
try {
242+
$namespaceCommands = $namespace['commands'];
243+
foreach ($namespaceCommands as $key => $commandName) {
244+
if (!\array_key_exists($commandName, $commands)) {
245+
// If the array key does not exist, then this is an alias.
246+
unset($namespaceCommands[$key]);
247+
} elseif ($commands[$commandName]->isHidden()) {
248+
unset($namespaceCommands[$key]);
249+
}
250+
}
251+
if (!$namespaceCommands) {
252+
// If the namespace contained only aliases or hidden commands, skip the namespace.
253+
continue;
254+
}
255+
} catch (\Exception) {
256+
}
257+
$this->visibleNamespaces[] = $namespace['id'];
258+
}
259+
}
260+
261+
private function removeAliasesAndHiddenCommands(array $commands): array
262+
{
263+
foreach ($commands as $key => $command) {
264+
if ($command->isHidden() || \in_array($key, $command->getAliases(), true)) {
265+
unset($commands[$key]);
266+
}
267+
}
268+
unset($commands['completion']);
269+
270+
return $commands;
271+
}
272+
}

src/Symfony/Component/Console/Helper/DescriptorHelper.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\Console\Descriptor\DescriptorInterface;
1515
use Symfony\Component\Console\Descriptor\JsonDescriptor;
1616
use Symfony\Component\Console\Descriptor\MarkdownDescriptor;
17+
use Symfony\Component\Console\Descriptor\ReStructuredTextDescriptor;
1718
use Symfony\Component\Console\Descriptor\TextDescriptor;
1819
use Symfony\Component\Console\Descriptor\XmlDescriptor;
1920
use Symfony\Component\Console\Exception\InvalidArgumentException;
@@ -38,6 +39,7 @@ public function __construct()
3839
->register('xml', new XmlDescriptor())
3940
->register('json', new JsonDescriptor())
4041
->register('md', new MarkdownDescriptor())
42+
->register('rst', new ReStructuredTextDescriptor())
4143
;
4244
}
4345

src/Symfony/Component/Console/Tests/Command/HelpCommandTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public function provideCompletionSuggestions()
8787
{
8888
yield 'option --format' => [
8989
['--format', ''],
90-
['txt', 'xml', 'json', 'md'],
90+
['txt', 'xml', 'json', 'md', 'rst'],
9191
];
9292

9393
yield 'nothing' => [

src/Symfony/Component/Console/Tests/Command/ListCommandTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ public function provideCompletionSuggestions()
131131
{
132132
yield 'option --format' => [
133133
['--format', ''],
134-
['txt', 'xml', 'json', 'md'],
134+
['txt', 'xml', 'json', 'md', 'rst'],
135135
];
136136

137137
yield 'namespace' => [
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Tests\Descriptor;
13+
14+
use Symfony\Component\Console\Descriptor\ReStructuredTextDescriptor;
15+
use Symfony\Component\Console\Tests\Fixtures\DescriptorApplicationMbString;
16+
use Symfony\Component\Console\Tests\Fixtures\DescriptorCommandMbString;
17+
18+
class ReStructuredTextDescriptorTest extends AbstractDescriptorTest
19+
{
20+
public function getDescribeCommandTestData()
21+
{
22+
return $this->getDescriptionTestData(array_merge(
23+
ObjectsProvider::getCommands(),
24+
['command_mbstring' => new DescriptorCommandMbString()]
25+
));
26+
}
27+
28+
public function getDescribeApplicationTestData()
29+
{
30+
return $this->getDescriptionTestData(array_merge(
31+
ObjectsProvider::getApplications(),
32+
['application_mbstring' => new DescriptorApplicationMbString()]
33+
));
34+
}
35+
36+
protected function getDescriptor()
37+
{
38+
return new ReStructuredTextDescriptor();
39+
}
40+
41+
protected function getFormat()
42+
{
43+
return 'rst';
44+
}
45+
}

0 commit comments

Comments
 (0)
0