MarkdownDocumentation::commandUsage()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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