File Coverage

blib/lib/HTML/BBCode.pm
Criterion Covered Total %
statement 161 170 94.7
branch 78 88 88.6
condition 37 44 84.0
subroutine 17 18 94.4
pod 2 2 100.0
total 295 322 91.6


line stmt bran cond sub pod time code
1             package HTML::BBCode;
2              
3             =head1 NAME
4              
5             HTML::BBCode - Perl extension for converting BBcode to HTML.
6              
7             =head1 SYNOPSIS
8              
9             use HTML::BBCode;
10              
11             my $bbc = HTML::BBCode->new( \%options );
12             my $html = $bbc->parse($bbcode);
13              
14             # Input
15             print $bbc->{bbcode};
16              
17             # Output
18             print $bbc->{html};
19              
20             =head1 DESCRIPTION
21              
22             C converts BBCode -as used on the phpBB bulletin
23             boards- to its HTML equivalent.
24              
25             Please note that, although this was the first BBCode module, it's by
26             far not the best nor fastest. It's also not heavilly maintained, so
27             you might want to look at L and L.
28              
29             =head2 METHODS
30              
31             The following methods can be used
32              
33             =head3 new
34              
35             my $bbc = HTML::BBCode->new({
36             allowed_tags => [ @bbcode_tags ],
37             stripscripts => 1,
38             linebreaks => 1,
39             });
40              
41             C creates a new C object using the configuration
42             passed to it. The object's default configuration allows all BBCode to
43             be converted to the default HTML.
44              
45             =head4 options
46              
47             =over 5
48              
49             =item allowed_tags
50              
51             Defaults to all currently know C, being:
52             b, u, i, color, size, quote, code, list, url, email, img. With this
53             option, you can specify what BBCode tags you would like to convert.
54              
55             =item stripscripts
56              
57             Enabled by default, this option will remove all the XSS trickery (and
58             thus is probably best not to turn it off).
59              
60             =item no_html
61              
62             This option has been removed since version 2.0
63              
64             =item no_jslink
65              
66             This option has been removed since version 2.0
67              
68             =item linebreaks
69              
70             Disabled by default.
71              
72             When true, will substitute linebreaks into HTML ('
')
73              
74             =back
75              
76             =head3 parse
77              
78             my $html = $bbc->parse($bbcode);
79              
80             Parses text supplied as a single scalar string and returns the HTML as
81             a single scalar string.
82              
83             =head1 CAVEAT: API CHANGES
84              
85             Please do note that the C, C, C options in
86             the new method have been removed since version 2.0 due to the XSS protection
87             (provided by L). This will most likely
88             break your current scripts (if you used the C option).
89              
90             =head1 SEE ALSO
91              
92             =over 4
93              
94             =item * L
95              
96             =item * L
97              
98             =item * L, L, L
99              
100             =back
101              
102             =head1 BUGS
103              
104             C. Please report bugs to L.
105              
106             =head1 AUTHOR
107              
108             Menno Blom, Eblom@cpan.orgE
109              
110             =head1 COPYRIGHT AND LICENSE
111              
112             Copyright (C) 2004-2009 by Menno Blom
113              
114             This library is free software; you can redistribute it and/or modify
115             it under the same terms as Perl itself.
116              
117             =cut
118             #------------------------------------------------------------------------------#
119 17     17   563844 use strict;
  17         42  
  17         890  
120 17     17   126 use warnings;
  17         35  
  17         579  
121 17     17   10255 use HTML::BBCode::StripScripts;
  17         60  
  17         61218  
122              
123             our $VERSION = '2.07';
124             our @bbcode_tags = qw(code quote b u i color size list url email img);
125              
126             sub new {
127 24     24 1 260 my ($class, $args) = @_;
128 24   100     168 $args ||= {};
129 24 50       122 $class->_croak("Options must be a hash reference")
130             if ref($args) ne 'HASH';
131 24         61 my $self = {};
132 24         71 bless $self, $class;
133 24 50       124 $self->_init($args) or return undef;
134              
135 24         95 return $self;
136             }
137              
138             sub _init {
139 24     24   56 my ($self, $args) = @_;
140              
141 24         399 my %html_tags = (
142             code => '
Code:
'.
143             '
%s
',
144             quote => '
%s
'.
145             '
%s
',
146             b => '%s',
147             u => '%s',
148             i => '%s',
149             color => '%s',
150             size => '%s',
151             url => '%s',
152             email => '%s',
153             img => '',
154             ul => '
    %s
',
155             ol_number => '
    %s
',
156             ol_alpha => '
    %s
',
157             );
158              
159 24         173 my %options = (
160             allowed_tags=> \@bbcode_tags,
161             html_tags => \%html_tags,
162             stripscripts => 1,
163             linebreaks => 0,
164 24         82 %{ $args },
165             );
166 24         153 $self->{options} = \%options;
167              
168 24         1545 $self->{'hss'} = HTML::BBCode::StripScripts->new({
169             Context => 'Flow',
170             AllowSrc => 1,
171             AllowMailto => 1,
172             AllowHref => 1,
173             AllowRelURL => 1,
174             EscapeFiltered => 1,
175             BanAllBut => [qr/a div img li ol span ul/],
176             Rules => {
177             br => 1,
178             img => {
179             required => ['src'],
180             'src' => 1,
181             'alt' => 1,
182             '*' => 0,
183             },
184             a => {
185             required => ['href'],
186             'href' => 1,
187             '*' => 0,
188             },
189             img => {
190             'src' => 1,
191             'alt' => 1,
192             '*' => 0,
193             },
194             div => {
195             class => qr{^bbcode_},
196             '*' => 0,
197             },
198             span => {
199             style => \&_filter_style,
200             '*' => 0,
201             },
202             ol => {
203             style => qr/^list-style-type:lower-alpha$/,
204             '*' => 0,
205             },
206             ul => 1,
207             li => 1,
208             }
209             });
210              
211 24         8448 return $self;
212             }
213              
214             # Parse the input!
215             sub parse {
216 70     70 1 19090 my ($self, $bbcode) = @_;
217 70 100       215 return if(!defined $bbcode);
218              
219 69         174 $self->{_stack} = [];
220 69         160 $self->{_in_code_block} = 0;
221 69         137 $self->{_skip_nest} = '';
222 69         115 $self->{_nest_count} = 0;
223 69         107 $self->{_nest_count_stack} = 0;
224 69         241 $self->{_dont_nest} = ['code', 'url', 'email', 'img'];
225 69         154 $self->{bbcode} = '';
226 69         410 $self->{html} = '';
227              
228 69         119 $self->{bbcode} = $bbcode;
229 69         109 my $input = $bbcode;
230              
231             main:
232 69         89 while(1) {
233             # End tag
234 374 100       2024 if($input =~ /^(\[\/[^\]]+\])/s) {
    100          
    100          
    100          
235 84         170 my $end = lc $1;
236 84 100 66     1876 if(($self->{_skip_nest} ne '' && $end ne "[/$self->{_skip_nest}]") ||
      100        
      33        
237             ($self->{_in_code_block} && $end ne "[/code]")) {
238 1         3 _content($self, $end);
239             } else {
240 83         191 _end_tag($self, $end);
241             }
242 84         181 $input = $';
243             }
244              
245             # Opening tag
246             elsif($input =~ /^(\[[^\]]+\])/s ) {
247 100 100       233 if($self->{_in_code_block}) {
248 1         3 _content($self, $1);
249             } else {
250 99         257 _open_tag($self, $1);
251             }
252 100         233 $input = $';
253             }
254              
255             # None BBCode content till next tag
256             elsif($input =~ /^([^\[]+)/s) {
257 118         245 _content($self, $1);
258 118         239 $input = $';
259             }
260              
261             # BUG #14138 unmatched bracket, content till end of input
262             elsif($input =~ /^(.+)$/s) {
263 3         7 _content($self, $1);
264 3         7 $input = $';
265             }
266              
267             # Now what?
268             else {
269 69 50       206 last main if(!$input); # We're at the end now, stop parsing!
270             }
271             }
272 69         110 $self->{html} = join('', @{$self->{_stack}});
  69         215  
273 69 100       330 return $self->{options}->{stripscripts} ? $self->_stripscripts() : $self->{html};
274             }
275              
276             sub _open_tag {
277 99     99   224 my ($self, $open) = @_;
278 99         523 my ($tag, $rest) = $open =~ m/\[([^=\]]+)(.*)?\]/s; # Don't do this! ARGH!
279 99         184 $tag = lc $tag;
280 99 100 100     241 if(_dont_nest($self, $tag) && $tag eq 'img') {
281 9         18 $self->{_skip_nest} = $tag;
282             }
283 99 100       288 if($self->{_skip_nest} eq $tag) {
284 9         16 $self->{_nest_count}++;
285 9         15 $self->{_nest_count_stack}++;
286             }
287 99 100       216 $self->{_in_code_block}++ if($tag eq 'code');
288 99         118 push @{$self->{_stack}}, '['.$tag.$rest.']';
  99         369  
289             }
290              
291             sub _content {
292 123     123   259 my ($self, $content) = @_;
293 123         1284 $content =~ s|\r*||gs;
294 123 100 100     484 $content =~ s|\n|
\n|gs if($self->{options}->{linebreaks} &&
295             $self->{_in_code_block} == 0);
296 123         154 push @{$self->{_stack}}, $content;
  123         373  
297             }
298              
299             sub _end_tag {
300 83     83   142 my ($self, $end) = @_;
301 83         97 my ($tag, $arg);
302 83         157 my @buf = ( $end );
303              
304 83 50 66     416 if("[/$self->{_skip_nest}]" eq $end && $self->{_nest_count} > 1) {
305 0         0 push @{$self->{_stack}}, $end;
  0         0  
306 0         0 $self->{_nest_count}--;
307 0         0 return;
308             }
309              
310 83 100       354 $self->{_in_code_block} = 0 if($end eq '[/code]');
311            
312             # Loop through the stack
313 83         113 while(1) {
314 204         220 my $item = pop(@{$self->{_stack}});
  204         391  
315 204         330 push @buf, $item;
316              
317 204 100       566 if(!defined $item) {
318 2 100       7 map { push @{$self->{_stack}}, $_ if($_) } reverse @buf;
  6         19  
  4         15  
319 2         3 last;
320             }
321              
322            
323 202 100       551 if("[$self->{_skip_nest}]" eq "$item") {
324 5         16 $self->{_nest_count_stack}--;
325 5 50       18 next if($self->{_nest_count_stack} > 0);
326             }
327              
328 202 100 100     609 $self->{_nest_count}--
329             if("[/$self->{_skip_nest}]" eq $end && $self->{_nest_count} > 0) ;
330              
331              
332 202 100       829 if($item =~ /\[([^=\]]+).*\]/s) {
333 101         192 $tag = $1;
334 101 100 66     571 if ($tag && $end eq "[/$tag]") {
335 81 100       99 push @{$self->{_stack}}, (_is_allowed($self, $tag))
  81         254  
336             ? _do_BB($self, @buf)
337             : reverse @buf;
338             # Clear the _skip_nest?
339 81 100 66     469 $self->{_skip_nest} = '' if(defined $self->{_skip_nest} &&
340             $tag eq $self->{_skip_nest});
341 81         138 last;
342             }
343             }
344             }
345 83         237 $self->{_nest_count_stack} = 0;
346             }
347              
348             sub _do_BB {
349 75     75   203 my ($self, @buf) = @_;
350 75         95 my ($tag, $attr);
351 0         0 my $html;
352              
353             # Get the opening tag
354 75         233 my $open = pop(@buf);
355             # We prefer to read in non-reverse way
356 75         120 @buf = reverse @buf;
357             # Closing tag is kinda useless, pop it
358 75         93 pop(@buf);
359             # Rest should be content;
360 75         176 my $content = join(' ', @buf);
361              
362             # What are we dealing with anyway? Any attributes maybe?
363 75 50       439 if($open =~ /\[([^=\]]+)=?([^\]]+)?]/) {
364 75         127 $tag = $1;
365 75         146 $attr = $2;
366             }
367              
368             # Kludgy way to handle specific BBCodes ...
369 75 100 100     685 if($tag eq 'quote') {
    100 100        
    100          
    100          
    100          
370 8 100       64 $html = sprintf($self->{options}->{html_tags}->{quote},
371             ($attr) ? "$attr wrote:"
372             : "Quote:",
373             $content
374             );
375             } elsif($tag eq 'code') {
376 3         22 $html = sprintf($self->{options}->{html_tags}->{code}, _code($content));
377             } elsif($tag eq 'list') {
378 8         29 $html = _list($self, $attr, $content);
379             } elsif(($tag eq 'email' || $tag eq 'url') && !$attr) {
380 4         24 $html = sprintf($self->{options}->{html_tags}->{$tag}, $content,$content);
381             } elsif ($attr) {
382 24         159 $html = sprintf($self->{options}->{html_tags}->{$tag}, $attr, $content);
383             } else {
384 28         145 $html = sprintf($self->{options}->{html_tags}->{$tag}, $content);
385             }
386             # Return ...
387 75         224 return $html;
388             }
389              
390             sub _is_allowed {
391 81     81   126 my ($self, $check) = @_;
392 447 100       1258 map {
393 81         202 return 1 if ($_ eq $check);
394 81         98 } @{$self->{options}->{allowed_tags}};
395 6         19 return 0;
396             }
397              
398             sub _dont_nest {
399 99     99   143 my ($self, $check) = @_;
400 345 100       1145 map {
401 99         228 return 1 if($_ eq $check);
402 99         139 } @{$self->{_dont_nest}};
403 65         207 return 0;
404             }
405              
406             sub _code {
407 3     3   6 my $code = shift;
408 3         10 $code =~ s|^\s+?[\n\r]+?||;
409 3         8 $code =~ s|<|\<|g;
410 3         9 $code =~ s|>|\>|g;
411 3         11 $code =~ s|\[|\[|g;
412 3         12 $code =~ s|\]|\]|g;
413 3         10 $code =~ s| |\ |g;
414 3         9 $code =~ s|\n|
|g;
415 3         19 return $code;
416             }
417              
418             sub _list {
419 8     8   18 my ($self, $attr, $content) = @_;
420 8         18 $content =~ s|^
[\s\r\n]*|\n|s;
421 8         43 $content =~ s|\[\*\]([^\[]+)|_list_removelastbr($1)|egs;
  18         65  
422 8         20 $content =~ s|
$|\n|s;
423 8 100       23 if($attr) {
424 4 100       28 return sprintf($self->{options}->{html_tags}->{ol_number}, $content)
425             if($attr =~ /^\d/);
426 2 50       55 return sprintf($self->{options}->{html_tags}->{ol_alpha}, $content)
427             if($attr =~ /^\D/);
428             } else {
429 4         36 return sprintf($self->{options}->{html_tags}->{ul}, $content);
430             }
431             }
432              
433             sub _list_removelastbr {
434 18     18   38 my $content = shift;
435 18         32 $content =~ s|
[\s\r\n]*$||;
436 18         51 $content =~ s|^\s*||;
437 18         71 $content =~ s|\s*$||;
438 18         78 return "
  • $content
  • \n";
    439             }
    440              
    441             sub _stripscripts {
    442 67     67   96 my $self = shift;
    443 67         376 $self->{'html'} = $self->{'hss'}->filter_html($self->{'html'});
    444 67         26575 return $self->{'html'};
    445             }
    446              
    447             sub _filter_style {
    448 29     29   6236 my ($filter, $tag, $attr_name, $attr_val) = @_;
    449 29 100 100     216 if ($attr_val eq 'font-weight:bold'
          100        
          66        
    450             or $attr_val eq 'text-decoration:underline'
    451             or $attr_val eq 'font-style:italic'
    452             or $attr_val eq 'list-style-type') {
    453 22         60 return $attr_val;
    454             }
    455 7 100       43 if ( my ($color) = $attr_val =~ /^color:(.*)/ ) {
    456 4         26 my @html_color = qw/
    457             black gray maroon red green lime olive yellow
    458             navy blue purple fuchsia teal aqua silver white
    459             /;
    460 4 50       16 return $attr_val if $color =~ /^#[a-fA-F\d]{6}$/;
    461 4 50       16 return $attr_val if $color =~ /^#[a-fA-F\d]{3}$/;
    462 4 50       10 return $attr_val if grep { $color eq $_ } @html_color;
      64         121  
    463 0         0 return undef;
    464             }
    465 3 100       16 if ( $attr_val =~ /font-size:\d+px/ ) {
    466 2         7 return $attr_val;
    467             }
    468 1         4 return undef;
    469             }
    470              
    471             sub _croak {
    472 0     0     my ($class, @error) = @_;
    473 0           require Carp;
    474 0           Carp::croak(@error);
    475             }
    476              
    477             1;