File Coverage

blib/lib/String/Tagged/IRC.pm
Criterion Covered Total %
statement 89 100 89.0
branch 61 80 76.2
condition 21 34 61.7
subroutine 11 13 84.6
pod 2 4 50.0
total 184 231 79.6


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2014-2016 -- leonerd@leonerd.org.uk
5              
6             package String::Tagged::IRC;
7              
8 4     4   39904 use strict;
  4         7  
  4         100  
9 4     4   13 use warnings;
  4         6  
  4         83  
10 4     4   66 use 5.010; # //
  4         10  
11 4     4   17 use base qw( String::Tagged );
  4         3  
  4         2021  
12             String::Tagged->VERSION( '0.11' ); # ->clone
13              
14             our $VERSION = '0.03';
15              
16 4     4   22158 use Convert::Color::mIRC;
  4         64035  
  4         98  
17 4     4   21 use Convert::Color::RGB8;
  4         6  
  4         6008  
18              
19             =head1 NAME
20              
21             C - parse and format IRC messages using C
22              
23             =head1 TAGS
24              
25             This module provides the following tags, conforming to the
26             L API specification.
27              
28             =head2 bold, under, italic, reverse
29              
30             Boolean values indicating bold, underline, italics, or reverse-video.
31              
32             =head2 fg, bg
33              
34             L objects encoding the color. These will likely be instances
35             of L, unless a full RGB triplet colour code has been
36             provided; in which case it will be an instance of L.
37              
38             =cut
39              
40             # IRC [well, technically mIRC but other clients have adopted it] uses Ctrl
41             # characters to toggle formatting
42             # ^B = bold
43             # ^U = underline
44             # ^_ = underline
45             # ^R = reverse or italic - we'll use italic
46             # ^V = reverse
47             # ^] = italics
48             # ^O = reset
49             # ^C = colour; followed by a code
50             # ^C = reset colours
51             # ^Cff = foreground
52             # ^Cff,bb = background
53             #
54             # irssi uses the following
55             # ^D$$ = foreground/background, in chr('0'+$colour),
56             # ^Db = underline
57             # ^Dc = bold
58             # ^Dd = reverse or italic - we'll use italic
59             # ^Dg = reset colours
60             #
61             # As a side effect we'll also strip all the other Ctrl chars
62              
63             # We'll also look for "poor-man's" highlighting
64             # *bold*
65             # _underline_
66             # /italic/
67              
68             =head1 METHODS
69              
70             =cut
71              
72             =head2 $st = String::Tagged::IRC->parse_irc( $raw, %opts )
73              
74             Parses a text string containing IRC formatting codes and returns a new
75             C instance.
76              
77             Takes the following named options:
78              
79             =over 8
80              
81             =item parse_plain_formatting => BOOL
82              
83             If true, also parse "poor-man's" plain-text formatting of B<*bold*>,
84             I and _underline_. In this case, formatting tags are added but the
85             original text formatting is preserved.
86              
87             =back
88              
89             =cut
90              
91             sub _parse_colour_mirc
92             {
93 3     3   2 shift;
94 3         5 my ( $colcode ) = @_;
95              
96             # RRGGBB hex triplet
97 3 100       10 $colcode =~ m/^#([0-9a-f]{6})/i and
98             return Convert::Color::RGB8->new( $1 );
99              
100             # RGB hex triplet
101 2 50       4 $colcode =~ m/^#([0-9a-f])([0-9a-f])([0-9a-f])/i and
102             return Convert::Color::RGB8->new( "$1$1$2$2$3$3" );
103              
104             # mIRC index
105 2 50 33     23 $colcode =~ m/^(\d\d?)/ and $1 < 16 and
106             return Convert::Color::mIRC->new( $1 );
107              
108 0         0 return undef;
109             }
110              
111             my @termcolours =
112             map { chomp; Convert::Color::RGB8->new( $_ ) } ;
113             close DATA;
114              
115             sub _parse_colour_ansiterm
116             {
117 3     3   3 shift;
118 3         3 my ( $idx ) = @_;
119              
120 3 50 33     19 $idx >= 0 and $idx < @termcolours and
121             return $termcolours[$idx];
122              
123 0         0 return undef;
124             }
125              
126             sub parse_irc
127             {
128 7     7 1 6438 my $class = shift;
129 7         13 my ( $text, %opts ) = @_;
130              
131 7         25 my $self = $class->new( "" );
132              
133 7         81 my %format;
134              
135 7         18 while( length $text ) {
136 35 100       824 if( $text =~ s/^([\x00-\x1f])// ) {
137 16         24 my $ctrl = chr(ord($1)+0x40);
138              
139 16 100 33     112 if( $ctrl eq "B" ) {
    50 66        
    100          
    50          
    50          
    100          
    50          
140 2 100       7 $format{bold} ? delete $format{bold} : ( $format{bold} = 1 );
141             }
142             elsif( $ctrl eq "U" or $ctrl eq "_" ) {
143 0 0       0 $format{under} ? delete $format{under} : ( $format{under} = 1 );
144             }
145             elsif( $ctrl eq "R" or $ctrl eq "]" ) {
146 2 100       6 $format{italic} ? delete $format{italic} : ( $format{italic} = 1 );
147             }
148             elsif( $ctrl eq "V" ) {
149 0 0       0 $format{reverse} ? delete $format{reverse} : ( $format{reverse} = 1 );
150             }
151             elsif( $ctrl eq "O" ) {
152 0         0 undef %format;
153             }
154             elsif( $ctrl eq "C" ) {
155 4         9 my $colourre = qr/#[0-9a-f]{6}|#[0-9a-f]{3}|\d\d?/i;
156              
157 4 100       80 if( $text =~ s/^($colourre),($colourre)// ) {
    100          
158 1         3 $format{fg} = $self->_parse_colour_mirc( $1 );
159 1         8 $format{bg} = $self->_parse_colour_mirc( $2 );
160             }
161             elsif( $text =~ s/^($colourre)// ) {
162 1         4 $format{fg} = $self->_parse_colour_mirc( $1 );
163             }
164             else {
165 2         3 delete $format{fg};
166 2         7 delete $format{bg};
167             }
168             }
169             elsif( $ctrl eq "D" ) {
170 8 50       30 if( $text =~ s/^b// ) { # underline
    100          
    100          
    100          
171 0 0       0 $format{under} ? delete $format{under} : ( $format{under} = 1 );
172             }
173             elsif( $text =~ s/^c// ) { # bold
174 2 100       8 $format{bold} ? delete $format{bold} : ( $format{bold} = 1 );
175             }
176             elsif( $text =~ s/^d// ) { # revserse/italic
177 2 100       7 $format{italic} ? delete $format{italic} : ( $format{italic} = 1 );
178             }
179             elsif( $text =~ s/^g// ) {
180 2         5 undef %format
181             }
182             else {
183 2         5 $text =~ s/^(.)(.)//;
184 2         2 my ( $fg, $bg ) = map { ord( $_ ) - ord('0') } ( $1, $2 );
  4         7  
185 2 50       20 if( $fg > 0 ) {
186 2         4 $format{fg} = $self->_parse_colour_ansiterm( $fg );
187             }
188 2 100       5 if( $bg > 0 ) {
189 1         2 $format{bg} = $self->_parse_colour_ansiterm( $bg );
190             }
191             }
192             }
193             }
194             else {
195 19         42 $text =~ s/^([^\x00-\x1f]+)//;
196 19         33 my $piece = $1;
197              
198             # Now scan this piece for the text-based ones
199 19   66     57 while( length $piece and $opts{parse_plain_formatting} ) {
200             # Look behind/ahead asserts to ensure we don't capture e.g.
201             # /usr/bin/perl by mistake
202 2 50       50 $piece =~ s/^(.*?)(?
203             last;
204              
205 2         5 my ( $pre, $inner, $flag ) = ( $1, $2, $3 );
206              
207 2 50       11 $self->append_tagged( $pre, %format ) if length $pre;
208              
209 2         66 my %innerformat = %format;
210              
211             $innerformat{
212 2         12 { '*' => "bold", '_' => "under", '/' => "italic" }->{$flag}
213             } = 1;
214              
215 2         7 $self->append_tagged( $inner, %innerformat );
216             }
217              
218 19 100       105 $self->append_tagged( $piece, %format ) if length $piece;
219             }
220             }
221              
222 7         93 return $self;
223             }
224              
225             =head2 $raw = $st->build_irc
226              
227             Returns a plain text string containing IRC formatting codes built from the
228             given instance. When outputting a colour index, this method always outputs it
229             as a two-digit number, to avoid parsing ambiguity if the coloured text starts
230             with a digit.
231              
232             Currently this will only output F-style formatting, not F-style.
233              
234             Takes the following options:
235              
236             =over 8
237              
238             =item default_fg => INT
239              
240             Default foreground colour to emit for extents that have only the C tag
241             set. This is required because F formatting codes cannot set just the
242             background colour without setting the foreground as well.
243              
244             =back
245              
246             =cut
247              
248             sub build_irc
249             {
250 3     3 1 1088 my $self = shift;
251 3         5 my %opts = @_;
252              
253 3   50     12 my $default_fg = $opts{default_fg} // 0;
254              
255 3         2 my $ret = "";
256 3         2 my %formats;
257              
258             $self->iter_extents_nooverlap( sub {
259 9     9   206 my ( $extent, %tags ) = @_;
260              
261 9 100       18 $ret .= "\cB" if !$formats{bold} != !$tags{bold};
262 9 50       14 $ret .= "\c_" if !$formats{under} != !$tags{under};
263 9 100       15 $ret .= "\c]" if !$formats{italic} != !$tags{italic};
264 9 50       13 $ret .= "\cV" if !$formats{reverse} != !$tags{reverse};
265 9         25 $formats{$_} = $tags{$_} for qw( bold under italic reverse );
266              
267 9 100       23 my $fg = $tags{fg} ? $tags{fg}->as_mirc->index : undef;
268 9 100       9906 my $bg = $tags{bg} ? $tags{bg}->as_mirc->index : undef;
269              
270 9 100 100     717 if( ( $fg//'' ) ne ( $formats{fg}//'' ) or ( $bg//'' ) ne ( $formats{bg}//'' ) ) {
      100        
      100        
      50        
      100        
271 3 100       6 if( defined $bg ) {
    100          
272             # Can't set just bg alone, so if fg isn't defined, use the default
273 1   33     5 $fg //= $default_fg;
274              
275 1         3 $ret .= sprintf "\cC%02d,%02d", $fg, $bg;
276             }
277             elsif( defined $fg ) {
278 1         3 $ret .= sprintf "\cC%02d", $fg;
279             }
280             else {
281 1         2 $ret .= "\cC";
282             }
283             }
284              
285 9         10 $formats{fg} = $fg;
286 9         6 $formats{bg} = $bg;
287              
288             # TODO: colours
289              
290 9         18 $ret .= $extent->plain_substr;
291 3         18 });
292              
293             # Be polite and reset colours at least
294 3 100 66     76 $ret .= "\cC" if defined $formats{fg} or defined $formats{bg};
295              
296 3         13 return $ret;
297             }
298              
299             sub new_from_formatted
300             {
301 0     0 0   my $class = shift;
302 0           my ( $orig ) = @_;
303              
304 0           return $class->clone( $orig,
305             only_tags => [qw( bold under italic reverse fg bg )]
306             );
307             }
308              
309             sub as_formatted
310             {
311 0     0 0   my $self = shift;
312 0           return $self;
313             }
314              
315             =head1 TODO
316              
317             =over 4
318              
319             =item *
320              
321             Define a nicer way to do the ANSI terminal colour space of F-style
322             formatting codes.
323              
324             =back
325              
326             =head1 AUTHOR
327              
328             Paul Evans
329              
330             =cut
331              
332             0x55AA;
333              
334             # Palette used for irssi->RGB8 conversion
335              
336             __DATA__