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__ |