File Coverage

blib/lib/String/Tagged/IRC.pm
Criterion Covered Total %
statement 91 97 93.8
branch 61 80 76.2
condition 22 34 64.7
subroutine 10 10 100.0
pod 2 2 100.0
total 186 223 83.4


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