File Coverage

blib/lib/String/Tagged/IRC.pm
Criterion Covered Total %
statement 87 98 88.7
branch 61 80 76.2
condition 22 34 64.7
subroutine 10 12 83.3
pod 2 4 50.0
total 182 228 79.8


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