File Coverage

blib/lib/Term/ANSIColor/Concise.pm
Criterion Covered Total %
statement 178 212 83.9
branch 78 116 67.2
condition 26 35 74.2
subroutine 31 33 93.9
pod 8 17 47.0
total 321 413 77.7


line stmt bran cond sub pod time code
1             # -*- indent-tabs-mode: nil -*-
2              
3             package Term::ANSIColor::Concise;
4              
5             our $VERSION = "3.02";
6              
7 5     5   710003 use v5.14;
  5         20  
8 5     5   27 use warnings;
  5         10  
  5         378  
9 5     5   580 use utf8;
  5         244  
  5         87  
10              
11 5     5   208 use Exporter 'import';
  5         15  
  5         675  
12             our @EXPORT = qw();
13             our @EXPORT_OK = qw(
14             ansi_color ansi_color_24 ansi_code ansi_pair csi_code csi_report
15             cached_ansi_color
16             map_256_to_6 map_to_256
17             );
18             our %EXPORT_TAGS = (all => [ @EXPORT_OK ]);
19              
20 5     5   33 use Carp;
  5         11  
  5         433  
21 5     5   3413 use Data::Dumper;
  5         49022  
  5         575  
22             $Data::Dumper::Sortkeys = 1;
23 5     5   42 use List::Util qw(min max first);
  5         10  
  5         480  
24              
25 5     5   71328 use Term::ANSIColor::Concise::Util;
  5         15  
  5         187  
26              
27 5     5   2691 use aliased;
  5         4593  
  5         31  
28             my $Color = alias 'Term::ANSIColor::Concise::' . ($ENV{TAC_COLOR_PACKAGE} || 'ColorObject');
29 80     80 1 515 sub Color { $Color }
30              
31             our $NO_NO_COLOR //= $ENV{ANSICOLOR_NO_NO_COLOR};
32             our $NO_COLOR //= !$NO_NO_COLOR && defined $ENV{NO_COLOR};
33             our $RGB24 //= $ENV{ANSICOLOR_RGB24} // ($ENV{COLORTERM}//'' eq 'truecolor');
34             our $LINEAR_256 //= $ENV{ANSICOLOR_LINEAR_256};
35             our $LINEAR_GRAY //= $ENV{ANSICOLOR_LINEAR_GRAY};
36             our $NO_RESET_EL //= $ENV{ANSICOLOR_NO_RESET_EL};
37             our $SPLIT_ANSI //= $ENV{ANSICOLOR_SPLIT_ANSI};
38             our $NO_CUMULATIVE //= $ENV{ANSICOLOR_NO_CUMULATIVE};
39              
40             my @nonlinear = do {
41             map { ( $_->[0] ) x $_->[1] } (
42             [ 0, 75 ], # 0 .. 74
43             [ 1, 40 ], # 75 .. 114
44             [ 2, 40 ], # 115 .. 154
45             [ 3, 40 ], # 155 .. 194
46             [ 4, 40 ], # 195 .. 234
47             [ 5, 21 ], # 235 .. 255
48             );
49             };
50              
51             sub map_256_to_6 {
52 5     5   4153 use integer;
  5         78  
  5         24  
53 306     306 0 481 my $i = shift;
54 306 100       482 if ($LINEAR_256) {
55 12         36 5 * $i / 255;
56             } else {
57             # ( $i - 35 ) / 40;
58 294         719 $nonlinear[$i];
59             }
60             }
61              
62             sub map_to_256 {
63 0     0 0 0 my($base, $i) = @_;
64 0 0       0 if ($i == 0) { 0 }
  0 0       0  
    0          
    0          
65 0         0 elsif ($base == 6) { $i * 40 + 55 }
66 0         0 elsif ($base == 12) { $i * 20 + 35 }
67 0         0 elsif ($base == 24) { $i * 10 + 25 }
68 0         0 else { die }
69             }
70              
71             sub ansi256_number {
72 12     12 0 43 my $code = shift;
73 12         31 my($r, $g, $b, $gray);
74 12 100       61 if ($code =~ /^([0-5])([0-5])([0-5])$/) {
    50          
75 8         57 ($r, $g, $b) = ($1, $2, $3);
76             }
77             elsif (my($n) = $code =~ /^L(\d+)/i) {
78 4 50       18 $n > 25 and croak "Color spec error: $code.";
79 4 100 100     21 if ($n == 0 or $n == 25) {
80 2         7 $r = $g = $b = $n / 5;
81             } else {
82 2         5 $gray = $n - 1;
83             }
84             }
85             else {
86 0         0 croak "Color spec error: $code.";
87             }
88 12 100       59 defined $gray ? ($gray + 232) : ($r*36 + $g*6 + $b + 16);
89             }
90              
91             sub rgb24_number {
92 5     5   2457 use integer;
  5         10  
  5         27  
93 104     104 0 233 my($rx, $gx, $bx) = @_;
94 104         166 my($r, $g, $b, $gray);
95 104 100 100     612 if ($rx != 0 and $rx != 255 and $rx == $gx and $rx == $bx) {
      100        
      100        
96 2 50       8 if ($LINEAR_GRAY) {
97             ##
98             ## Divide area into 25 segments, and map to BLACK and 24 GRAYS
99             ##
100 0         0 $gray = $rx * 25 / 255 - 1;
101 0 0       0 if ($gray < 0) {
102 0         0 $r = $g = $b = 0;
103 0         0 $gray = undef;
104             }
105             } else {
106             ## map to 8, 18, 28, ... 238
107 2         13 $gray = min(23, ($rx - 3) / 10);
108             }
109             } else {
110 102         209 ($r, $g, $b) = map { map_256_to_6 $_ } $rx, $gx, $bx;
  306         576  
111             }
112 104 100       620 defined $gray ? ($gray + 232) : ($r*36 + $g*6 + $b + 16);
113             }
114              
115             sub rgb24 {
116 25     25 0 103 my $rgb = shift;
117 25         87 $rgb =~ s/^#//;
118 25         52 my $len = length $rgb;
119 25 50 33     174 croak "$rgb: Invalid RGB value." if $len == 0 || $len % 3;
120 25         57 $len /= 3;
121 25         82 my $max = (2 ** ($len * 4)) - 1;
122 25         771 map { hex($_) * 255 / $max } $rgb =~ /[0-9a-z]{$len}/gi;
  75         284  
123             }
124              
125             sub rgbseq {
126 121     121 0 341 my($mod, @rgb) = @_;
127 121 100       361 if ($mod) {
128 31         104 @rgb = transform($mod, @rgb);
129             }
130 120 100       4622 if ($RGB24) {
131 16         75 return (2, @rgb);
132             } else {
133 104         276 return (5, rgb24_number @rgb);
134             }
135             }
136              
137             my %numbers = (
138             ';' => undef, # ; : NOP
139             N => undef, # N : None (NOP)
140             E => 'EL', # E : Erase Line
141             Z => 0, # Z : Zero (Reset)
142             D => 1, # D : Double Strike (Bold)
143             P => 2, # P : Pale (Dark)
144             I => 3, # I : Italic
145             U => 4, # U : Underline
146             F => 5, # F : Flash (Blink: Slow)
147             Q => 6, # Q : Quick (Blink: Rapid)
148             S => 7, # S : Stand out (Reverse)
149             H => 8, # H : Hide (Concealed)
150             X => 9, # X : Cross out
151             K => 30, k => 90, # K : Kuro (Black)
152             R => 31, r => 91, # R : Red
153             G => 32, g => 92, # G : Green
154             Y => 33, y => 93, # Y : Yellow
155             B => 34, b => 94, # B : Blue
156             M => 35, m => 95, # M : Magenta
157             C => 36, c => 96, # C : Cyan
158             W => 37, w => 97, # W : White
159             );
160              
161 5     5   5556 use Term::ANSIColor::Concise::Transform qw(transform $mod_re);
  5         16  
  5         6866  
162              
163             my $colorspec_re = qr{
164             (?
165             (? /) # /
166             | (? \^) # ^
167             # Fullcolor with modifier
168             | ((?
169             (? [0-9a-f]{6} ## RGB 24bit hex
170             | \#([0-9a-f]{3})+ ) ## RGB generic hex
171             | (?(rgb)? (?&TRIPLET) ) ## RGB 24bit decimal (0-255, 0-255, 0-255)
172             | (? hsl (?&TRIPLET) ) ## HSL (0-360, 0-100, 0-100)
173             | (? lch (?&TRIPLET) ) ## LCHab (0-100, 0~130, 0-360)
174             | (? lab (?&TRIPLET) ) ## Lab (0-100, -128-127, -128-127)
175             | < (? \w+ ) > ##
176             )
177             (? $mod_re* ) ## color modifiers
178             )
179             # Basic 256/16 colors
180             | (? [0-5][0-5][0-5] # 216 (6x6x6) colors
181             | L([01][0-9]|[2][0-5]) ) # 24 gray levels + B/W
182             | (? [KRGYBMCW] ) # 16 colors
183             # Effects and controls
184             | (? ~[DPIUFQSHX] # ~effect
185             | [;NZDPIUFQSHX] ) # effect
186             | (? \{ (?[A-Z]+) # other CSI
187             (?

\( )? # optional (

188             (?[\d,;]*) # 0;1;2
189             (?(

) \) ) # closing )

190             \}
191             | (?[E]) ) # abbreviation
192             )
193             (?(DEFINE)
194             (? -? \d+ (\.\d+)? )
195             (? \( (?&DIGIT), (?&DIGIT), (?&DIGIT) \) )
196             )
197             }xi;
198              
199             sub ansi_numbers {
200 149   50 149 0 474 local $_ = shift // '';
201 149         269 my @numbers;
202 149         763 my $toggle = ToggleValue->new(value => 10);
203 149         268 my %F;
204 149     121   942 my $rgb_numbers = sub { 38 + $toggle->value, rgbseq($F{mod}, @_) };
  121         504  
205              
206 149         6079 while (m{\G (?: $colorspec_re | (? .+ ) ) }xig) {
207 221         4463 %F = %+;
208 221 100       4361 if ($+{toggle}) {
    50          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    0          
209 17         61 $toggle->toggle;
210             }
211             elsif ($+{reset}) {
212 0         0 $toggle->reset;
213             }
214             elsif ($+{hex}) {
215 25         105 my @rgb = rgb24($+{hex});
216 25         103 push @numbers, $rgb_numbers->(@rgb);
217             }
218             elsif (my $dec = $+{dec}) {
219 6         51 my @rgb = $dec =~ /\d+/g;
220 6 50       18 croak "Unexpected value: $dec." if grep { $_ > 255 } @rgb;
  18         63  
221 6         23 push @numbers, $rgb_numbers->(@rgb);
222             }
223             elsif (my $hsl = $+{hsl}) {
224 21 50       58 Color->can('hsl') or die "HSL format is not supported.\n";
225 21         180 my @hsl = $hsl =~ /\d+/g;
226 21         44 my @rgb = Color->hsl(@hsl)->rgb;
227 21         2418 push @numbers, $rgb_numbers->(@rgb);
228             }
229             elsif (my $lch = $+{lch}) {
230 7 50       21 Color->can('lch') or die "LCHab format is not supported.\n";
231 7         88 my($L, $C, $H) = $lch =~ /-?\d+/g;
232 7         18 my @rgb = Color->lch($L, $C, $H)->rgb;
233 7         1271 push @numbers, $rgb_numbers->(@rgb);
234             }
235             elsif (my $lab = $+{lab}) {
236 12 50       28 Color->can('lab') or die "Lab format is not supported.\n";
237 12         115 my($L, $a, $b) = $lab =~ /-?\d+/g;
238 12         26 my @rgb = Color->lab($L, $a, $b)->rgb;
239 12         1634 push @numbers, $rgb_numbers->(@rgb);
240             }
241             elsif ($+{name}) {
242 51         390 require Graphics::ColorNames;
243 51         118 state $colornames = Graphics::ColorNames->new;
244 51 100       1142 if (my @rgb = $colornames->rgb($+{name})) {
245 50         2554 push @numbers, $rgb_numbers->(@rgb);
246             } else {
247 1         285 croak "Unknown color name: $+{name}.";
248             }
249             }
250             elsif ($+{c256}) {
251 12         47 push @numbers, 38 + $toggle->value, 5, ansi256_number $+{c256};
252             }
253             elsif ($+{c16}) {
254 24         198 push @numbers, $numbers{$+{c16}} + $toggle->value;
255             }
256             elsif ($+{efct}) {
257 37         161 my $efct = uc $+{efct};
258 37 100       139 my $offset = $efct =~ s/^~// ? 20 : 0;
259 37 100       126 if (defined (my $n = $numbers{$efct})) {
260 35         94 push @numbers, $n + $offset;
261             }
262             }
263             elsif ($+{csi}) {
264 9         20 push @numbers, do {
265 9 100       44 if ($+{csi_abbr}) {
266 3         24 [ $numbers{uc $+{csi_abbr}} ];
267             } else {
268 6         91 [ uc $+{csi_name}, $+{csi_param} =~ /\d+/g ];
269             }
270             };
271             }
272             elsif (my $err = $+{err}) {
273 0         0 croak "Color spec error: \"$err\" in \"$_\"."
274             }
275             else {
276 0         0 croak "$_: Something strange.";
277             }
278             } continue {
279 219 100       3175 if ($SPLIT_ANSI) {
280 3     4   23 my $index = first { not ref $numbers[$_] } keys @numbers;
  4         11  
281 3 100       26 if (defined $index) {
282 2         7 my @sgr = splice @numbers, $index;
283 2         30 push @numbers, [ 'SGR', @sgr ];
284             }
285             }
286             }
287 147         1393 @numbers;
288             }
289              
290             use constant {
291 5         5644 CSI => "\e[", # Control Sequence Introducer
292             RESET => "\e[m", # SGR Reset
293             EL => "\e[K", # Erase Line
294 5     5   60 };
  5         9  
295              
296             my %csi_terminator = (
297             ICH => '@', # Insert Character
298             CUU => 'A', # Cursor up
299             CUD => 'B', # Cursor Down
300             CUF => 'C', # Cursor Forward
301             CUB => 'D', # Cursor Back
302             CNL => 'E', # Cursor Next Line
303             CPL => 'F', # Cursor Previous line
304             CHA => 'G', # Cursor Horizontal Absolute
305             CUP => 'H', # Cursor Position
306             ED => 'J', # Erase in Display (0 after, 1 before, 2 entire, 3 w/buffer)
307             EL => 'K', # Erase in Line (0 after, 1 before, 2 entire)
308             IL => 'L', # Insert Line
309             DL => 'M', # Delete Line
310             DCH => 'P', # Delete Character
311             SU => 'S', # Scroll Up
312             SD => 'T', # Scroll Down
313             ECH => 'X', # Erase Character
314             VPA => 'd', # Vertical Position Absolute
315             VPR => 'e', # Vertical Position Relative
316             HVP => 'f', # Horizontal Vertical Position
317             SGR => 'm', # Select Graphic Rendition
318             DSR => 'n', # Device Status Report (6 cursor position)
319             SCP => 's', # Save Cursor Position
320             RCP => 'u', # Restore Cursor Position
321              
322             # Non-standard
323             CPR => 'R', # Cursor Position Report – VT100 to Host
324             STBM => 'r', # Set Top and Bottom Margins
325             SLRM => 's', # Set Left Right Margins
326             );
327              
328             my %other_sequence = (
329             CSI => "\e[", # Control Sequence Introducer
330             OSC => "\e]", # Operating System Command
331             RIS => "\ec", # Reset to Initial State
332             DECSC => "\e7", # DEC Save Cursor
333             DECRC => "\e8", # DEC Restore Cursor
334             DECEC => "\e[?25h", # DEC Enable Cursor
335             DECDC => "\e[?25l", # DEC Disable Cursor
336             DECELRM => "\e[?69h", # DEC Enable Left Right Margin Mode
337             DECDLRM => "\e[?69l", # DEC Disable Left Right Margin Mode
338             );
339              
340             sub csi_code {
341 154     154 1 289 my $name = shift;
342 154 50       522 if (my $seq = $other_sequence{$name}) {
343 0         0 return $seq;
344             }
345 154 50       455 my $c = $csi_terminator{$name} or die "$name: Unknown ANSI name.\n";
346 154 50 100     765 if ($name eq 'SGR' and @_ == 1 and $_[0] == 0) {
      66        
347 0         0 @_ = ();
348             }
349 154         1170 CSI . join(';', @_) . $c;
350             }
351              
352             sub csi_report {
353 0     0 1 0 my($name, $n, $report) = @_;
354 0 0       0 my $c = $csi_terminator{$name} or die "$name: Unknown ANSI name.\n";
355 0         0 my $format = quotemeta(CSI) . join(';', ('(\d+)') x $n) . $c;
356 0         0 $report =~ /$format/;
357             }
358              
359             sub ansi_code {
360 149     149 1 209916 my $spec = shift;
361 149         422 my @numbers = ansi_numbers $spec;
362 147         273 my @code;
363 147         379 while (@numbers) {
364 154         282 my $item = shift @numbers;
365 154 100       408 if (ref($item) eq 'ARRAY') {
366 11         36 push @code, csi_code @$item;
367             } else {
368 143         257 my @sgr = ($item);
369 143   100     581 while (@numbers and not ref $numbers[0]) {
370 342         1093 push @sgr, shift @numbers;
371             }
372 143         352 push @code, csi_code 'SGR', @sgr;
373             }
374             }
375 147         934 join '', @code;
376             }
377              
378             sub ansi_pair {
379 13     13 1 894 my $spec = shift;
380 13         25 my $el = 0;
381 13   50     57 my $start = ansi_code $spec // '';
382 13 100       47 my $end = $start eq '' ? '' : do {
383 11 100       81 if ($start =~ /(.*)(\e\[[0;]*K)(.*)/) {
384 2         5 $el = 1;
385 2 50       9 if ($3) {
386 0         0 $1 . EL . RESET;
387             } else {
388 2         6 EL . RESET;
389             }
390             } else {
391 9 50       23 if ($NO_RESET_EL) {
392 0         0 RESET;
393             } else {
394 9         21 RESET . EL;
395             }
396             }
397             };
398 13         128 ($start, $end, $el);
399             }
400              
401             sub ansi_color {
402 39     39 1 787721 cached_ansi_color(state $cache = {}, @_);
403             }
404              
405             sub ansi_color_24 {
406 2     2 1 7 local $RGB24 = 1;
407 2         11 cached_ansi_color(state $cache = {}, @_);
408             }
409              
410             sub cached_ansi_color {
411 41     41 1 90 my $cache = shift;
412 41         72 my @result;
413 41         143 while (@_ >= 2) {
414 41         153 my($spec, $text) = splice @_, 0, 2;
415 41 50       169 for my $color (ref $spec eq 'ARRAY' ? @$spec : $spec) {
416 41         106 $text = apply_color($cache, $color, $text);
417             }
418 41         183 push @result, $text;
419             }
420 41 50       102 croak "Wrong number of parameters." if @_;
421 41 50       286 wantarray ? @result : join('', @result);
422             }
423              
424             sub IsEOL {
425 5     5 0 1583 <<"END";
426             0000\t0000
427             000A\t000D
428             2028\t2029
429             END
430             }
431              
432 5     5   76 use Scalar::Util qw(blessed);
  5         8  
  5         2508  
433              
434             sub apply_color {
435 41     41 0 119 (my($cache, $color), local($_)) = @_;
436 41 50 33     232 if (ref $color eq 'CODE') {
    50          
    100          
    50          
437 0         0 return $color->($_);
438             }
439             elsif (blessed $color and $color->can('call')) {
440 0         0 return $color->call;
441             }
442             elsif ($NO_COLOR) {
443 18         58 return $_;
444             }
445             elsif ($NO_CUMULATIVE) { # old behavior
446 0   0     0 my($s, $e, $el) = @{ $cache->{$color} //= [ ansi_pair($color) ] };
  0         0  
447 0         0 state $reset = qr{ \e\[[0;]*m (?: \e\[[0;]*[Km] )* }x;
448 0 0       0 if ($el) {
449 0         0 s/(\A|(?<=\p{IsEOL})|$reset)\K(?[^\e\p{IsEOL}]+|(?
450             } else {
451 0         0 s/(\A|(?<=\p{IsEOL})|$reset)\K(?[^\e\p{IsEOL}]+)/${s}$+{x}${e}/g;
452             }
453 0         0 return $_;
454             }
455             else {
456 23   100     42 my($s, $e, $el) = @{ $cache->{$color} //= [ ansi_pair($color) ] };
  23         137  
457 23         84 state $reset = qr{ \e\[[0;]*m (?: \e\[[0;]*[Km] )* }x;
458 23 50       104 if ($el) {
459 0         0 s/(?:\A|(?:\p{IsEOL}(?!\z)|$reset++))\K/${s}/g;
460 0         0 s/(\p{IsEOL}|(?
461             } else {
462 23         730 s/(?:\A|\p{IsEOL}|$reset++)(?=.)\K/${s}/g;
463 23         347 s/(?
464             }
465 23         123 return $_;
466             }
467             }
468              
469             1;
470              
471             __END__