Passed
Push — code-quality-badges ( 242de6...32d41e )
by Christian
01:39
created

MarkdownDescriptor::tableOfContents()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 10
nc 1
nop 1
dl 0
loc 14
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
            .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,
144
            function (InputOption $a, InputOption $b) {
145
                $aRequired = $a->isValueRequired() ? 1 : 0;
146
                $bRequired = $b->isValueRequired() ? 1 : 0;
147
                if ($aRequired == $bRequired) {
148
                    return 0;
149
                }
150
151
                return ($aRequired > $bRequired) ? -1 : 1;
152
            });
153
154
        return $options;
155
    }
156
157
    /**
158
     * @param $description
159
     * @param $emphasis
160
     * @return string
161
     */
162
    private function formatOptionDescription($description, $emphasis): string
163
    {
164
        return ($description ? $emphasis.preg_replace('/\s*[\r\n]\s*/', "\n", $description)."\n\n" : '');
165
    }
166
167
    /**
168
     * @param InputOption $option
169
     * @return string
170
     */
171
    private function optionValue(InputOption $option): string
172
    {
173
        return ($option->acceptValue() ? 'yes' : 'no');
174
    }
175
176
    /**
177
     * @param InputOption $option
178
     * @return string
179
     */
180
    private function optionValueRequired(InputOption $option): string
181
    {
182
        return ($option->isValueRequired() ? 'yes' : 'no');
183
    }
184
185
    /**
186
     * @param InputOption $option
187
     * @return string
188
     */
189
    private function optionIsMultiple(InputOption $option): string
190
    {
191
        return ($option->isArray() ? 'yes' : 'no');
192
    }
193
194
    /**
195
     * @param InputOption $option
196
     * @return string
197
     */
198
    private function optionEmphasis(InputOption $option): string
199
    {
200
        return $option instanceof StyledInputOption ? '##### ' : '';
201
    }
202
203
    /**
204
     * @param InputArgument $argument
205
     * @return string
206
     */
207
    private function argumentIsRequired(InputArgument $argument): string
208
    {
209
        return ($argument->isRequired() ? 'yes' : 'no');
210
    }
211
212
    /**
213
     * @param InputArgument $argument
214
     * @return string
215
     */
216
    private function argumentIsArray(InputArgument $argument): string
217
    {
218
        return ($argument->isArray() ? 'yes' : 'no');
219
    }
220
221
    /**
222
     * @param InputArgument $argument
223
     * @return string
224
     */
225
    private function argumentName(InputArgument $argument): string
226
    {
227
        return ($argument->getName() ?: '<none>');
228
    }
229
230
    /**
231
     * @param $description
232
     * @return string
233
     */
234
    private function formatArgumentDescription($description): string
235
    {
236
        return ($description ? preg_replace('/\s*[\r\n]\s*/',
237
                "\n",
238
                $description)."\n\n" : '');
239
    }
240
241
    /**
242
     * @param InputDefinition $definition
243
     * @return bool
244
     */
245
    private function describeInputDefinitionArguments(InputDefinition $definition): bool
246
    {
247
        if ($showArguments = count($definition->getArguments()) > 0) {
248
            $this->write('### Arguments');
249
            foreach ($definition->getArguments() as $argument) {
250
                $this->write("\n\n");
251
                $this->describeInputArgument($argument);
252
            }
253
        }
254
255
        return $showArguments;
256
    }
257
258
    /**
259
     * @param InputDefinition $definition
260
     * @param $showArguments
261
     */
262
    private function describeInputDefinitionOptions(InputDefinition $definition, $showArguments): void
263
    {
264
        $inputOptions = $this->getInputOptions($definition);
265
        if (count($inputOptions) > 0) {
266
            if ($showArguments) {
267
                $this->write("\n\n");
268
            }
269
270
            $this->write('### Options');
271
            foreach ($inputOptions as $option) {
272
                $this->write("\n\n");
273
                $this->describeInputOption($option);
274
            }
275
        }
276
    }
277
278
    /**
279
     * @param \Symfony\Component\Console\Descriptor\ApplicationDescription $description
280
     * @return array
281
     */
282
    private function filterCommands($description): array
283
    {
284
        return array_filter($description->getCommands(),
285
            function (Command $command) {
286
                return $command instanceof DocumentedCommand;
287
            });
288
    }
289
290
    /**
291
     * @param \Symfony\Component\Console\Descriptor\ApplicationDescription $description
292
     */
293
    private function tableOfContents($description): void
294
    {
295
        $this->write("\n\n");
296
        $this->write(implode("\n",
297
                array_map(
298
                    function (Command $command) {
299
                        return sprintf(
300
                            "* [`%s`](#%s)\n  > %s",
301
                            $command->getName(),
302
                            str_replace(':', '', $command->getName()),
303
                            $this->removeDecoration($command->getDescription())
304
                        );
305
                    },
306
                    $this->filterCommands($description)
307
                )
308
            )
309
        );
310
    }
311
312
    /**
313
     * @param $options
314
     * @return array
315
     */
316
    private function filterOptions($options): array
317
    {
318
        return array_filter($options,
319
            function (InputOption $option) {
320
                return !in_array(
321
                    $option->getName(),
322
                    ['help', 'quiet', 'verbose', 'version', 'ansi', 'no-ansi', 'no-interaction']
323
                );
324
            });
325
    }
326
}
327