1717 * Formatter style class for defining styles.
1818 *
1919 * @author Konstantin Kudryashov <ever.zet@gmail.com>
20+ * @author Rob Frawley 2nd <rmf@src.run>
2021 */
2122class OutputFormatterStyle implements OutputFormatterStyleInterface
2223{
23- private static $ availableForegroundColors = array (
24- 'black ' => array ('set ' => 30 , 'unset ' => 39 ),
25- 'red ' => array ('set ' => 31 , 'unset ' => 39 ),
26- 'green ' => array ('set ' => 32 , 'unset ' => 39 ),
27- 'yellow ' => array ('set ' => 33 , 'unset ' => 39 ),
28- 'blue ' => array ('set ' => 34 , 'unset ' => 39 ),
29- 'magenta ' => array ('set ' => 35 , 'unset ' => 39 ),
30- 'cyan ' => array ('set ' => 36 , 'unset ' => 39 ),
31- 'white ' => array ('set ' => 37 , 'unset ' => 39 ),
32- 'default ' => array ('set ' => 39 , 'unset ' => 39 ),
24+ private const FOREGROUND_COLOR_FORMATS = array (
25+ '04-bit-normal ' => '3%d ' ,
26+ '04-bit-bright ' => '9%d ' ,
27+ '08-bit ' => '38;5;%d ' ,
28+ '24-bit ' => '38;2;%d;%d;%d ' ,
3329 );
34- private static $ availableBackgroundColors = array (
35- 'black ' => array ('set ' => 40 , 'unset ' => 49 ),
36- 'red ' => array ('set ' => 41 , 'unset ' => 49 ),
37- 'green ' => array ('set ' => 42 , 'unset ' => 49 ),
38- 'yellow ' => array ('set ' => 43 , 'unset ' => 49 ),
39- 'blue ' => array ('set ' => 44 , 'unset ' => 49 ),
40- 'magenta ' => array ('set ' => 45 , 'unset ' => 49 ),
41- 'cyan ' => array ('set ' => 46 , 'unset ' => 49 ),
42- 'white ' => array ('set ' => 47 , 'unset ' => 49 ),
43- 'default ' => array ('set ' => 49 , 'unset ' => 49 ),
30+ private const FOREGROUND_COLOR_RANGE = array (0 , 255 );
31+ private const FOREGROUND_COLOR_UNSET = 39 ;
32+ private const FOREGROUND_COLOR_NAMES = array (
33+ 'black ' => 0 ,
34+ 'red ' => 1 ,
35+ 'green ' => 2 ,
36+ 'yellow ' => 3 ,
37+ 'blue ' => 4 ,
38+ 'magenta ' => 5 ,
39+ 'cyan ' => 6 ,
40+ 'white ' => 7 ,
41+ 'default ' => 9 ,
4442 );
45- private static $ availableOptions = array (
43+ private const BACKGROUND_COLOR_FORMATS = array (
44+ '04-bit-normal ' => '4%d ' ,
45+ '04-bit-bright ' => '10%d ' ,
46+ '08-bit ' => '48;5;%d ' ,
47+ '24-bit ' => '48;2;%d;%d;%d ' ,
48+ );
49+ private const BACKGROUND_COLOR_RANGE = array (0 , 255 );
50+ private const BACKGROUND_COLOR_UNSET = 49 ;
51+ private const BACKGROUND_COLOR_NAMES = array (
52+ 'black ' => 0 ,
53+ 'red ' => 1 ,
54+ 'green ' => 2 ,
55+ 'yellow ' => 3 ,
56+ 'blue ' => 4 ,
57+ 'magenta ' => 5 ,
58+ 'cyan ' => 6 ,
59+ 'white ' => 7 ,
60+ 'default ' => 9 ,
61+ );
62+ private const FORMATTING_OPTIONS = array (
4663 'bold ' => array ('set ' => 1 , 'unset ' => 22 ),
4764 'underscore ' => array ('set ' => 4 , 'unset ' => 24 ),
4865 'blink ' => array ('set ' => 5 , 'unset ' => 25 ),
@@ -89,15 +106,11 @@ public function setForeground($color = null)
89106 return ;
90107 }
91108
92- if (!isset (static ::$ availableForegroundColors [$ color ])) {
93- throw new InvalidArgumentException (sprintf (
94- 'Invalid foreground color specified: "%s". Expected one of (%s) ' ,
95- $ color ,
96- implode (', ' , array_keys (static ::$ availableForegroundColors ))
97- ));
109+ if (null === $ definition = self ::buildColorDefinition ($ color , 'foreground ' )) {
110+ throw new InvalidArgumentException (self ::getInputColorExcMessage ($ color , 'foreground ' ));
98111 }
99112
100- $ this ->foreground = static :: $ availableForegroundColors [ $ color ] ;
113+ $ this ->foreground = $ definition ;
101114 }
102115
103116 /**
@@ -115,15 +128,11 @@ public function setBackground($color = null)
115128 return ;
116129 }
117130
118- if (!isset (static ::$ availableBackgroundColors [$ color ])) {
119- throw new InvalidArgumentException (sprintf (
120- 'Invalid background color specified: "%s". Expected one of (%s) ' ,
121- $ color ,
122- implode (', ' , array_keys (static ::$ availableBackgroundColors ))
123- ));
131+ if (null === $ definition = self ::buildColorDefinition ($ color , 'background ' )) {
132+ throw new InvalidArgumentException (self ::getInputColorExcMessage ($ color , 'background ' ));
124133 }
125134
126- $ this ->background = static :: $ availableBackgroundColors [ $ color ] ;
135+ $ this ->background = $ definition ;
127136 }
128137
129138 /**
@@ -135,16 +144,12 @@ public function setBackground($color = null)
135144 */
136145 public function setOption ($ option )
137146 {
138- if (!isset (static ::$ availableOptions [$ option ])) {
139- throw new InvalidArgumentException (sprintf (
140- 'Invalid option specified: "%s". Expected one of (%s) ' ,
141- $ option ,
142- implode (', ' , array_keys (static ::$ availableOptions ))
143- ));
147+ if (!isset (self ::FORMATTING_OPTIONS [$ option ])) {
148+ throw new InvalidArgumentException (self ::getInputOptionExcMessage ($ option ));
144149 }
145150
146- if (!in_array (static :: $ availableOptions [$ option ], $ this ->options )) {
147- $ this ->options [] = static :: $ availableOptions [$ option ];
151+ if (!in_array (self :: FORMATTING_OPTIONS [$ option ], $ this ->options )) {
152+ $ this ->options [] = self :: FORMATTING_OPTIONS [$ option ];
148153 }
149154 }
150155
@@ -157,15 +162,11 @@ public function setOption($option)
157162 */
158163 public function unsetOption ($ option )
159164 {
160- if (!isset (static ::$ availableOptions [$ option ])) {
161- throw new InvalidArgumentException (sprintf (
162- 'Invalid option specified: "%s". Expected one of (%s) ' ,
163- $ option ,
164- implode (', ' , array_keys (static ::$ availableOptions ))
165- ));
165+ if (!isset (self ::FORMATTING_OPTIONS [$ option ])) {
166+ throw new InvalidArgumentException (self ::getInputOptionExcMessage ($ option ));
166167 }
167168
168- $ pos = array_search (static :: $ availableOptions [$ option ], $ this ->options );
169+ $ pos = array_search (self :: FORMATTING_OPTIONS [$ option ], $ this ->options );
169170 if (false !== $ pos ) {
170171 unset($ this ->options [$ pos ]);
171172 }
@@ -216,4 +217,182 @@ public function apply($text)
216217
217218 return sprintf ("\033[%sm%s \033[%sm " , implode ('; ' , $ setCodes ), $ text , implode ('; ' , $ unsetCodes ));
218219 }
220+
221+ /**
222+ * @param string $input
223+ * @param string $context
224+ *
225+ * @return array|null
226+ */
227+ private static function buildColorDefinition (string $ input , string $ context ): ?array
228+ {
229+ if (null !== $ inst = self ::createColorDef24Bit ($ input )) {
230+ return self ::finalizeColorDefinition ($ inst , $ context );
231+ }
232+
233+ if (null !== $ inst = self ::createColorDef08Bit ($ input , self ::getColorConstForContext ($ context , 'range ' ))) {
234+ return self ::finalizeColorDefinition ($ inst , $ context );
235+ }
236+
237+ if (null !== $ inst = self ::createColorDef04Bit ($ input , self ::getColorConstForContext ($ context , 'names ' ))) {
238+ return self ::finalizeColorDefinition ($ inst , $ context );
239+ }
240+
241+ return null ;
242+ }
243+
244+ /**
245+ * @param string $color
246+ *
247+ * @return array|null
248+ */
249+ private static function createColorDef24Bit (string $ color ): ?array
250+ {
251+ $ match = self ::matchColorStr ('/^mode\-(?<r>[0-9]+),(?<b>[0-9]+),(?<g>[0-9]+)$/ ' , $ color );
252+ $ level = function (int $ coordinate ): bool {
253+ return $ coordinate >= 0 && $ coordinate <= 255 ;
254+ };
255+
256+ if (null !== $ match && $ level ($ match ['r ' ]) && $ level ($ match ['b ' ]) && $ level ($ match ['g ' ])) {
257+ return array (
258+ 'type ' => '24-bit ' ,
259+ 'args ' => array ($ match ['r ' ], $ match ['b ' ], $ match ['g ' ]),
260+ );
261+ }
262+
263+ return null ;
264+ }
265+
266+ /**
267+ * @param string $color
268+ * @param array $range
269+ *
270+ * @return array|null
271+ */
272+ private static function createColorDef08Bit (string $ color , array $ range ): ?array
273+ {
274+ $ match = self ::matchColorStr ('/^mode\-(?<name>[0-9]+)$/ ' , $ color );
275+
276+ if (null !== $ match && $ match ['name ' ] >= $ range [0 ] && $ match ['name ' ] <= $ range [1 ]) {
277+ return array (
278+ 'type ' => '08-bit ' ,
279+ 'args ' => array ($ match ['name ' ]),
280+ );
281+ }
282+
283+ return null ;
284+ }
285+
286+ /**
287+ * @param string $color
288+ * @param array $allow
289+ *
290+ * @return array|null
291+ */
292+ private static function createColorDef04Bit (string $ color , array $ allow ): ?array
293+ {
294+ $ match = self ::matchColorStr ('/^(?<type>normal|bright)\-(?<name>[a-z]+)$/ ' , $ color );
295+
296+ if (null !== $ match && isset ($ allow [$ match ['name ' ]])) {
297+ return array (
298+ 'type ' => sprintf ('04-bit-%s ' , $ match ['type ' ]),
299+ 'args ' => array ($ allow [$ match ['name ' ]]),
300+ );
301+ }
302+
303+ if (isset ($ allow [$ color ])) {
304+ return array (
305+ 'type ' => '04-bit-normal ' ,
306+ 'args ' => array ($ allow [$ color ]),
307+ );
308+ }
309+
310+ return null ;
311+ }
312+
313+ /**
314+ * @param array $partial
315+ * @param string $context
316+ *
317+ * @return array
318+ */
319+ private static function finalizeColorDefinition (array $ partial , string $ context )
320+ {
321+ return $ partial + array (
322+ 'unset ' => self ::getColorConstForContext ($ context , 'unset ' ),
323+ 'set ' => vsprintf (
324+ self ::getColorConstForContext ($ context , 'formats ' )[$ partial ['type ' ]], $ partial ['args ' ]
325+ ),
326+ );
327+ }
328+
329+ /**
330+ * @param string $regex
331+ * @param string $color
332+ *
333+ * @return array|null
334+ */
335+ private static function matchColorStr (string $ regex , string $ color ): ?array
336+ {
337+ if (1 === preg_match ($ regex , $ color , $ match )) {
338+ return $ match ;
339+ }
340+
341+ return null ;
342+ }
343+
344+ /**
345+ * @param string $color
346+ * @param string $context
347+ *
348+ * @return string
349+ */
350+ private static function getInputColorExcMessage (string $ color , string $ context ): string
351+ {
352+ $ format = 'Invalid %s color "%s" specified. Accepted colors include 24-bit colors as "mode-<int>,<int>,<int>" '
353+ .'(RGB), 8-bit colors as "mode-<int>" (0-255), and 4-bit colors as "<string>", "default-<name>", or '
354+ .'"bright-<name>". Accepted 4-bit color names: %s. ' ;
355+
356+ return vsprintf ($ format , array (
357+ $ context ,
358+ $ color ,
359+ self ::getFormattedArrayKeysFlattened (self ::getColorConstForContext ($ context , 'names ' )),
360+ ));
361+ }
362+
363+ /**
364+ * @param string $option
365+ *
366+ * @return string
367+ */
368+ private static function getInputOptionExcMessage (string $ option ): string
369+ {
370+ return vsprintf ('Invalid option specified: "%s". Accepted options names: %s. ' , array (
371+ $ option ,
372+ self ::getFormattedArrayKeysFlattened (self ::FORMATTING_OPTIONS ),
373+ ));
374+ }
375+
376+ /**
377+ * @param string[] $array
378+ *
379+ * @return string
380+ */
381+ private static function getFormattedArrayKeysFlattened (array $ array ): string
382+ {
383+ return implode (', ' , array_map (function (string $ string ): string {
384+ return sprintf ('"%s" ' , $ string );
385+ }, array_keys ($ array )));
386+ }
387+
388+ /**
389+ * @param string $context
390+ * @param string $name
391+ *
392+ * @return mixed
393+ */
394+ private static function getColorConstForContext (string $ context , string $ name )
395+ {
396+ return constant (strtoupper (sprintf ('self::%s_COLOR_%s ' , $ context , $ name )));
397+ }
219398}
0 commit comments