File Coverage

blib/lib/Term/ANSIColor/Concise.pm
Criterion Covered Total %
statement 153 177 86.4
branch 66 96 68.7
condition 25 33 75.7
subroutine 26 27 96.3
pod 6 13 46.1
total 276 346 79.7


line stmt bran cond sub pod time code
1             package Term::ANSIColor::Concise;
2              
3             our $VERSION = "2.03";
4              
5 3     3   135029 use v5.14;
  3         23  
6 3     3   16 use warnings;
  3         5  
  3         71  
7 3     3   624 use utf8;
  3         17  
  3         20  
8              
9 3     3   96 use Exporter 'import';
  3         6  
  3         288  
10             our @EXPORT = qw();
11             our @EXPORT_OK = qw(
12             ansi_color ansi_color_24 ansi_code ansi_pair csi_code
13             cached_ansi_color
14             map_256_to_6 map_to_256
15             );
16             our %EXPORT_TAGS = (all => [ @EXPORT_OK ]);
17              
18 3     3   19 use Carp;
  3         6  
  3         215  
19 3     3   1982 use Data::Dumper;
  3         21181  
  3         202  
20             $Data::Dumper::Sortkeys = 1;
21              
22 3     3   1186 use Term::ANSIColor::Concise::Util;
  3         8  
  3         97  
23 3     3   18 use List::Util qw(min max first);
  3         7  
  3         838  
24              
25             our $NO_NO_COLOR //= $ENV{ANSICOLOR_NO_NO_COLOR};
26             our $NO_COLOR //= !$NO_NO_COLOR && defined $ENV{NO_COLOR};
27             our $RGB24 //= $ENV{ANSICOLOR_RGB24} // ($ENV{COLORTERM}//'' eq 'truecolor');
28             our $LINEAR_256 //= $ENV{ANSICOLOR_LINEAR_256};
29             our $LINEAR_GRAY //= $ENV{ANSICOLOR_LINEAR_GRAY};
30             our $NO_RESET_EL //= $ENV{ANSICOLOR_NO_RESET_EL};
31             our $SPLIT_ANSI //= $ENV{ANSICOLOR_SPLIT_ANSI};
32              
33             my @nonlinear = do {
34             map { ( $_->[0] ) x $_->[1] } (
35             [ 0, 75 ], # 0 .. 74
36             [ 1, 40 ], # 75 .. 114
37             [ 2, 40 ], # 115 .. 154
38             [ 3, 40 ], # 155 .. 194
39             [ 4, 40 ], # 195 .. 234
40             [ 5, 21 ], # 235 .. 255
41             );
42             };
43              
44             sub map_256_to_6 {
45 3     3   1557 use integer;
  3         43  
  3         13  
46 69     69 0 103 my $i = shift;
47 69 100       110 if ($LINEAR_256) {
48 12         26 5 * $i / 255;
49             } else {
50             # ( $i - 35 ) / 40;
51 57         124 $nonlinear[$i];
52             }
53             }
54              
55             sub map_to_256 {
56 0     0 0 0 my($base, $i) = @_;
57 0 0       0 if ($i == 0) { 0 }
  0 0       0  
    0          
    0          
58 0         0 elsif ($base == 6) { $i * 40 + 55 }
59 0         0 elsif ($base == 12) { $i * 20 + 35 }
60 0         0 elsif ($base == 24) { $i * 10 + 25 }
61 0         0 else { die }
62             }
63              
64             sub ansi256_number {
65 12     12 0 35 my $code = shift;
66 12         26 my($r, $g, $b, $gray);
67 12 100       48 if ($code =~ /^([0-5])([0-5])([0-5])$/) {
    50          
68 8         30 ($r, $g, $b) = ($1, $2, $3);
69             }
70             elsif (my($n) = $code =~ /^L(\d+)/i) {
71 4 50       15 $n > 25 and croak "Color spec error: $code.";
72 4 100 100     16 if ($n == 0 or $n == 25) {
73 2         6 $r = $g = $b = $n / 5;
74             } else {
75 2         5 $gray = $n - 1;
76             }
77             }
78             else {
79 0         0 croak "Color spec error: $code.";
80             }
81 12 100       46 defined $gray ? ($gray + 232) : ($r*36 + $g*6 + $b + 16);
82             }
83              
84             sub rgb24_number {
85 3     3   1059 use integer;
  3         7  
  3         19  
86 24     24 0 51 my($rx, $gx, $bx) = @_;
87 24         35 my($r, $g, $b, $gray);
88 24 100 100     140 if ($rx != 0 and $rx != 255 and $rx == $gx and $rx == $bx) {
      100        
      66        
89 1 50       4 if ($LINEAR_GRAY) {
90             ##
91             ## Divide area into 25 segments, and map to BLACK and 24 GRAYS
92             ##
93 0         0 $gray = $rx * 25 / 255 - 1;
94 0 0       0 if ($gray < 0) {
95 0         0 $r = $g = $b = 0;
96 0         0 $gray = undef;
97             }
98             } else {
99             ## map to 8, 18, 28, ... 238
100 1         5 $gray = min(23, ($rx - 3) / 10);
101             }
102             } else {
103 23         44 ($r, $g, $b) = map { map_256_to_6 $_ } $rx, $gx, $bx;
  69         115  
104             }
105 24 100       116 defined $gray ? ($gray + 232) : ($r*36 + $g*6 + $b + 16);
106             }
107              
108             sub rgbhex {
109 31     31 0 120 my $rgb = shift =~ s/^#//r;
110 31         65 my $len = length $rgb;
111 31 50 33     139 croak "$rgb: Invalid RGB value." if $len == 0 || $len % 3;
112 31         64 $len /= 3;
113 31         76 my $max = (2 ** ($len * 4)) - 1;
114 31 50       490 my @rgb24 = map { hex($_) * 255 / $max } $rgb =~ /[0-9a-z]{$len}/gi or die;
  93         279  
115 31 100       87 if ($RGB24) {
116 7         26 return (2, @rgb24);
117             } else {
118 24         52 return (5, rgb24_number @rgb24);
119             }
120             }
121              
122             my %numbers = (
123             ';' => undef, # ; : NOP
124             N => undef, # N : None (NOP)
125             E => 'EL', # E : Erase Line
126             Z => 0, # Z : Zero (Reset)
127             D => 1, # D : Double Strike (Bold)
128             P => 2, # P : Pale (Dark)
129             I => 3, # I : Italic
130             U => 4, # U : Underline
131             F => 5, # F : Flash (Blink: Slow)
132             Q => 6, # Q : Quick (Blink: Rapid)
133             S => 7, # S : Stand out (Reverse)
134             H => 8, # H : Hide (Concealed)
135             X => 9, # X : Cross out
136             K => 30, k => 90, # K : Kuro (Black)
137             R => 31, r => 91, # R : Red
138             G => 32, g => 92, # G : Green
139             Y => 33, y => 93, # Y : Yellow
140             B => 34, b => 94, # B : Blue
141             M => 35, m => 95, # M : Magenta
142             C => 36, c => 96, # C : Cyan
143             W => 37, w => 97, # W : White
144             );
145              
146             my $colorspec_re = qr{
147             (? /) # /
148             | (? \^) # ^
149             | (? [0-9a-f]{6} # 24bit hex
150             | \#[0-9a-f]{3,} ) # generic hex
151             | (? \(\d+,\d+,\d+\) ) # 24bit decimal
152             | (? [0-5][0-5][0-5] # 216 (6x6x6) colors
153             | L(?:[01][0-9]|[2][0-5]) ) # 24 gray levels + B/W
154             | (? [KRGYBMCW] ) # 16 colors
155             | (? ~?[;NZDPIUFQSHX] ) # effects
156             | (? { (?[A-Z]+) # other CSI
157             (?

\( )? # optional (

158             (?[\d,;]*) # 0;1;2
159             (?(

) \) ) # closing )

160             }
161             | (?[E]) ) # abbreviation
162             | < (? \w+ ) > #
163             }xi;
164              
165             sub ansi_numbers {
166 53   50 53 0 139 local $_ = shift // '';
167 53         82 my @numbers;
168 53         195 my $toggle = ToggleValue->new(value => 10);
169              
170 53         899 while (m{\G (?: $colorspec_re | (? .+ ) ) }xig) {
171 3 100   3   3518 if ($+{toggle}) {
  3 50       1106  
  3 100       1986  
  114 100       1177  
    100          
    100          
    100          
    100          
    50          
    0          
172 17         41 $toggle->toggle;
173             }
174             elsif ($+{reset}) {
175 0         0 $toggle->reset;
176             }
177             elsif ($+{hex}) {
178 23         80 push @numbers, 38 + $toggle->value, rgbhex($+{hex});
179             }
180             elsif (my $rgb = $+{rgb}) {
181 5         31 my @rgb = $rgb =~ /(\d+)/g;
182 5 50       12 croak "Unexpected value: $rgb." if grep { $_ > 255 } @rgb;
  15         49  
183 5         23 my $hex = sprintf "%02X%02X%02X", @rgb;
184 5         17 push @numbers, 38 + $toggle->value, rgbhex($hex);
185             }
186             elsif ($+{c256}) {
187 12         35 push @numbers, 38 + $toggle->value, 5, ansi256_number $+{c256};
188             }
189             elsif ($+{c16}) {
190 16         80 push @numbers, $numbers{$+{c16}} + $toggle->value;
191             }
192             elsif ($+{efct}) {
193 28         95 my $efct = uc $+{efct};
194 28 100       88 my $offset = $efct =~ s/^~// ? 20 : 0;
195 28 100       67 if (defined (my $n = $numbers{$efct})) {
196 26         51 push @numbers, $n + $offset;
197             }
198             }
199             elsif ($+{csi}) {
200 9         15 push @numbers, do {
201 9 100       29 if ($+{csi_abbr}) {
202 3         17 [ $numbers{uc $+{csi_abbr}} ];
203             } else {
204 6         52 [ uc $+{csi_name}, $+{csi_param} =~ /\d+/g ];
205             }
206             };
207             }
208             elsif ($+{name}) {
209 4         8 state $colornames = do {
210 1         524 require Graphics::ColorNames;
211 1         6647 Graphics::ColorNames->new;
212             };
213 4 100       2347 if (my $rgb = $colornames->hex($+{name})) {
214 3         119 push @numbers, 38 + $toggle->value, rgbhex($rgb);
215             } else {
216 1         222 croak "Unknown color name: $+{name}.";
217             }
218             }
219             elsif (my $err = $+{err}) {
220 0         0 croak "Color spec error: \"$err\" in \"$_\"."
221             }
222             else {
223 0         0 croak "$_: Something strange.";
224             }
225             } continue {
226 113 100       811 if ($SPLIT_ANSI) {
227 3     4   24 my $index = first { not ref $numbers[$_] } 0 .. $#numbers;
  4         11  
228 3 100       17 if (defined $index) {
229 2         6 my @sgr = splice @numbers, $index;
230 2         17 push @numbers, [ 'SGR', @sgr ];
231             }
232             }
233             }
234 52         224 @numbers;
235             }
236              
237             use constant {
238 3         2602 CSI => "\e[", # Control Sequence Introducer
239             RESET => "\e[m", # SGR Reset
240             EL => "\e[K", # Erase Line
241 3     3   37 };
  3         7  
242              
243             my %csi_terminator = (
244             ICH => '@', # Insert Character
245             CUU => 'A', # Cursor up
246             CUD => 'B', # Cursor Down
247             CUF => 'C', # Cursor Forward
248             CUB => 'D', # Cursor Back
249             CNL => 'E', # Cursor Next Line
250             CPL => 'F', # Cursor Previous line
251             CHA => 'G', # Cursor Horizontal Absolute
252             CUP => 'H', # Cursor Position
253             ED => 'J', # Erase in Display (0 after, 1 before, 2 entire, 3 w/buffer)
254             EL => 'K', # Erase in Line (0 after, 1 before, 2 entire)
255             IL => 'L', # Insert Line
256             DL => 'M', # Delete Line
257             DCH => 'P', # Delete Character
258             SU => 'S', # Scroll Up
259             SD => 'T', # Scroll Down
260             ECH => 'X', # Erase Character
261             HVP => 'f', # Horizontal Vertical Position
262             SGR => 'm', # Select Graphic Rendition
263             SCP => 's', # Save Cursor Position
264             RCP => 'u', # Restore Cursor Position
265             );
266              
267             my %other_sequence = (
268             RIS => "\ec", # Reset to Initial State
269             DECSC => "\e7", # DEC Save Cursor
270             DECRC => "\e8", # DEC Restore Cursor
271             );
272              
273             sub csi_code {
274 59     59 1 120 my $name = shift;
275 59 50       128 if (my $seq = $other_sequence{$name}) {
276 0         0 return $seq;
277             }
278 59 50       141 my $c = $csi_terminator{$name} or do {
279 0         0 warn "$name: Unknown ANSI name.\n";
280 0         0 return '';
281             };
282 59 50 100     222 if ($name eq 'SGR' and @_ == 1 and $_[0] == 0) {
      66        
283 0         0 @_ = ();
284             }
285 59         361 CSI . join(';', @_) . $c;
286             }
287              
288             sub ansi_code {
289 53     53 1 4552 my $spec = shift;
290 53         111 my @numbers = ansi_numbers $spec;
291 52         85 my @code;
292 52         124 while (@numbers) {
293 59         105 my $item = shift @numbers;
294 59 100       127 if (ref($item) eq 'ARRAY') {
295 11         24 push @code, csi_code @$item;
296             } else {
297 48         81 my @sgr = ($item);
298 48   100     171 while (@numbers and not ref $numbers[0]) {
299 135         373 push @sgr, shift @numbers;
300             }
301 48         108 push @code, csi_code 'SGR', @sgr;
302             }
303             }
304 52         265 join '', @code;
305             }
306              
307             sub ansi_pair {
308 8     8 1 617 my $spec = shift;
309 8         14 my $el = 0;
310 8   50     24 my $start = ansi_code $spec // '';
311 8 100       21 my $end = $start eq '' ? '' : do {
312 6 100       31 if ($start =~ /(.*)(\e\[[0;]*K)(.*)/) {
313 2         4 $el = 1;
314 2 50       6 if ($3) {
315 0         0 $1 . EL . RESET;
316             } else {
317 2         4 EL . RESET;
318             }
319             } else {
320 4 50       11 if ($NO_RESET_EL) {
321 0         0 RESET;
322             } else {
323 4         7 RESET . EL;
324             }
325             }
326             };
327 8         43 ($start, $end, $el);
328             }
329              
330             sub ansi_color {
331 37     37 1 2896 cached_ansi_color(state $cache = {}, @_);
332             }
333              
334             sub ansi_color_24 {
335 2     2 1 6 local $RGB24 = 1;
336 2         36 cached_ansi_color(state $cache = {}, @_);
337             }
338              
339             sub cached_ansi_color {
340 39     39 1 69 my $cache = shift;
341 39         58 my @result;
342 39         88 while (@_ >= 2) {
343 39         106 my($spec, $text) = splice @_, 0, 2;
344 39 50       98 for my $color (ref $spec eq 'ARRAY' ? @$spec : $spec) {
345 39         65 $text = apply_color($cache, $color, $text);
346             }
347 39         104 push @result, $text;
348             }
349 39 50       71 croak "Wrong number of parameters." if @_;
350 39         182 join '', @result;
351             }
352              
353 3     3   26 use Scalar::Util qw(blessed);
  3         5  
  3         1094  
354              
355             sub apply_color {
356 39     39 0 73 my($cache, $color, $text) = @_;
357 39 50 33     139 if (ref $color eq 'CODE') {
    50          
    100          
358 0         0 return $color->($text);
359             }
360             elsif (blessed $color and $color->can('call')) {
361 0         0 return $color->call for $text;
362             }
363             elsif ($NO_COLOR) {
364 18         45 return $text;
365             }
366             else {
367 21   100     28 my($s, $e, $el) = @{ $cache->{$color} //= [ ansi_pair($color) ] };
  21         66  
368 21         36 state $reset = qr{ \e\[[0;]*m (?: \e\[[0;]*[Km] )* }x;
369 21 50       37 if ($el) {
370 0         0 $text =~ s/(\A|(?<=[\r\n])|$reset)\K(?[^\e\r\n]+|(?
371             } else {
372 21         301 $text =~ s/(\A|(?<=[\r\n])|$reset)\K(?[^\e\r\n]+)/${s}$+{x}${e}/mg;
373             }
374 21         87 return $text;
375             }
376             }
377              
378             1;
379              
380             __END__