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