Passed
Push — code-quality-badges ( b32c3e...5ee141 )
by Christian
02:04
created

MarkdownDescriptor::sortOptions()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 1
nop 0
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Cocotte\Console;
4
5
use Symfony\Component\Console\Application;
6
use Symfony\Component\Console\Command\Command;
7
use Symfony\Component\Console\Formatter\OutputFormatter;
8
use Symfony\Component\Console\Helper\Helper;
9
use Symfony\Component\Console\Input\InputArgument;
10
use Symfony\Component\Console\Input\InputDefinition;
11
use Symfony\Component\Console\Input\InputOption;
12
use Symfony\Component\Console\Output\OutputInterface;
13
14
class MarkdownDescriptor
15
{
16
17
    /**
18
     * @var OutputInterface
19
     */
20
    private $output;
21
22
    public function describe(OutputInterface $output, Application $object)
23
    {
24
        $decorated = $output->isDecorated();
25
        $output->setDecorated(false);
26
27
        $this->output = $output;
28
29
        $this->describeApplication($object);
30
31
        $output->setDecorated($decorated);
32
    }
33
34
    /**
35
     * Writes content to output.
36
     *
37
     * @param string $content
38
     * @param bool $decorated
39
     */
40
    private function write($content, $decorated = false)
41
    {
42
        $this->output->write($content,
43
            false,
44
            $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW);
45
    }
46
47
    private function describeInputArgument(InputArgument $argument)
48
    {
49
        $description = $this->removeDecoration($argument->getDescription());
50
        $this->write(
51
            '#### `'.$this->argumentName($argument)."`\n\n"
52
            .$this->formatArgumentDescription($description)
53
            .'* Is required: '.$this->argumentIsRequired($argument)."\n"
54
            .'* Is array: '.$this->argumentIsArray($argument)."\n"
55
            .'* Default: `'.str_replace("\n", '', var_export($argument->getDefault(), true)).'`'
56
        );
57
    }
58
59
    private function describeInputOption(InputOption $option)
60
    {
61
        $name = '--'.$option->getName();
62
        if ($option->getShortcut()) {
63
            $name .= '|-'.implode('|-', explode('|', $option->getShortcut())).'';
64
        }
65
66
        $emphasis = $this->optionEmphasis($option);
67
68
        $description = $this->removeDecoration($option->getDescription());
69
        $this->write(
70
            '#### `'.$name.'`'."\n\n"
71
            .$this->formatOptionDescription($description, $emphasis)
72
            .'* Accept value: '.$this->optionValue($option)."\n"
73
            .'* Is value required: '.$this->optionValueRequired($option)."\n"
74
            .'* Is multiple: '.$this->optionIsMultiple($option)."\n"
75
            .'* Default: `'.str_replace("\n", '', var_export($option->getDefault(), true)).'`'
76
        );
77
    }
78
79
    private function describeInputDefinition(InputDefinition $definition)
80
    {
81
        $showArguments = $this->describeInputDefinitionArguments($definition);
82
83
        $this->describeInputDefinitionOptions($definition, $showArguments);
84
    }
85
86
    private function describeCommand(Command $command)
87
    {
88
        $synopsis = $command->getSynopsis(true);
89
90
        $this->write(
91
            $command->getName()."\n"
92
            .str_repeat('-', Helper::strlen($command->getName()) + 2)."\n\n"
93
            .'### Usage'."\n\n"
94
            .$this->commandUsage($command, $synopsis)
95
        );
96
97
        if ($help = $command->getProcessedHelp()) {
98
            $this->write("\n");
99
            $this->write($this->removeDecoration($help));
100
        }
101
102
        if ($command->getNativeDefinition()) {
103
            $this->write("\n\n");
104
            $this->describeInputDefinition($command->getNativeDefinition());
105
        }
106
    }
107
108
    private function describeApplication(Application $application)
109
    {
110
        $description = new \Symfony\Component\Console\Descriptor\ApplicationDescription($application);
111
        $title = "Console API Reference";
112
113
        $this->write($title."\n".str_repeat('=', Helper::strlen($title)));
114
115
        $this->tableOfContents($description);
116
117
        foreach ($this->filterCommands($description) as $command) {
118
            $this->write("\n\n---\n\n");
119
            $this->describeCommand($command);
120
        }
121
    }
122
123
    private function removeDecoration(string $string): string
124
    {
125
        $f = new OutputFormatter();
126
127
        return $f->format($string);
128
    }
129
130
    /**
131
     * @param InputDefinition $definition
132
     * @return array|InputOption[]
133
     */
134
    private function getInputOptions(InputDefinition $definition)
135
    {
136
        $options = $definition->getOptions();
137
        $options = $this->filterOptions($options);
138
        usort($options, $this->sortOptions());
139
140
        return $options;
141
    }
142
143
    /**
144
     * @param $description
145
     * @param $emphasis
146
     * @return string
147
     */
148
    private function formatOptionDescription($description, $emphasis): string
149
    {
150
        return ($description ? $emphasis.preg_replace('/\s*[\r\n]\s*/', "\n", $description)."\n\n" : '');
151
    }
152
153
    /**
154
     * @param InputOption $option
155
     * @return string
156
     */
157
    private function optionValue(InputOption $option): string
158
    {
159
        return ($option->acceptValue() ? 'yes' : 'no');
160
    }
161
162
    /**
163
     * @param InputOption $option
164
     * @return string
165
     */
166
    private function optionValueRequired(InputOption $option): string
167
    {
168
        return ($option->isValueRequired() ? 'yes' : 'no');
169
    }
170
171
    /**
172
     * @param InputOption $option
173
     * @return string
174
     */
175
    private function optionIsMultiple(InputOption $option): string
176
    {
177
        return ($option->isArray() ? 'yes' : 'no');
178
    }
179
180
    /**
181
     * @param InputOption $option
182
     * @return string
183
     */
184
    private function optionEmphasis(InputOption $option): string
185
    {
186
        return $option instanceof StyledInputOption ? '##### ' : '';
187
    }
188
189
    /**
190
     * @param InputArgument $argument
191
     * @return string
192
     */
193
    private function argumentIsRequired(InputArgument $argument): string
194
    {
195
        return ($argument->isRequired() ? 'yes' : 'no');
196
    }
197
198
    /**
199
     * @param InputArgument $argument
200
     * @return string
201
     */
202
    private function argumentIsArray(InputArgument $argument): string
203
    {
204
        return ($argument->isArray() ? 'yes' : 'no');
205
    }
206
207
    /**
208
     * @param InputArgument $argument
209
     * @return string
210
     */
211
    private function argumentName(InputArgument $argument): string
212
    {
213
        return ($argument->getName() ?: '<none>');
214
    }
215
216
    /**
217
     * @param $description
218
     * @return string
219
     */
220
    private function formatArgumentDescription($description): string
221
    {
222
        return ($description ? preg_replace('/\s*[\r\n]\s*/',
223
                "\n",
224
                $description)."\n\n" : '');
225
    }
226
227
    /**
228
     * @param InputDefinition $definition
229
     * @return bool
230
     */
231
    private function describeInputDefinitionArguments(InputDefinition $definition): bool
232
    {
233
        if ($showArguments = count($definition->getArguments()) > 0) {
234
            $this->write('### Arguments');
235
            foreach ($definition->getArguments() as $argument) {
236
                $this->write("\n\n");
237
                $this->describeInputArgument($argument);
238
            }
239
        }
240
241
        return $showArguments;
242
    }
243
244
    /**
245
     * @param InputDefinition $definition
246
     * @param $showArguments
247
     */
248
    private function describeInputDefinitionOptions(InputDefinition $definition, $showArguments): void
249
    {
250
        $inputOptions = $this->getInputOptions($definition);
251
        if (count($inputOptions) > 0) {
252
            if ($showArguments) {
253
                $this->write("\n\n");
254
            }
255
256
            $this->write('### Options');
257
            foreach ($inputOptions as $option) {
258
                $this->write("\n\n");
259
                $this->describeInputOption($option);
260
            }
261
        }
262
    }
263
264
    /**
265
     * @param \Symfony\Component\Console\Descriptor\ApplicationDescription $description
266
     * @return array
267
     */
268
    private function filterCommands($description): array
269
    {
270
        return array_filter($description->getCommands(),
271
            function (Command $command) {
272
                return $command instanceof DocumentedCommand;
273
            });
274
    }
275
276
    /**
277
     * @param \Symfony\Component\Console\Descriptor\ApplicationDescription $description
278
     */
279
    private function tableOfContents($description): void
280
    {
281
        $this->write("\n\n");
282
        $this->write(implode("\n",
283
                array_map(
284
                    function (Command $command) {
285
                        return sprintf(
286
                            "* [`%s`](#%s)\n  > %s",
287
                            $command->getName(),
288
                            str_replace(':', '', $command->getName()),
289
                            $this->removeDecoration($command->getDescription())
290
                        );
291
                    },
292
                    $this->filterCommands($description)
293
                )
294
            )
295
        );
296
    }
297
298
    /**
299
     * @param $options
300
     * @return array
301
     */
302
    private function filterOptions($options): array
303
    {
304
        return array_filter($options,
305
            function (InputOption $option) {
306
                return !in_array(
307
                    $option->getName(),
308
                    ['help', 'quiet', 'verbose', 'version', 'ansi', 'no-ansi', 'no-interaction']
309
                );
310
            });
311
    }
312
313
    /**
314
     * @return \Closure
315
     */
316
    private function sortOptions(): \Closure
317
    {
318
        return function (InputOption $a, InputOption $b) {
319
            if ($a->isValueRequired() == $b->isValueRequired()) {
320
                return 0;
321
            }
322
            if ($a->isValueRequired()) {
323
                return -1;
324
            }
325
326
            return 1;
327
        };
328
    }
329
330
    /**
331
     * @param Command $command
332
     * @param $synopsis
333
     * @return string
334
     */
335
    private function commandUsage(Command $command, $synopsis): string
336
    {
337
        return array_reduce(array_merge(array($synopsis),
338
            $command->getAliases(),
339
            $command->getUsages()),
340
            function ($carry, $usage) {
341
                return $carry.'* `'.$usage.'`'."\n";
342
            });
343
    }
344
}
345