Test Failed
Push — master ( 5c7057...3a5cce )
by Alec
02:34
created

ConsoleColor::addTheme()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 2
dl 0
loc 11
ccs 5
cts 5
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace AlecRabbit\ConsoleColour;
4
5
use AlecRabbit\ConsoleColour\Contracts\ConsoleColorInterface;
6
use AlecRabbit\ConsoleColour\Exception\InvalidStyleException;
7
8
class ConsoleColor implements ConsoleColorInterface
9
{
10
    /** @var bool */
11
    protected $supported;
12
13
    /** @var bool */
14
    protected $forceStyle = false;
15
16
    /** @var array */
17
    protected $themes = [];
18
19
    /** @var bool */
20
    protected $are256ColorsSupported;
21
22 74
    public function __construct()
23
    {
24 74
        $terminal = new Terminal();
25 74
        $this->supported = $terminal->supportsColor();
26 74
        $this->are256ColorsSupported = $terminal->supports256Color();
27 74
    }
28
29
    /** {@inheritdoc} */
30 55
    public function apply($styles, $text): string
31
    {
32 55
        if (!$this->isStyleForced() && !$this->isSupported()) {
33 3
            return $text;
34
        }
35
36
        $sequences =
37 52
            $this->getSequencesFrom(
38 52
                $this->refineStyles($styles)
39
            );
40
41 43
        if (empty($sequences)) {
42 6
            return $text;
43
        }
44
45 37
        return $this->applySequences($text, $sequences);
46
    }
47
48
    /** {@inheritdoc} */
49 58
    public function isStyleForced(): bool
50
    {
51 58
        return $this->forceStyle;
52
    }
53
54
    /** {@inheritdoc} */
55 6
    public function isSupported(): bool
56
    {
57 6
        return $this->supported;
58
    }
59
60
    /**
61
     * @param array $styles
62
     * @return array
63
     * @throws InvalidStyleException
64
     */
65 49
    protected function getSequencesFrom(array $styles): array
66
    {
67 49
        $sequences = [[]];
68
69 49
        foreach ($styles as $style) {
70 49
            if (isset($this->themes[$style])) {
71 16
                $sequences[] = $this->themeSequence($style);
72 38
            } elseif ($this->isValid($style)) {
73 32
                $sequences[][] = $this->styleSequence($style);
74
            } else {
75 6
                throw new InvalidStyleException($style);
76
            }
77
        }
78
79
        $sequences =
80 43
            array_filter(
81 43
                array_merge(...$sequences),
82
                /**
83
                 * @param mixed $val
84
                 * @return bool
85
                 * @psalm-suppress MissingClosureParamType
86
                 */
87
                static function ($val): bool {
88 43
                    return $val !== null;
89 43
                }
90
            );
91
92 43
        return $sequences;
93
    }
94
95
    /**
96
     * @param string $name
97
     * @return string[]|null[]
98
     */
99 16
    protected function themeSequence($name): array
100
    {
101 16
        $sequences = [];
102 16
        foreach ($this->themes[$name] as $style) {
103 16
            $sequences[] = $this->styleSequence($style);
104
        }
105 16
        return $sequences;
106
    }
107
108
    /**
109
     * @param string $style
110
     * @return null|string
111
     */
112 43
    protected function styleSequence($style): ?string
113
    {
114 43
        if (\array_key_exists($style, static::CODES)) {
115 31
            return static::CODES[$style];
116
        }
117
118 12
        if (!$this->are256ColorsSupported()) {
119 3
            return null;
120
        }
121
122
        return
123 9
            $this->process256ColorStyle($style);
124
    }
125
126
    /** {@inheritdoc} */
127 2
    public function are256ColorsSupported(): bool
128
    {
129 2
        return $this->are256ColorsSupported;
130
    }
131
132
    /**
133
     * @param string $style
134
     * @return string
135
     */
136 9
    protected function process256ColorStyle(string $style): string
137
    {
138 9
        preg_match(self::COLOR256_REGEXP, $style, $matches);
139
        return
140 9
            sprintf(
141 9
                '%s;5;%s',
142
                $matches[1] === self::BG ? self::BACKGROUND : self::FOREGROUND,
143 9
                $matches[2]
144
            );
145
    }
146
147
    /**
148
     * @param string $style
149
     * @return bool
150 59
     */
151
    protected function isValid($style): bool
152 59
    {
153
        return
154
            \array_key_exists($style, static::CODES) || (bool)preg_match(self::COLOR256_REGEXP, $style);
155
    }
156
157
    /**
158
     * @param int|string|array $styles
159 62
     * @return array
160
     */
161 62
    protected function refineStyles($styles): array
162 38
    {
163
        if (\is_int($styles)) {
164 62
            $styles = [$styles];
165 59
        }
166
        if (\is_string($styles)) {
167
            $styles = [$styles];
168
        }
169
        $this->assertStyles($styles);
170
        return $styles;
171 62
    }
172
173 62
    /**
174 3
     * @param mixed $styles
175
     */
176 59
    protected function assertStyles($styles): void
177
    {
178
        if (!\is_array($styles)) {
179
            throw new \InvalidArgumentException('Style must be string or array.');
180
        }
181
    }
182
183 37
    /**
184
     * @param string $text
185
     * @param array $sequences
186 37
     * @return string
187 37
     */
188 37
    protected function applySequences(string $text, array $sequences): string
189
    {
190
        return
191
            $this->escSequence(implode(';', $sequences)) .
192
            $text .
193
            $this->escSequence((string)self::RESET);
194
    }
195 37
196
    /**
197 37
     * @param string $value
198
     * @return string
199
     */
200
    protected function escSequence(string $value): string
201 6
    {
202
        return "\033[{$value}m";
203 6
    }
204 6
205
    /** {@inheritdoc} */
206
    public function setForceStyle(bool $forceStyle): void
207 1
    {
208
        $this->forceStyle = $forceStyle;
209 1
    }
210
211
    /** {@inheritdoc} */
212
    public function getThemes(): array
213 3
    {
214
        return $this->themes;
215 3
    }
216 3
217 3
    /** {@inheritdoc} */
218
    public function setThemes(array $themes): void
219 3
    {
220
        $this->themes = [];
221
        foreach ($themes as $name => $styles) {
222 26
            $this->addTheme($name, $styles);
223
        }
224 26
    }
225
226 26
    /** {@inheritdoc} */
227 26
    public function addTheme($name, $styles): void
228 3
    {
229
        $styles = $this->refineStyles($styles);
230
231
        foreach ($styles as $style) {
232 23
            if (!$this->isValid($style)) {
233 23
                throw new InvalidStyleException($style);
234
            }
235
        }
236 3
237
        $this->themes[$name] = $styles;
238 3
    }
239
240
    /** {@inheritdoc} */
241
    public function hasTheme($name): bool
242 3
    {
243
        return isset($this->themes[$name]);
244 3
    }
245 3
246
    /** {@inheritdoc} */
247
    public function removeTheme($name): void
248 3
    {
249
        unset($this->themes[$name]);
250 3
    }
251
252
    /** {@inheritdoc} */
253
    public function getPossibleStyles(): array
254
    {
255
        return array_keys(static::CODES);
256
    }
257
}
258