File Coverage

blib/lib/Term/ANSIColor/Concise.pm
Criterion Covered Total %
statement 154 186 82.8
branch 67 100 67.0
condition 25 35 71.4
subroutine 26 27 96.3
pod 6 13 46.1
total 278 361 77.0


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

\( )? # optional (

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

) \) ) # closing )

161             }
162             | (?[E]) ) # abbreviation
163             | < (? \w+ ) > #
164             }xi;
165              
166             sub ansi_numbers {
167 59   50 59 0 152 local $_ = shift // '';
168 59         93 my @numbers;
169 59         208 my $toggle = ToggleValue->new(value => 10);
170              
171 59         1017 while (m{\G (?: $colorspec_re | (? .+ ) ) }xig) {
172 3 100   3   3548 if ($+{toggle}) {
  3 50       1162  
  3 100       2034  
  120 100       1292  
    100          
    100          
    100          
    100          
    50          
    0          
173 17         51 $toggle->toggle;
174             }
175             elsif ($+{reset}) {
176 0         0 $toggle->reset;
177             }
178             elsif ($+{hex}) {
179 23         77 push @numbers, 38 + $toggle->value, rgbhex($+{hex});
180             }
181             elsif (my $rgb = $+{rgb}) {
182 5         47 my @rgb = $rgb =~ /(\d+)/g;
183 5 50       13 croak "Unexpected value: $rgb." if grep { $_ > 255 } @rgb;
  15         45  
184 5         25 my $hex = sprintf "%02X%02X%02X", @rgb;
185 5         16 push @numbers, 38 + $toggle->value, rgbhex($hex);
186             }
187             elsif ($+{c256}) {
188 12         35 push @numbers, 38 + $toggle->value, 5, ansi256_number $+{c256};
189             }
190             elsif ($+{c16}) {
191 16         83 push @numbers, $numbers{$+{c16}} + $toggle->value;
192             }
193             elsif ($+{efct}) {
194 34         112 my $efct = uc $+{efct};
195 34 100       114 my $offset = $efct =~ s/^~// ? 20 : 0;
196 34 100       88 if (defined (my $n = $numbers{$efct})) {
197 32         66 push @numbers, $n + $offset;
198             }
199             }
200             elsif ($+{csi}) {
201 9         16 push @numbers, do {
202 9 100       33 if ($+{csi_abbr}) {
203 3         18 [ $numbers{uc $+{csi_abbr}} ];
204             } else {
205 6         54 [ uc $+{csi_name}, $+{csi_param} =~ /\d+/g ];
206             }
207             };
208             }
209             elsif ($+{name}) {
210 4         9 state $colornames = do {
211 1         697 require Graphics::ColorNames;
212 1         7643 Graphics::ColorNames->new;
213             };
214 4 100       2686 if (my $rgb = $colornames->hex($+{name})) {
215 3         125 push @numbers, 38 + $toggle->value, rgbhex($rgb);
216             } else {
217 1         300 croak "Unknown color name: $+{name}.";
218             }
219             }
220             elsif (my $err = $+{err}) {
221 0         0 croak "Color spec error: \"$err\" in \"$_\"."
222             }
223             else {
224 0         0 croak "$_: Something strange.";
225             }
226             } continue {
227 119 100       889 if ($SPLIT_ANSI) {
228 3     4   36 my $index = first { not ref $numbers[$_] } 0 .. $#numbers;
  4         13  
229 3 100       19 if (defined $index) {
230 2         7 my @sgr = splice @numbers, $index;
231 2         19 push @numbers, [ 'SGR', @sgr ];
232             }
233             }
234             }
235 58         250 @numbers;
236             }
237              
238             use constant {
239 3         2749 CSI => "\e[", # Control Sequence Introducer
240             RESET => "\e[m", # SGR Reset
241             EL => "\e[K", # Erase Line
242 3     3   25 };
  3         7  
243              
244             my %csi_terminator = (
245             ICH => '@', # Insert Character
246             CUU => 'A', # Cursor up
247             CUD => 'B', # Cursor Down
248             CUF => 'C', # Cursor Forward
249             CUB => 'D', # Cursor Back
250             CNL => 'E', # Cursor Next Line
251             CPL => 'F', # Cursor Previous line
252             CHA => 'G', # Cursor Horizontal Absolute
253             CUP => 'H', # Cursor Position
254             ED => 'J', # Erase in Display (0 after, 1 before, 2 entire, 3 w/buffer)
255             EL => 'K', # Erase in Line (0 after, 1 before, 2 entire)
256             IL => 'L', # Insert Line
257             DL => 'M', # Delete Line
258             DCH => 'P', # Delete Character
259             SU => 'S', # Scroll Up
260             SD => 'T', # Scroll Down
261             ECH => 'X', # Erase Character
262             VPA => 'd', # Vertical Position Absolute
263             VPR => 'e', # Vertical Position Relative
264             HVP => 'f', # Horizontal Vertical Position
265             SGR => 'm', # Select Graphic Rendition
266             DSR => 'n', # Device Status Report (0 cursor position)
267             SCP => 's', # Save Cursor Position
268             RCP => 'u', # Restore Cursor Position
269             );
270              
271             my %other_sequence = (
272             CSI => "\e[", # Control Sequence Introducer
273             OSC => "\e]", # Operating System Command
274             RIS => "\ec", # Reset to Initial State
275             DECSC => "\e7", # DEC Save Cursor
276             DECRC => "\e8", # DEC Restore Cursor
277             DECEC => "\e[?25h", # DEC Enable Cursor
278             DECDC => "\e[?25l", # DEC Disable Cursor
279             );
280              
281             sub csi_code {
282 65     65 1 117 my $name = shift;
283 65 50       155 if (my $seq = $other_sequence{$name}) {
284 0         0 return $seq;
285             }
286 65 50       143 my $c = $csi_terminator{$name} or do {
287 0         0 warn "$name: Unknown ANSI name.\n";
288 0         0 return '';
289             };
290 65 50 100     266 if ($name eq 'SGR' and @_ == 1 and $_[0] == 0) {
      66        
291 0         0 @_ = ();
292             }
293 65         430 CSI . join(';', @_) . $c;
294             }
295              
296             sub ansi_code {
297 59     59 1 4865 my $spec = shift;
298 59         129 my @numbers = ansi_numbers $spec;
299 58         94 my @code;
300 58         131 while (@numbers) {
301 65         111 my $item = shift @numbers;
302 65 100       144 if (ref($item) eq 'ARRAY') {
303 11         25 push @code, csi_code @$item;
304             } else {
305 54         102 my @sgr = ($item);
306 54   100     186 while (@numbers and not ref $numbers[0]) {
307 135         352 push @sgr, shift @numbers;
308             }
309 54         114 push @code, csi_code 'SGR', @sgr;
310             }
311             }
312 58         443 join '', @code;
313             }
314              
315             sub ansi_pair {
316 9     9 1 699 my $spec = shift;
317 9         14 my $el = 0;
318 9   50     24 my $start = ansi_code $spec // '';
319 9 100       30 my $end = $start eq '' ? '' : do {
320 7 100       42 if ($start =~ /(.*)(\e\[[0;]*K)(.*)/) {
321 2         3 $el = 1;
322 2 50       7 if ($3) {
323 0         0 $1 . EL . RESET;
324             } else {
325 2         6 EL . RESET;
326             }
327             } else {
328 5 50       10 if ($NO_RESET_EL) {
329 0         0 RESET;
330             } else {
331 5         11 RESET . EL;
332             }
333             }
334             };
335 9         49 ($start, $end, $el);
336             }
337              
338             sub ansi_color {
339 34     34 1 2977 cached_ansi_color(state $cache = {}, @_);
340             }
341              
342             sub ansi_color_24 {
343 2     2 1 7 local $RGB24 = 1;
344 2         9 cached_ansi_color(state $cache = {}, @_);
345             }
346              
347             sub cached_ansi_color {
348 36     36 1 59 my $cache = shift;
349 36         57 my @result;
350 36         89 while (@_ >= 2) {
351 36         89 my($spec, $text) = splice @_, 0, 2;
352 36 50       91 for my $color (ref $spec eq 'ARRAY' ? @$spec : $spec) {
353 36         65 $text = apply_color($cache, $color, $text);
354             }
355 36         99 push @result, $text;
356             }
357 36 50       91 croak "Wrong number of parameters." if @_;
358 36         202 join '', @result;
359             }
360              
361 3     3   25 use Scalar::Util qw(blessed);
  3         27  
  3         1784  
362              
363             sub apply_color {
364 36     36 0 81 (my($cache, $color), local($_)) = @_;
365 36 50 33     153 if (ref $color eq 'CODE') {
    50          
    100          
    50          
366 0         0 return $color->($_);
367             }
368             elsif (blessed $color and $color->can('call')) {
369 0         0 return $color->call;
370             }
371             elsif ($NO_COLOR) {
372 18         42 return $_;
373             }
374             elsif ($NO_CUMULATIVE) { # old behavior
375 0   0     0 my($s, $e, $el) = @{ $cache->{$color} //= [ ansi_pair($color) ] };
  0         0  
376 0         0 state $reset = qr{ \e\[[0;]*m (?: \e\[[0;]*[Km] )* }x;
377 0 0       0 if ($el) {
378 0         0 s/(\A|(?<=[\r\n])|$reset)\K(?[^\e\r\n]+|(?
379             } else {
380 0         0 s/(\A|(?<=[\r\n])|$reset)\K(?[^\e\r\n]+)/${s}$+{x}${e}/mg;
381             }
382 0         0 return $_;
383             }
384             else {
385 18   100     25 my($s, $e, $el) = @{ $cache->{$color} //= [ ansi_pair($color) ] };
  18         67  
386 18         35 state $reset = qr{ \e\[[0;]*m (?: \e\[[0;]*[Km] )* }x;
387 18 50       34 if ($el) {
388 0         0 s/(?:\A|(?:[\r\n](?!\z)|$reset++))\K/${s}/g;
389 0         0 s/([\r\n]|(?
390             } else {
391 18         290 s/(?:\A|[\r\n]|$reset++)(?=.)\K/${s}/g;
392 18         163 s/(?
393             }
394 18         69 return $_;
395             }
396             }
397              
398             1;
399              
400             __END__