Passed
Push — code-quality-badges ( 22ddc0...88262e )
by Christian
02:49
created

MarkdownDocumentation::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\Documentation;
4
5
use Cocotte\Console\DocumentedCommand;
6
use Symfony\Component\Console\Application;
7
use Symfony\Component\Console\Command\Command;
8
use Symfony\Component\Console\Formatter\OutputFormatter;
9
use Symfony\Component\Console\Helper\Helper;
10
use Symfony\Component\Console\Input\InputDefinition;
11
use Symfony\Component\Console\Input\InputOption;
12
use Symfony\Component\Console\Output\OutputInterface;
13
14
class MarkdownDocumentation
15
{
16
17
    /**
18
     * @var OutputInterface
19
     */
20
    private $output;
21
22
    /**
23
     * @var OptionDescriber
24
     */
25
    private $optionDescriber;
26
27
    /**
28
     * @var ArgumentDescriber
29
     */
30
    private $argumentDescriber;
31
32
    public function __construct(OutputInterface $output)
33
    {
34
        $this->output = $output;
35
        $this->optionDescriber = new OptionDescriber($output);
36
        $this->argumentDescriber = new ArgumentDescriber($output);
37
    }
38
39
    public function document(Application $object)
40
    {
41
        $decorated = $this->output->isDecorated();
42
        $this->output->setDecorated(false);
43
44
        $this->describeApplication($object);
45
46
        $this->output->setDecorated($decorated);
47
    }
48
49
    /**
50
     * Writes content to output.
51
     *
52
     * @param string $content
53
     * @param bool $decorated
54
     */
55
    private function write($content, $decorated = false)
56
    {
57
        $this->output->write($content,
58
            false,
59
            $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW);
60
    }
61
62
    private function describeInputDefinition(InputDefinition $definition)
63
    {
64
        $showArguments = $this->describeInputDefinitionArguments($definition);
65
66
        $this->describeInputDefinitionOptions($definition, $showArguments);
67
    }
68
69
    private function describeCommand(Command $command)
70
    {
71
        $synopsis = $command->getSynopsis(true);
72
73
        $this->write(
74
            $command->getName()."\n"
75
            .str_repeat('-', Helper::strlen($command->getName()) + 2)."\n\n"
76
            .'### Usage'."\n\n"
77
            .$this->commandUsage($command, $synopsis)
78
        );
79
80
        if ($help = $command->getProcessedHelp()) {
81
            $this->write("\n");
82
            $this->write($this->removeDecoration($help));
83
        }
84
85
        if ($command->getNativeDefinition()) {
86
            $this->write("\n\n");
87
            $this->describeInputDefinition($command->getNativeDefinition());
88
        }
89
    }
90
91
    private function describeApplication(Application $application)
92
    {
93
        $description = new \Symfony\Component\Console\Descriptor\ApplicationDescription($application);
94
        $title = "Console API Reference";
95
96
        $this->write($title."\n".str_repeat('=', Helper::strlen($title)));
97
98
        $this->tableOfContents($description);
99
100
        foreach ($this->filterCommands($description) as $command) {
101
            $this->write("\n\n---\n\n");
102
            $this->describeCommand($command);
103
        }
104
    }
105
106
    private function removeDecoration(string $string): string
107
    {
108
        $f = new OutputFormatter();
109
110
        return $f->format($string);
111
    }
112
113
    /**
114
     * @param InputDefinition $definition
115
     * @return array|InputOption[]
116
     */
117
    private function getInputOptions(InputDefinition $definition)
118
    {
119
        $options = $definition->getOptions();
120
        $options = $this->filterOptions($options);
121
        usort($options, $this->sortOptions());
122
123
        return $options;
124
    }
125
126
    /**
127
     * @param InputDefinition $definition
128
     * @return bool
129
     */
130
    private function describeInputDefinitionArguments(InputDefinition $definition): bool
131
    {
132
        if ($showArguments = count($definition->getArguments()) > 0) {
133
            $this->write('### Arguments');
134
            foreach ($definition->getArguments() as $argument) {
135
                $this->write("\n\n");
136
                $this->argumentDescriber->describe($argument);
137
            }
138
        }
139
140
        return $showArguments;
141
    }
142
143
    /**
144
     * @param InputDefinition $definition
145
     * @param $showArguments
146
     */
147
    private function describeInputDefinitionOptions(InputDefinition $definition, $showArguments): void
148
    {
149
        $inputOptions = $this->getInputOptions($definition);
150
        if (count($inputOptions) > 0) {
151
            if ($showArguments) {
152
                $this->write("\n\n");
153
            }
154
155
            $this->write('### Options');
156
            foreach ($inputOptions as $option) {
157
                $this->write("\n\n");
158
                $this->optionDescriber->describe($option);
159
            }
160
        }
161
    }
162
163
    /**
164
     * @param \Symfony\Component\Console\Descriptor\ApplicationDescription $description
165
     * @return array
166
     */
167
    private function filterCommands($description): array
168
    {
169
        return array_filter($description->getCommands(),
170
            function (Command $command) {
171
                return $command instanceof DocumentedCommand;
172
            });
173
    }
174
175
    /**
176
     * @param \Symfony\Component\Console\Descriptor\ApplicationDescription $description
177
     */
178
    private function tableOfContents($description): void
179
    {
180
        $this->write("\n\n");
181
        $this->write(implode("\n",
182
                array_map(
183
                    function (Command $command) {
184
                        return sprintf(
185
                            "* [`%s`](#%s)\n  > %s",
186
                            $command->getName(),
187
                            str_replace(':', '', $command->getName()),
188
                            $this->removeDecoration($command->getDescription())
189
                        );
190
                    },
191
                    $this->filterCommands($description)
192
                )
193
            )
194
        );
195
    }
196
197
    /**
198
     * @param $options
199
     * @return array
200
     */
201
    private function filterOptions($options): array
202
    {
203
        return array_filter($options,
204
            function (InputOption $option) {
205
                return !in_array(
206
                    $option->getName(),
207
                    ['help', 'quiet', 'verbose', 'version', 'ansi', 'no-ansi', 'no-interaction']
208
                );
209
            });
210
    }
211
212
    /**
213
     * @return \Closure
214
     */
215
    private function sortOptions(): \Closure
216
    {
217
        return function (InputOption $a, InputOption $b) {
218
            if ($a->isValueRequired() == $b->isValueRequired()) {
219
                return 0;
220
            }
221
            if ($a->isValueRequired()) {
222
                return -1;
223
            }
224
225
            return 1;
226
        };
227
    }
228
229
    /**
230
     * @param Command $command
231
     * @param $synopsis
232
     * @return string
233
     */
234
    private function commandUsage(Command $command, $synopsis): string
235
    {
236
        return array_reduce(array_merge(array($synopsis),
237
            $command->getAliases(),
238
            $command->getUsages()),
239
            function ($carry, $usage) {
240
                return $carry.'* `'.$usage.'`'."\n";
241
            });
242
    }
243
}
244