Passed
Push — code-quality-badges ( d8eb7b...c0878c )
by Christian
02:05
created

MarkdownDescriptor::sortOptions()   B

Complexity

Conditions 5
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 6
nc 1
nop 0
dl 0
loc 10
rs 8.8571
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
            .array_reduce(array_merge(array($synopsis),
95
                $command->getAliases(),
96
                $command->getUsages()),
97
                function ($carry, $usage) {
98
                    return $carry.'* `'.$usage.'`'."\n";
99
                })
100
        );
101
102
        if ($help = $command->getProcessedHelp()) {
103
            $this->write("\n");
104
            $this->write($this->removeDecoration($help));
105
        }
106
107
        if ($command->getNativeDefinition()) {
108
            $this->write("\n\n");
109
            $this->describeInputDefinition($command->getNativeDefinition());
110
        }
111
    }
112
113
    private function describeApplication(Application $application)
114
    {
115
        $description = new \Symfony\Component\Console\Descriptor\ApplicationDescription($application);
116
        $title = "Console API Reference";
117
118
        $this->write($title."\n".str_repeat('=', Helper::strlen($title)));
119
120
        $this->tableOfContents($description);
121
122
        foreach ($this->filterCommands($description) as $command) {
123
            $this->write("\n\n---\n\n");
124
            $this->describeCommand($command);
125
        }
126
    }
127
128
    private function removeDecoration(string $string): string
129
    {
130
        $f = new OutputFormatter();
131
132
        return $f->format($string);
133
    }
134
135
    /**
136
     * @param InputDefinition $definition
137
     * @return array|InputOption[]
138
     */
139
    private function getInputOptions(InputDefinition $definition)
140
    {
141
        $options = $definition->getOptions();
142
        $options = $this->filterOptions($options);
143
        usort($options, $this->sortOptions());
144
145
        return $options;
146
    }
147
148
    /**
149
     * @param $description
150
     * @param $emphasis
151
     * @return string
152
     */
153
    private function formatOptionDescription($description, $emphasis): string
154
    {
155
        return ($description ? $emphasis.preg_replace('/\s*[\r\n]\s*/', "\n", $description)."\n\n" : '');
156
    }
157
158
    /**
159
     * @param InputOption $option
160
     * @return string
161
     */
162
    private function optionValue(InputOption $option): string
163
    {
164
        return ($option->acceptValue() ? 'yes' : 'no');
165
    }
166
167
    /**
168
     * @param InputOption $option
169
     * @return string
170
     */
171
    private function optionValueRequired(InputOption $option): string
172
    {
173
        return ($option->isValueRequired() ? 'yes' : 'no');
174
    }
175
176
    /**
177
     * @param InputOption $option
178
     * @return string
179
     */
180
    private function optionIsMultiple(InputOption $option): string
181
    {
182
        return ($option->isArray() ? 'yes' : 'no');
183
    }
184
185
    /**
186
     * @param InputOption $option
187
     * @return string
188
     */
189
    private function optionEmphasis(InputOption $option): string
190
    {
191
        return $option instanceof StyledInputOption ? '##### ' : '';
192
    }
193
194
    /**
195
     * @param InputArgument $argument
196
     * @return string
197
     */
198
    private function argumentIsRequired(InputArgument $argument): string
199
    {
200
        return ($argument->isRequired() ? 'yes' : 'no');
201
    }
202
203
    /**
204
     * @param InputArgument $argument
205
     * @return string
206
     */
207
    private function argumentIsArray(InputArgument $argument): string
208
    {
209
        return ($argument->isArray() ? 'yes' : 'no');
210
    }
211
212
    /**
213
     * @param InputArgument $argument
214
     * @return string
215
     */
216
    private function argumentName(InputArgument $argument): string
217
    {
218
        return ($argument->getName() ?: '<none>');
219
    }
220
221
    /**
222
     * @param $description
223
     * @return string
224
     */
225
    private function formatArgumentDescription($description): string
226
    {
227
        return ($description ? preg_replace('/\s*[\r\n]\s*/',
228
                "\n",
229
                $description)."\n\n" : '');
230
    }
231
232
    /**
233
     * @param InputDefinition $definition
234
     * @return bool
235
     */
236
    private function describeInputDefinitionArguments(InputDefinition $definition): bool
237
    {
238
        if ($showArguments = count($definition->getArguments()) > 0) {
239
            $this->write('### Arguments');
240
            foreach ($definition->getArguments() as $argument) {
241
                $this->write("\n\n");
242
                $this->describeInputArgument($argument);
243
            }
244
        }
245
246
        return $showArguments;
247
    }
248
249
    /**
250
     * @param InputDefinition $definition
251
     * @param $showArguments
252
     */
253
    private function describeInputDefinitionOptions(InputDefinition $definition, $showArguments): void
254
    {
255
        $inputOptions = $this->getInputOptions($definition);
256
        if (count($inputOptions) > 0) {
257
            if ($showArguments) {
258
                $this->write("\n\n");
259
            }
260
261
            $this->write('### Options');
262
            foreach ($inputOptions as $option) {
263
                $this->write("\n\n");
264
                $this->describeInputOption($option);
265
            }
266
        }
267
    }
268
269
    /**
270
     * @param \Symfony\Component\Console\Descriptor\ApplicationDescription $description
271
     * @return array
272
     */
273
    private function filterCommands($description): array
274
    {
275
        return array_filter($description->getCommands(),
276
            function (Command $command) {
277
                return $command instanceof DocumentedCommand;
278
            });
279
    }
280
281
    /**
282
     * @param \Symfony\Component\Console\Descriptor\ApplicationDescription $description
283
     */
284
    private function tableOfContents($description): void
285
    {
286
        $this->write("\n\n");
287
        $this->write(implode("\n",
288
                array_map(
289
                    function (Command $command) {
290
                        return sprintf(
291
                            "* [`%s`](#%s)\n  > %s",
292
                            $command->getName(),
293
                            str_replace(':', '', $command->getName()),
294
                            $this->removeDecoration($command->getDescription())
295
                        );
296
                    },
297
                    $this->filterCommands($description)
298
                )
299
            )
300
        );
301
    }
302
303
    /**
304
     * @param $options
305
     * @return array
306
     */
307
    private function filterOptions($options): array
308
    {
309
        return array_filter($options,
310
            function (InputOption $option) {
311
                return !in_array(
312
                    $option->getName(),
313
                    ['help', 'quiet', 'verbose', 'version', 'ansi', 'no-ansi', 'no-interaction']
314
                );
315
            });
316
    }
317
318
    /**
319
     * @return \Closure
320
     */
321
    private function sortOptions(): \Closure
322
    {
323
        return function (InputOption $a, InputOption $b) {
324
            $aRequired = $a->isValueRequired() ? 1 : 0;
325
            $bRequired = $b->isValueRequired() ? 1 : 0;
326
            if ($aRequired == $bRequired) {
327
                return 0;
328
            }
329
330
            return ($aRequired > $bRequired) ? -1 : 1;
331
        };
332
    }
333
}
334