File Coverage

blib/lib/Parse/ANSIColor/Tiny.pm
Criterion Covered Total %
statement 114 115 99.1
branch 45 48 93.7
condition 7 7 100.0
subroutine 19 19 100.0
pod 13 13 100.0
total 198 202 98.0


line stmt bran cond sub pod time code
1             # vim: set ts=2 sts=2 sw=2 expandtab smarttab:
2             #
3             # This file is part of Parse-ANSIColor-Tiny
4             #
5             # This software is copyright (c) 2011 by Randy Stauner.
6             #
7             # This is free software; you can redistribute it and/or modify it under
8             # the same terms as the Perl 5 programming language system itself.
9             #
10 13     13   354983 use strict;
  13         70  
  13         427  
11 13     13   71 use warnings;
  13         27  
  13         24180  
12              
13             package Parse::ANSIColor::Tiny;
14             # git description: v0.601-1-g8166474
15              
16             our $AUTHORITY = 'cpan:RWSTAUNER';
17             # ABSTRACT: Determine attributes of ANSI-Colored string
18             $Parse::ANSIColor::Tiny::VERSION = '0.700';
19             our @COLORS = qw( black red green yellow blue magenta cyan white );
20             our %FOREGROUND = (
21             (map { ( $COLORS[$_] => 30 + $_ ) } 0 .. $#COLORS),
22             (map { ( 'bright_' . $COLORS[$_] => 90 + $_ ) } 0 .. $#COLORS),
23             );
24             our %BACKGROUND = (
25             (map { ( 'on_' . $COLORS[$_] => 40 + $_ ) } 0 .. $#COLORS),
26             (map { ('on_bright_' . $COLORS[$_] => 100 + $_ ) } 0 .. $#COLORS),
27             );
28              
29             # Generating the 256-color codes involves a lot of codes and offsets that are
30             # not helped by turning them into constants.
31             ## no critic (ValuesAndExpressions::ProhibitMagicNumbers)
32              
33             our @COLORS256;
34              
35             # The first 16 256-color codes are duplicates of the 16 ANSI colors,
36             # included for completeness.
37             for my $code (0 .. 15) {
38             my $name = "ansi$code";
39             $FOREGROUND{$name} = "38;5;$code";
40             $BACKGROUND{"on_$name"} = "48;5;$code";
41             push @COLORS256, $name;
42             }
43              
44             # 256-color RGB colors. Red, green, and blue can each be values 0 through 5,
45             # and the resulting 216 colors start with color 16.
46             for my $r (0 .. 5) {
47             for my $g (0 .. 5) {
48             for my $b (0 .. 5) {
49             my $code = 16 + (6 * 6 * $r) + (6 * $g) + $b;
50             my $name = "rgb$r$g$b";
51             $FOREGROUND{$name} = "38;5;$code";
52             $BACKGROUND{"on_$name"} = "48;5;$code";
53             push @COLORS256, $name;
54             }
55             }
56             }
57              
58             # The last 256-color codes are 24 shades of grey.
59             for my $n (0 .. 23) {
60             my $code = $n + 232;
61             my $name = "grey$n";
62             $FOREGROUND{$name} = "38;5;$code";
63             $BACKGROUND{"on_$name"} = "48;5;$code";
64             push @COLORS256, $name;
65             }
66              
67             our %ATTRIBUTES = (
68             clear => 0,
69             reset => 0,
70             bold => 1,
71             dark => 2,
72             faint => 2,
73             underline => 4,
74             underscore => 4,
75             blink => 5,
76             reverse => 7,
77             concealed => 8,
78             reverse_off => 27,
79             reset_foreground => 39,
80             reset_background => 49,
81             %FOREGROUND,
82             %BACKGROUND,
83             );
84              
85             # copied from Term::ANSIColor
86             our %ATTRIBUTES_R;
87             # Reverse lookup. Alphabetically first name for a sequence is preferred.
88             for (reverse sort keys %ATTRIBUTES) {
89             $ATTRIBUTES_R{$ATTRIBUTES{$_}} = $_;
90             }
91              
92              
93             sub new {
94 25     25 1 38587 my $class = shift;
95             my $self = {
96             remove_escapes => 1,
97 25 50       129 @_ == 1 ? %{ $_[0] } : @_,
  0         0  
98             };
99              
100             $self->{process} = 1
101 25 100       96 if $self->{auto_reverse};
102              
103             # fix incorrectly specified attributes
104 25   100     226 ($self->{background} ||= 'black') =~ s/^(on_)*/on_/;
105 25   100     158 ($self->{foreground} ||= 'white') =~ s/^(on_)*//;
106              
107 25         101 bless $self, $class;
108             }
109              
110              
111             sub colors {
112 1     1 1 1166 return (@COLORS, @COLORS256);
113             }
114             sub foreground_colors {
115             return (
116             @COLORS,
117 1     1 1 4 (map { "bright_$_" } @COLORS),
  8         66  
118             @COLORS256,
119             );
120             }
121             sub background_colors {
122             return (
123 8         19 (map { "on_$_" } @COLORS),
124 8         18 (map { "on_bright_$_" } @COLORS),
125 1     1 1 22682 (map { "on_$_" } @COLORS256),
  256         433  
126             );
127             }
128              
129              
130             sub __separate_and_normalize {
131 1296     1296   2169 my ($codes) = @_;
132              
133             # Treat empty as "clear".
134 1296 100 100     4015 defined($codes) && length($codes)
135             or return 0;
136              
137             # Replace empty (clear) with zero to simplify parsing and return values.
138 1287         1993 $codes =~ s/^;/0;/;
139 1287         1857 $codes =~ s/;$/;0/;
140             # Insert a zero between two semicolons (use look-ahead to get /g to find all).
141 1287         2285 $codes =~ s/;(?=;)/;0/g;
142              
143             # Remove any leading zeros from (sections of) codes.
144 1287         2665 $codes =~ s/\b0+(?=\d)//g;
145              
146             # Return all matches (of extended sequences or digits).
147 1287         5184 return $codes =~ m{ ( [34]8;5;\d+ | \d+) }xg;
148             }
149              
150             sub identify {
151 1289     1289 1 3036 my ($self, @codes) = @_;
152 1289         1754 local $_;
153             return
154 1404         4289 grep { defined }
155 1404         3387 map { $ATTRIBUTES_R{ $_ } }
156 1289         2033 map { __separate_and_normalize($_) }
  1296         2012  
157             @codes;
158             }
159              
160              
161             sub normalize {
162 1268     1268 1 2459 my $self = shift;
163 1268         1699 my @norm;
164 1268         2156 foreach my $attr ( @_ ){
165 2124 100       4913 if( $attr eq 'clear' ){
    100          
    100          
    100          
166 584         1127 @norm = ();
167             }
168             elsif( $attr eq 'reverse_off' ){
169             # reverse_off cancels reverse
170 8         19 @norm = grep { $_ ne 'reverse' } @norm;
  21         48  
171             }
172             elsif( $attr eq 'reset_foreground' ){
173 5         10 @norm = grep { !exists $FOREGROUND{$_} } @norm;
  6         19  
174             }
175             elsif( $attr eq 'reset_background' ){
176 2         6 @norm = grep { !exists $BACKGROUND{$_} } @norm;
  3         9  
177             }
178             else {
179             # remove previous (duplicate) occurrences of this attribute
180 1525         2267 @norm = grep { $_ ne $attr } @norm;
  375         861  
181             # new fg color overwrites previous fg
182 1525 100       3158 @norm = grep { !exists $FOREGROUND{$_} } @norm if exists $FOREGROUND{$attr};
  120         304  
183             # new bg color overwrites previous bg
184 1525 100       2818 @norm = grep { !exists $BACKGROUND{$_} } @norm if exists $BACKGROUND{$attr};
  66         146  
185 1525         2944 push @norm, $attr;
186             }
187             }
188 1268         2916 return @norm;
189             }
190              
191              
192             sub parse {
193 33     33 1 17459 my ($self, $orig) = @_;
194              
195 33         63 my $last_pos = 0;
196 33         73 my $last_attr = [];
197 33         59 my $processed = [];
198 33         57 my $parsed = [];
199              
200             # Strip escape sequences that we aren't going to use
201             $orig = $self->remove_escape_sequences($orig)
202 33 100       156 if $self->{remove_escapes};
203              
204 33         192 while( $orig =~ m/(\e\[([0-9;]*)m)/mg ){
205 1250         2565 my $seq = $1;
206 1250         1798 my $attrs = $2;
207              
208 1250         1747 my $cur_pos = pos($orig);
209              
210 1250         1792 my $len = ($cur_pos - length($seq)) - $last_pos;
211 1250 100       3722 push @$parsed, [
212             $processed,
213             substr($orig, $last_pos, $len)
214             ]
215             # don't bother with empty strings
216             if $len;
217              
218 1250         1841 $last_pos = $cur_pos;
219 1250         2386 $last_attr = [$self->normalize(@$last_attr, $self->identify($attrs))];
220 1250 100       5620 $processed = $self->{process} ? [$self->process(@$last_attr)] : $last_attr;
221             }
222              
223 33 100       115 push @$parsed, [
224             $processed,
225             substr($orig, $last_pos)
226             ]
227             # if there's any string left
228             if $last_pos < length($orig);
229              
230 33         391 return $parsed;
231             }
232              
233              
234             sub process {
235 26     26 1 1032 my ($self, @attr) = @_;
236 26 100       77 @attr = $self->process_reverse(@attr) if $self->{auto_reverse};
237 26         155 return @attr;
238             }
239              
240              
241             sub process_reverse {
242 37     37 1 2667 my $self = shift;
243 37         61 my ($rev, $fg, $bg, @attr);
244 37         53 my $i = 0;
245 37         70 foreach my $attr ( @_ ){
246 95 100       254 if( $attr eq 'reverse' ){
    100          
    100          
247 24         40 $rev = 1;
248 24         46 next;
249             }
250             elsif( $FOREGROUND{ $attr } ){
251 28         45 $fg = $i;
252             }
253             elsif( $BACKGROUND{ $attr } ){
254 10         16 $bg = $i;
255             }
256 71         112 push @attr, $attr;
257 71         111 $i++;
258             }
259             # maintain order for consistency with other methods
260 37 100       75 if( $rev ){
261             # if either color is missing then the default colors should be reversed
262             {
263 24 100       38 $attr[ $fg = $i++ ] = $self->{foreground} if !defined $fg;
  24         56  
264 24 100       86 $attr[ $bg = $i++ ] = $self->{background} if !defined $bg;
265             }
266 24 50       87 $attr[ $fg ] = 'on_' . $attr[ $fg ] if defined $fg;
267 24 50       66 $attr[ $bg ] = substr( $attr[ $bg ], 3 ) if defined $bg;
268             }
269 37         197 return @attr;
270             }
271              
272              
273             sub remove_escape_sequences {
274 32     32 1 73 my ($self, $string) = @_;
275              
276             # This is in no way comprehensive or accurate...
277             # it just seems like most of the sequences match this.
278             # We could certainly expand this if the need arises.
279 32         448 $string =~ s{
280             \e\[
281             [0-9;]*
282             [a-ln-zA-Z]
283             }{}gx;
284              
285 32         77 return $string;
286             }
287              
288              
289             our @EXPORT_OK;
290             BEGIN {
291 13     13   77 my @funcs = qw(identify normalize parse);
292 13         33 my $suffix = '_ansicolor';
293 13         29 local $_;
294             eval join '', ## no critic (StringyEval)
295 13     1 1 28 map { "sub ${_}$suffix { __PACKAGE__->new->$_(\@_) }" }
  39     1 1 1637  
  1     1 1 123  
  1         8  
  1         6  
296             @funcs;
297 13         62 @EXPORT_OK = map { $_ . $suffix } @funcs;
  39         733  
298             }
299              
300             sub import {
301 5     5   49 my $class = shift;
302 5 100       3288 return unless @_;
303              
304 2         5 my $caller = caller;
305 13     13   93 no strict 'refs'; ## no critic (NoStrict)
  13         28  
  13         2338  
306              
307 2         5 foreach my $arg ( @_ ){
308             die "'$arg' is not exported by $class"
309 4 100       9 unless grep { $arg eq $_ } @EXPORT_OK;
  12         45  
310 3         4 *{"${caller}::$arg"} = *{"${class}::$arg"}{CODE};
  3         1460  
  3         10  
311             }
312             }
313              
314             # TODO: option for blotting out 'concealed'? s/\S/ /g
315              
316             1;
317              
318             # NOTE: this synopsis is tested (eval'ed) in t/synopsis.t
319              
320             __END__