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.04";
4              
5 3     3   142902 use v5.14;
  3         30  
6 3     3   18 use warnings;
  3         7  
  3         75  
7 3     3   632 use utf8;
  3         20  
  3         24  
8              
9 3     3   100 use Exporter 'import';
  3         6  
  3         279  
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   21 use Carp;
  3         6  
  3         218  
19 3     3   1938 use Data::Dumper;
  3         21092  
  3         229  
20             $Data::Dumper::Sortkeys = 1;
21              
22 3     3   1229 use Term::ANSIColor::Concise::Util;
  3         7  
  3         106  
23 3     3   18 use List::Util qw(min max first);
  3         7  
  3         851  
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   1595 use integer;
  3         48  
  3         15  
46 69     69 0 106 my $i = shift;
47 69 100       104 if ($LINEAR_256) {
48 12         28 5 * $i / 255;
49             } else {
50             # ( $i - 35 ) / 40;
51 57         131 $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 36 my $code = shift;
66 12         22 my($r, $g, $b, $gray);
67 12 100       48 if ($code =~ /^([0-5])([0-5])([0-5])$/) {
    50          
68 8         29 ($r, $g, $b) = ($1, $2, $3);
69             }
70             elsif (my($n) = $code =~ /^L(\d+)/i) {
71 4 50       12 $n > 25 and croak "Color spec error: $code.";
72 4 100 100     17 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       43 defined $gray ? ($gray + 232) : ($r*36 + $g*6 + $b + 16);
82             }
83              
84             sub rgb24_number {
85 3     3   1086 use integer;
  3         15  
  3         23  
86 24     24 0 55 my($rx, $gx, $bx) = @_;
87 24         38 my($r, $g, $b, $gray);
88 24 100 100     145 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         6 $gray = min(23, ($rx - 3) / 10);
101             }
102             } else {
103 23         45 ($r, $g, $b) = map { map_256_to_6 $_ } $rx, $gx, $bx;
  69         116  
104             }
105 24 100       126 defined $gray ? ($gray + 232) : ($r*36 + $g*6 + $b + 16);
106             }
107              
108             sub rgbhex {
109 31     31 0 138 my $rgb = shift =~ s/^#//r;
110 31         66 my $len = length $rgb;
111 31 50 33     126 croak "$rgb: Invalid RGB value." if $len == 0 || $len % 3;
112 31         63 $len /= 3;
113 31         80 my $max = (2 ** ($len * 4)) - 1;
114 31 50       472 my @rgb24 = map { hex($_) * 255 / $max } $rgb =~ /[0-9a-z]{$len}/gi or die;
  93         268  
115 31 100       87 if ($RGB24) {
116 7         25 return (2, @rgb24);
117             } else {
118 24         53 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 145 local $_ = shift // '';
167 53         85 my @numbers;
168 53         204 my $toggle = ToggleValue->new(value => 10);
169              
170 53         906 while (m{\G (?: $colorspec_re | (? .+ ) ) }xig) {
171 3 100   3   3529 if ($+{toggle}) {
  3 50       1126  
  3 100       2039  
  114 100       1204  
    100          
    100          
    100          
    100          
    50          
    0          
172 17         44 $toggle->toggle;
173             }
174             elsif ($+{reset}) {
175 0         0 $toggle->reset;
176             }
177             elsif ($+{hex}) {
178 23         78 push @numbers, 38 + $toggle->value, rgbhex($+{hex});
179             }
180             elsif (my $rgb = $+{rgb}) {
181 5         40 my @rgb = $rgb =~ /(\d+)/g;
182 5 50       14 croak "Unexpected value: $rgb." if grep { $_ > 255 } @rgb;
  15         47  
183 5         25 my $hex = sprintf "%02X%02X%02X", @rgb;
184 5         19 push @numbers, 38 + $toggle->value, rgbhex($hex);
185             }
186             elsif ($+{c256}) {
187 12         34 push @numbers, 38 + $toggle->value, 5, ansi256_number $+{c256};
188             }
189             elsif ($+{c16}) {
190 16         77 push @numbers, $numbers{$+{c16}} + $toggle->value;
191             }
192             elsif ($+{efct}) {
193 28         96 my $efct = uc $+{efct};
194 28 100       90 my $offset = $efct =~ s/^~// ? 20 : 0;
195 28 100       70 if (defined (my $n = $numbers{$efct})) {
196 26         53 push @numbers, $n + $offset;
197             }
198             }
199             elsif ($+{csi}) {
200 9         17 push @numbers, do {
201 9 100       32 if ($+{csi_abbr}) {
202 3         17 [ $numbers{uc $+{csi_abbr}} ];
203             } else {
204 6         53 [ uc $+{csi_name}, $+{csi_param} =~ /\d+/g ];
205             }
206             };
207             }
208             elsif ($+{name}) {
209 4         9 state $colornames = do {
210 1         598 require Graphics::ColorNames;
211 1         6712 Graphics::ColorNames->new;
212             };
213 4 100       2331 if (my $rgb = $colornames->hex($+{name})) {
214 3         129 push @numbers, 38 + $toggle->value, rgbhex($rgb);
215             } else {
216 1         221 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       808 if ($SPLIT_ANSI) {
227 3     4   23 my $index = first { not ref $numbers[$_] } 0 .. $#numbers;
  4         11  
228 3 100       19 if (defined $index) {
229 2         7 my @sgr = splice @numbers, $index;
230 2         19 push @numbers, [ 'SGR', @sgr ];
231             }
232             }
233             }
234 52         223 @numbers;
235             }
236              
237             use constant {
238 3         2581 CSI => "\e[", # Control Sequence Introducer
239             RESET => "\e[m", # SGR Reset
240             EL => "\e[K", # Erase Line
241 3     3   35 };
  3         10  
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             VPA => 'd', # Vertical Position Absolute
262             VPR => 'e', # Vertical Position Relative
263             HVP => 'f', # Horizontal Vertical Position
264             SGR => 'm', # Select Graphic Rendition
265             DSR => 'n', # Device Status Report (0 cursor position)
266             SCP => 's', # Save Cursor Position
267             RCP => 'u', # Restore Cursor Position
268             );
269              
270             my %other_sequence = (
271             CSI => "\e[", # Control Sequence Introducer
272             OSC => "\e]", # Operating System Command
273             RIS => "\ec", # Reset to Initial State
274             DECSC => "\e7", # DEC Save Cursor
275             DECRC => "\e8", # DEC Restore Cursor
276             DECEC => "\e[?25h", # DEC Enable Cursor
277             DECDC => "\e[?25l", # DEC Disable Cursor
278             );
279              
280             sub csi_code {
281 59     59 1 96 my $name = shift;
282 59 50       136 if (my $seq = $other_sequence{$name}) {
283 0         0 return $seq;
284             }
285 59 50       145 my $c = $csi_terminator{$name} or do {
286 0         0 warn "$name: Unknown ANSI name.\n";
287 0         0 return '';
288             };
289 59 50 100     215 if ($name eq 'SGR' and @_ == 1 and $_[0] == 0) {
      66        
290 0         0 @_ = ();
291             }
292 59         380 CSI . join(';', @_) . $c;
293             }
294              
295             sub ansi_code {
296 53     53 1 4610 my $spec = shift;
297 53         113 my @numbers = ansi_numbers $spec;
298 52         84 my @code;
299 52         113 while (@numbers) {
300 59         109 my $item = shift @numbers;
301 59 100       144 if (ref($item) eq 'ARRAY') {
302 11         26 push @code, csi_code @$item;
303             } else {
304 48         81 my @sgr = ($item);
305 48   100     164 while (@numbers and not ref $numbers[0]) {
306 135         417 push @sgr, shift @numbers;
307             }
308 48         108 push @code, csi_code 'SGR', @sgr;
309             }
310             }
311 52         265 join '', @code;
312             }
313              
314             sub ansi_pair {
315 8     8 1 617 my $spec = shift;
316 8         15 my $el = 0;
317 8   50     22 my $start = ansi_code $spec // '';
318 8 100       18 my $end = $start eq '' ? '' : do {
319 6 100       31 if ($start =~ /(.*)(\e\[[0;]*K)(.*)/) {
320 2         5 $el = 1;
321 2 50       6 if ($3) {
322 0         0 $1 . EL . RESET;
323             } else {
324 2         4 EL . RESET;
325             }
326             } else {
327 4 50       9 if ($NO_RESET_EL) {
328 0         0 RESET;
329             } else {
330 4         8 RESET . EL;
331             }
332             }
333             };
334 8         43 ($start, $end, $el);
335             }
336              
337             sub ansi_color {
338 37     37 1 2906 cached_ansi_color(state $cache = {}, @_);
339             }
340              
341             sub ansi_color_24 {
342 2     2 1 8 local $RGB24 = 1;
343 2         9 cached_ansi_color(state $cache = {}, @_);
344             }
345              
346             sub cached_ansi_color {
347 39     39 1 71 my $cache = shift;
348 39         60 my @result;
349 39         91 while (@_ >= 2) {
350 39         99 my($spec, $text) = splice @_, 0, 2;
351 39 50       95 for my $color (ref $spec eq 'ARRAY' ? @$spec : $spec) {
352 39         74 $text = apply_color($cache, $color, $text);
353             }
354 39         108 push @result, $text;
355             }
356 39 50       70 croak "Wrong number of parameters." if @_;
357 39         221 join '', @result;
358             }
359              
360 3     3   25 use Scalar::Util qw(blessed);
  3         6  
  3         1127  
361              
362             sub apply_color {
363 39     39 0 74 my($cache, $color, $text) = @_;
364 39 50 33     149 if (ref $color eq 'CODE') {
    50          
    100          
365 0         0 return $color->($text);
366             }
367             elsif (blessed $color and $color->can('call')) {
368 0         0 return $color->call for $text;
369             }
370             elsif ($NO_COLOR) {
371 18         45 return $text;
372             }
373             else {
374 21   100     31 my($s, $e, $el) = @{ $cache->{$color} //= [ ansi_pair($color) ] };
  21         67  
375 21         36 state $reset = qr{ \e\[[0;]*m (?: \e\[[0;]*[Km] )* }x;
376 21 50       36 if ($el) {
377 0         0 $text =~ s/(\A|(?<=[\r\n])|$reset)\K(?[^\e\r\n]+|(?
378             } else {
379 21         308 $text =~ s/(\A|(?<=[\r\n])|$reset)\K(?[^\e\r\n]+)/${s}$+{x}${e}/mg;
380             }
381 21         87 return $text;
382             }
383             }
384              
385             1;
386              
387             __END__