File Coverage

blib/lib/Parse/BBCode.pm
Criterion Covered Total %
statement 506 509 99.4
branch 195 210 92.8
condition 102 125 81.6
subroutine 31 31 100.0
pod 10 10 100.0
total 844 885 95.3


line stmt bran cond sub pod time code
1             package Parse::BBCode;
2             $Parse::BBCode::VERSION = '0.15';
3 14     14   340464 use strict;
  14         34  
  14         617  
4 14     14   88 use warnings;
  14         29  
  14         411  
5 14     14   8491 use Parse::BBCode::Tag;
  14         46  
  14         219  
6 14     14   11182 use Parse::BBCode::HTML qw/ &defaults &default_escapes &optional /;
  14         49  
  14         2624  
7 14     14   139 use base 'Class::Accessor::Fast';
  14         43  
  14         1536  
8             __PACKAGE__->follow_best_practice;
9             __PACKAGE__->mk_accessors(qw/
10             tags allowed compiled plain strict_attributes close_open_tags error
11             tree escapes direct_attribute params url_finder text_processor linebreaks
12             smileys attribute_parser strip_linebreaks attribute_quote /);
13             #use Data::Dumper;
14 14     14   72 use Carp;
  14         87  
  14         64708  
15             my $scalar_util = eval "require Scalar::Util; 1";
16              
17             my %defaults = (
18             strict_attributes => 1,
19             direct_attribute => 1,
20             linebreaks => 1,
21             smileys => 0,
22             url_finder => 0,
23             strip_linebreaks => 1,
24             attribute_quote => '"',
25             );
26             sub new {
27 37     37 1 24621 my ($class, $args) = @_;
28 37   100     147 $args ||= {};
29 37         161 my %args = %$args;
30 37 100       125 unless ($args{tags}) {
31 6         33 $args{tags} = { $class->defaults };
32             }
33             else {
34 31         44 $args{tags} = { %{ $args{tags} } };
  31         230  
35             }
36 37 100       140 unless ($args{escapes}) {
37 34         164 $args{escapes} = {$class->default_escapes };
38             }
39             else {
40 3         14 $args{escapes} = { %{ $args{escapes} } }
  3         22  
41             }
42 37         522 my $self = $class->SUPER::new({
43             %defaults,
44             %args
45             });
46 37         588 $self->set_allowed([ grep { length } keys %{ $self->get_tags } ]);
  283         862  
  37         166  
47 37         339 $self->_compile_tags;
48 37         366 return $self;
49             }
50              
51             my $re_split = qr{ % (?:\{ (?:[a-zA-Z\|]+) \})? (?:attr|[Aas]) }x;
52             my $re_cmp = qr{ % (?:\{ ([a-zA-Z\|]+) \})? (attr|[Aas]) }x;
53              
54             sub forbid {
55 2     2 1 5236 my ($self, @tags) = @_;
56 2         12 my $allowed = $self->get_allowed;
57 2         15 my $re = join '|', map { quotemeta } @tags;
  2         12  
58 2         5 @$allowed = grep { ! m/^(?:$re)\z/ } @$allowed;
  27         107  
59             }
60              
61             sub permit {
62 3     3 1 6302 my ($self, @tags) = @_;
63 3         19 my $allowed = $self->get_allowed;
64 3         22 my %seen;
65 42 50       394 @$allowed = grep {
66 3         10 !$seen{$_}++ && $self->get_tags->{$_};
67             } (@$allowed, @tags);
68             }
69              
70             sub _compile_tags {
71 37     37   61 my ($self) = @_;
72             # unless ($self->get_compiled) {
73             {
74 37         54 my $defs = $self->get_tags;
  37         112  
75              
76             # get definition for how text should be rendered which is not in tags
77 37         156 my $plain;
78 37 100       115 if (exists $defs->{""}) {
79 9         26 $plain = delete $defs->{""};
80 9 100       50 if (ref $plain eq 'CODE') {
81 8         60 $self->set_plain($plain);
82             }
83             }
84             else {
85 28         102 my $url_finder = $self->get_url_finder;
86 28         174 my $linebreaks = $self->get_linebreaks;
87 28         176 my $smileys = $self->get_smileys;
88 28 100       169 if ($url_finder) {
89 6         9 my $result = eval { require URI::Find; 1 };
  6         37  
  6         9  
90 6 50       17 unless ($result) {
91 0         0 undef $url_finder;
92             }
93             }
94 28         65 my $escape = \&Parse::BBCode::escape_html;
95 28         45 my $post_processor_1 = $escape;
96 28         36 my $post_processor;
97 28         92 my $text_processor = $self->get_text_processor;
98 28 100       155 if ($text_processor) {
99 5         8 $post_processor_1 = $text_processor;
100             }
101 28 100 66     144 if ($smileys and ref($smileys->{icons}) eq 'HASH') {
102 1   50     10 $smileys = {
      50        
103             icons => $smileys->{icons},
104             base_url => $smileys->{base_url} || '/smileys/',
105             format => $smileys->{format} || '%s',
106             };
107 4         8 my $re = join '|', map { quotemeta $_ } sort { length $b <=> length $a }
  5         7  
  1         5  
108 1         2 keys %{ $smileys->{icons} };
109             my $code = sub {
110 4     4   7 my ($text, $post_processor) = @_;
111 4         6 my $out = '';
112 4         86 while ($text =~ s/\A (^|.*?[\s]) ($re) (?=[\s]|$)//xsm) {
113 7         21 my ($pre, $emo) = ($1, $2);
114 7         18 my $url = "$smileys->{base_url}$smileys->{icons}->{$emo}";
115 7         13 my $emo_escaped = Parse::BBCode::escape_html($emo);
116 7         24 my $image_tag = sprintf $smileys->{format}, $url, $emo_escaped;
117 7         16 $out .= $post_processor_1->($pre) . $image_tag;
118             }
119 4         8 $out .= $post_processor_1->($text);
120 4         7 return $out;
121 1         6 };
122 1         1 $post_processor = $code;
123             }
124             else {
125 27         58 $post_processor = $post_processor_1;
126             }
127              
128 28 100       67 if ($url_finder) {
129 6         6 my $url_find_sub;
130 6 100       15 if (ref($url_finder) eq 'CODE') {
131 1         3 $url_find_sub = $url_finder;
132             }
133             else {
134 5 100       14 unless (ref($url_finder) eq 'HASH') {
135 1         3 $url_finder = {
136             max_length => 50,
137             format => '%s',
138             };
139             }
140 5   50     14 my $max_url = $url_finder->{max_length} || 0;
141 5         8 my $format = $url_finder->{format};
142             my $finder = URI::Find->new(sub {
143 2     2   9684 my ($url) = @_;
144 2         4 my $title = $url;
145 2 100 66     12 if ($max_url and length($title) > $max_url) {
146 1         7 $title = substr($title, 0, $max_url) . "...";
147             }
148 2         18 my $escaped = Parse::BBCode::escape_html($url);
149 2         5 my $escaped_title = Parse::BBCode::escape_html($title);
150 2         10 my $href = sprintf $format, $escaped, $title;
151 2         19 return $href;
152 5         36 });
153             $url_find_sub = sub {
154 5     5   6 my ($ref_content, $post, $info) = @_;
155 5         26 $finder->find($ref_content, sub { $post->($_[0], $info) });
  2         91  
156 5         56 };
157             }
158             $plain = sub {
159 9     9   13 my ($parser, $attr, $content, $info) = @_;
160 9 100       21 unless ($info->{classes}->{url}) {
161 6         16 $url_find_sub->(\$content, $post_processor, $info);
162             }
163             else {
164 3         8 $content = $post_processor->($content);
165             }
166 9 100       1360 $content =~ s/\r?\n|\r/
\n/g if $linebreaks;
167 9         32 $content;
168 6         21 };
169             }
170             else {
171             $plain = sub {
172 234     234   402 my ($parser, $attr, $content, $info) = @_;
173 234         431 my $text = $post_processor->($content, $info);
174 234 100       1217 $text =~ s/\r?\n|\r/
\n/g if $linebreaks;
175 234         702 $text;
176 22         179 };
177             }
178 28         108 $self->set_plain($plain);
179             }
180              
181             # now compile the rest of definitions
182 37         327 for my $key (keys %$defs) {
183 274         394 my $def = $defs->{$key};
184             #warn __PACKAGE__.':'.__LINE__.": $key: $def\n";
185 274 100 66     846 if (not ref $def) {
    100          
186 186         631 my $new_def = $self->_compile_def($def);
187 186         340 $defs->{$key} = $new_def;
188             }
189             elsif (not exists $def->{code} and exists $def->{output}) {
190 8         24 my $new_def = $self->_compile_def($def);
191 8         17 $defs->{$key} = $new_def;
192             }
193 274   100     867 $defs->{$key}->{class} ||= 'inline';
194 274 100       796 $defs->{$key}->{classic} = 1 unless defined $defs->{$key}->{classic};
195 274 100       797 $defs->{$key}->{close} = 1 unless defined $defs->{$key}->{close};
196             }
197 37         214 $self->set_compiled(1);
198             }
199             }
200              
201             sub _compile_def {
202 194     194   271 my ($self, $def) = @_;
203 194         504 my $esc = $self->get_escapes;
204 194         987 my $parse = 0;
205 194         261 my $new_def = {};
206 194         242 my $output = $def;
207 194         222 my $close = 1;
208 194         249 my $class = 'inline';
209 194 100       399 if (ref $def eq 'HASH') {
210 8         2797 $new_def = { %$def };
211 8         28 $output = delete $new_def->{output};
212 8         177 $parse = $new_def->{parse};
213 8 100       27 $close = $new_def->{close} if exists $new_def->{close};
214 8 100       30 $class = $new_def->{class} if exists $new_def->{class};
215             }
216             else {
217             }
218             # we have a string, compile
219             #warn __PACKAGE__.':'.__LINE__.": $key => $output\n";
220 194 100       648 if ($output =~ s/^(inline|block|url)://) {
221 43         110 $class = $1;
222             }
223 194         8161 my @parts = split m!($re_split)!, $output;
224             #warn __PACKAGE__.':'.__LINE__.$".Data::Dumper->Dump([\@parts], ['parts']);
225 194         297 my @compiled;
226 194         300 for my $p (@parts) {
227 774 100       3667 if ($p =~ m/$re_cmp/) {
228 305         704 my ($escape, $type) = ($1, $2);
229 305   100     980 $escape ||= 'parse';
230 305         1445 my @escapes = split /\|/, $escape;
231 305 100       466 if (grep { $_ eq 'parse' } @escapes) {
  309         1183  
232 161         203 $parse = 1;
233             }
234 305         1345 push @compiled, [\@escapes, $type];
235             }
236             else {
237 469         1235 push @compiled, $p;
238             }
239             #warn __PACKAGE__.':'.__LINE__.$".Data::Dumper->Dump([\@compiled], ['compiled']);
240             }
241             my $code = sub {
242 216     216   429 my ($self, $attr, $string, $fallback, $tag) = @_;
243 216         320 my $out = '';
244 216         349 for my $c (@compiled) {
245              
246             # just text
247 760 100       1365 unless (ref $c) {
248 468         1047 $out .= $c;
249             }
250             # tag attribute or content
251             else {
252 292         545 my ($escapes, $type) = @$c;
253 292         609 my @escapes = @$escapes;
254 292         369 my $var = '';
255 292         750 my $attributes = $tag->get_attr;
256 292 100 100     2355 if ($type eq 'attr' and @$attributes > 1) {
    100          
    100          
    100          
257 4         10 my $name = shift @escapes;
258 4         15 for my $item (@$attributes[1 .. $#$attributes]) {
259 4 50       22 if ($item->[0] eq $name) {
260 4         7 $var = $item->[1];
261 4         10 last;
262             }
263             }
264             }
265             elsif ($type eq 'a') {
266 13         26 $var = $attr;
267             }
268             elsif ($type eq 'A') {
269 57         150 $var = $fallback;
270             }
271             elsif ($type eq 's') {
272 217 100       558 if (ref $string eq 'SCALAR') {
273             # this text is already finished and escaped
274 208         419 $string = $$string;
275             }
276 217         296 $var = $string;
277             }
278 292         459 for my $e (@escapes) {
279 289         508 my $sub = $esc->{$e};
280 289 100       804 if ($sub) {
281 91         362 $var = $sub->($self, $c, $var);
282 91 100       543 unless (defined $var) {
283             # if escape returns undef, we return it unparsed
284 9         39 return $tag->get_start
285             . (join '', map {
286 5         35 $self->_render_tree($_);
287 5         20 } @{ $tag->get_content })
288             . $tag->get_end;
289             }
290             }
291             }
292 287         768 $out .= $var;
293             }
294             }
295 211         545 return $out;
296 194         1804 };
297 194         418 $new_def->{parse} = $parse;
298 194         328 $new_def->{code} = $code;
299 194         294 $new_def->{close} = $close;
300 194         378 $new_def->{class} = $class;
301 194         683 return $new_def;
302             }
303              
304             sub _render_text {
305 521     521   839 my ($self, $tag, $text, $info) = @_;
306             #warn __PACKAGE__.':'.__LINE__.": text '$text'\n";
307 521 100       1425 defined (my $code = $self->get_plain) or return $text;
308 518         3018 return $code->($self, $tag, $text, $info);
309             }
310              
311             sub parse {
312 192     192 1 387 my ($self, $text, $params) = @_;
313 192 100       656 my $parse_attributes = $self->get_attribute_parser ? $self->get_attribute_parser : $self->can('parse_attributes');
314 192         2096 $self->set_error(undef);
315 192         2034 my $defs = $self->get_tags;
316 192   50     1190 my $tags = $self->get_allowed || [keys %$defs];
317 192         1170 my @classic_tags = grep { $defs->{$_}->{classic} } @$tags;
  2221         4528  
318 192         315 my @short_tags = grep { $defs->{$_}->{short} } @$tags;
  2221         3351  
319 192         800 my $re_classic = join '|', map { quotemeta } sort {length $b <=> length $a } @classic_tags;
  2211         3912  
  5811         7375  
320             #$re_classic = qr/$re_classic/i;
321 192         742 my $re_short = join '|', map { quotemeta } sort {length $b <=> length $a } @short_tags;
  30         58  
  30         46  
322             #$re_short = qr/$re_short/i;
323             #warn __PACKAGE__.':'.__LINE__.$".Data::Dumper->Dump([\$re], ['re']);
324 192         221 my @tags;
325 192         253 my $out = '';
326 192         217 my @opened;
327 192         289 my $current_open_re = '';
328             my $callback_found_text = sub {
329 324     324   463 my ($text) = @_;
330 324 100       624 if (@opened) {
331 109         141 my $o = $opened[-1];
332 109         379 $o->add_content($text);
333             }
334             else {
335 215 100 100     987 if (@tags and !ref $tags[-1]) {
336             # text tag, concatenate
337 14         33 $tags[-1] .= $text;
338             }
339             else {
340 201         447 push @tags, $text;
341             }
342             }
343             #warn __PACKAGE__.':'.__LINE__.$".Data::Dumper->Dump([\@opened], ['opened']);
344 192         964 };
345 192         286 my $callback_found_tag;
346 192         221 my $in_url = 0;
347             $callback_found_tag = sub {
348 324     324   533 my ($tag) = @_;
349             #warn __PACKAGE__.':'.__LINE__.$".Data::Dumper->Dump([\$tag], ['tag']);
350             #warn __PACKAGE__.':'.__LINE__.$".Data::Dumper->Dump([\@opened], ['opened']);
351 324 100       825 if (@opened) {
    100          
352 126         174 my $o = $opened[-1];
353 126         318 my $class = $o->get_class;
354             #warn __PACKAGE__.':'.__LINE__.": tag $tag\n";
355 126 100 100     1515 if (ref $tag and $class =~ m/inline|url/ and $tag->get_class eq 'block') {
    100 100        
356 6         49 $self->_add_error('block_inline', $tag);
357 6         35 pop @opened;
358             #warn __PACKAGE__.':'.__LINE__.$".Data::Dumper->Dump([\$o], ['o']);
359 6 100       21 if ($self->get_close_open_tags) {
360             # we close the tag for you
361 2         16 $self->_finish_tag($o, '[/' . $o->get_name . ']', 1);
362 2         20 $callback_found_tag->($o);
363 2         11 $callback_found_tag->($tag);
364             }
365             else {
366             # nope, no automatic closing, invalidate all
367             # open inline tags before
368 4         28 my @red = $o->_reduce;
369 4         38 $callback_found_tag->($_) for @red;
370 4         16 $callback_found_tag->($tag);
371             }
372             }
373             elsif (ref $tag) {
374 105         772 my $def = $defs->{lc $tag->get_name};
375 105         547 my $parse = $def->{parse};
376 105 100       202 if ($parse) {
377 96         255 $o->add_content($tag);
378             }
379             else {
380 9         38 my $content = $tag->get_content;
381 9         45 my $string = '';
382 9         22 for my $c (@$content) {
383 8 100       22 if (ref $c) {
384 1         5 $string .= $c->raw_text( auto_close => 0 );
385             }
386             else {
387 7         27 $string .= $c;
388             }
389             }
390 9         43 $tag->set_content([$string]);
391 9         108 $o->add_content($tag);
392             }
393             }
394             else {
395 15         61 $o->add_content($tag);
396             }
397             }
398             elsif (ref $tag) {
399 191         530 my $def = $defs->{lc $tag->get_name};
400 191         1044 my $parse = $def->{parse};
401 191 100       368 if ($parse) {
402 155         290 push @tags, $tag;
403             }
404             else {
405 36         107 my $content = $tag->get_content;
406 36         162 my $string = '';
407 36         75 for my $c (@$content) {
408 35 100       78 if (ref $c) {
409 2         8 $string .= $c->raw_text( auto_close => 0 );
410             }
411             else {
412 33         176 $string .= $c;
413             }
414             }
415 36         143 $tag->set_content([$string]);
416 36         253 push @tags, $tag;
417             }
418             }
419             else {
420 7         11 push @tags, $tag;
421             }
422 155         646 $current_open_re = join '|', map {
423 324         1121 quotemeta $_->get_name
424             } @opened;
425              
426 192         1201 };
427 192         398 my @class = 'block';
428 192   100     1084 while (defined $text and length $text) {
429 511         759 $in_url = grep { $_->get_class eq 'url' } @opened;
  476         1920  
430             #warn __PACKAGE__.':'.__LINE__.$".Data::Dumper->Dump([\$in_url], ['in_url']);
431             #warn __PACKAGE__.':'.__LINE__.": ============= match $text\n";
432 511         1874 my $tag;
433 511         575 my ($before, $tag1, $tag2, $after);
434 511 100 66     4180 if ($re_classic and $re_short) {
    50 33        
    50 33        
435 29         447 ($before, $tag1, $tag2, $after) = split m{
436             (?:
437             \[ ($re_short) (?=://)
438             |
439             \[ ($re_classic) (?=\b|\]|\=)
440             )
441             }ix, $text, 2;
442             }
443             elsif (! $re_classic and $re_short) {
444 0         0 ($before, $tag1, $after) = split m{
445             \[ ($re_short) (?=://)
446             }ix, $text, 2;
447             }
448             elsif ($re_classic and !$re_short) {
449 482         6627 ($before, $tag2, $after) = split m{
450             \[ ($re_classic) (?=\b|\]|\=)
451             }ix, $text, 2;
452             }
453 14     14   145 { no warnings;
  14         27  
  14         45970  
  511         831  
454             # warn __PACKAGE__.':'.__LINE__.": $before, $tag1, $tag2, $after)\n";
455             #warn __PACKAGE__.':'.__LINE__.": RE: $current_open_re\n";
456             }
457             #warn __PACKAGE__.':'.__LINE__.$".Data::Dumper->Dump([\@opened], ['opened']);
458 511 100       1119 if (length $before) {
459             # look if it contains a closing tag
460             #warn __PACKAGE__.':'.__LINE__.": BEFORE $before\n";
461 313   100     5986 while (length $current_open_re and $before =~ s# (.*?) (\[ / ($current_open_re) \]) ##ixs) {
462             # found closing tag
463 207         2173 my ($content, $end, $name) = ($1, $2, $3);
464             #warn __PACKAGE__.':'.__LINE__.": found closing tag $name!\n";
465 207         250 my $f;
466             # try to find the matching opening tag
467             my @not_close;
468 207         463 while (@opened) {
469 240         410 my $try = pop @opened;
470 141         539 $current_open_re = join '|', map {
471 240         453 quotemeta $_->get_name
472             } @opened;
473 240 100       1140 if ($try->get_name eq lc $name) {
    100          
474 207         1137 $f = $try;
475 207         342 last;
476             }
477             elsif (!$try->get_close) {
478 25         268 $self->_finish_tag($try, '');
479 25         83 unshift @not_close, $try;
480             }
481             else {
482             # unbalanced
483 8         123 $self->_add_error('unclosed', $try);
484 8 100       69 if ($self->get_close_open_tags) {
485             # close
486 1         6 $f = $try;
487 1         3 unshift @not_close, $try;
488 1 50       4 if (@opened) {
489 1         5 $opened[-1]->add_content('');
490             }
491 1         6 $self->_finish_tag($try, '[/'. $try->get_name() .']', 1);
492             }
493             else {
494             # just add unparsed text
495 7         62 $callback_found_tag->($_) for $try->_reduce;
496             }
497             }
498             }
499 207 100       505 if (@not_close) {
500 22         69 $not_close[-1]->add_content($content);
501             }
502 207         488 for my $n (@not_close) {
503 26         82 $f->add_content($n);
504             #$callback_found_tag->($n);
505             }
506             # add text before closing tag as content to the current open tag
507 207 50       459 if ($f) {
508 207 100       436 unless (@not_close) {
509             #warn __PACKAGE__.':'.__LINE__.$".Data::Dumper->Dump([\$f], ['f']);
510 185         570 $f->add_content( $content );
511             }
512             # TODO
513 207         1129 $self->_finish_tag($f, $end);
514             #warn __PACKAGE__.':'.__LINE__.$".Data::Dumper->Dump([\$f], ['f']);
515 207         379 $callback_found_tag->($f);
516             }
517             }
518             # warn __PACKAGE__." === before='$before' ($tag)\n";
519 313         985 $callback_found_text->($before);
520             }
521              
522 511 100       1079 if (defined $tag1) {
523 10         17 $in_url = grep { $_->get_class eq 'url' } @opened;
  2         7  
524             # short tag
525             # $callback_found_text->($before) if length $before;
526 10 100       69 if ($after =~ s{ :// ([^\[]+) \] }{}x) {
527 8         18 my $content = $1;
528 8         25 my ($attr, $title) = split /\|/, $content, 2;
529 8 100 66     133 my $tag = $self->new_tag({
530             name => lc $tag1,
531             attr => [[$attr]],
532             attr_raw => $attr,
533             content => [(defined $title and length $title) ? $title : ()],
534             start => "[$tag1://$content]",
535             close => 0,
536             class => $defs->{lc $tag1}->{class},
537             single => $defs->{lc $tag1}->{single},
538             in_url => $in_url,
539             type => 'short',
540             });
541 8 100 66     156 if ($in_url and $tag->get_class eq 'url') {
542 1         8 $callback_found_text->($tag->get_start);
543             }
544             else {
545 7         16 $callback_found_tag->($tag);
546             }
547             }
548             else {
549 2         9 $callback_found_text->("[$tag1");
550             }
551 10         26 $text = $after;
552 10         48 next;
553             }
554 501         645 $tag = $tag2;
555              
556              
557 501         708 $in_url = grep { $_->get_class eq 'url' } @opened;
  229         847  
558              
559 501 100       1639 if ($after) {
    100          
560             # found start of a tag
561             #warn __PACKAGE__.':'.__LINE__.": find attribute for $tag\n";
562 341         1111 my ($ok, $attributes, $attr_string, $end) = $self->$parse_attributes(
563             text => \$after,
564             tag => lc $tag,
565             );
566 341 100       997 if ($ok) {
567 335         412 my $attr = $attr_string;
568 335 50       677 $attr = '' unless defined $attr;
569             #warn __PACKAGE__.':'.__LINE__.": found attribute for $tag: $attr\n";
570 335         758 my $close = $defs->{lc $tag}->{close};
571 335         506 my $def = $defs->{lc $tag};
572 335         3693 my $open = $self->new_tag({
573             name => lc $tag,
574             attr => $attributes,
575             attr_raw => $attr_string,
576             content => [],
577             start => "[$tag$attr]",
578             close => $close,
579             class => $defs->{lc $tag}->{class},
580             single => $defs->{lc $tag}->{single},
581             in_url => $in_url,
582             type => 'classic',
583             });
584 335         5979 my $success = 1;
585 335   100     818 my $nested_url = $in_url && $open->get_class eq 'url';
586             {
587 335         422 my $last = $opened[-1];
  335         448  
588 335 100 100     1154 if ($last and not $last->get_close and not $close) {
      100        
589 24         228 $self->_finish_tag($last, '');
590             # tag which should not have closing tag
591 24         30 pop @opened;
592 24         48 $callback_found_tag->($last);
593             }
594             }
595 335 100 66     1782 if ($open->get_single && !$nested_url) {
    100          
596 3         26 $self->_finish_tag($open, '');
597 3         7 $callback_found_tag->($open);
598             }
599             elsif (!$nested_url) {
600 331         2164 push @opened, $open;
601 331         601 my $def = $defs->{lc $tag};
602             #warn __PACKAGE__.':'.__LINE__.": $tag $def\n";
603 331         499 my $parse = $def->{parse};
604 331 100       581 if ($parse) {
605 460         1792 $current_open_re = join '|', map {
606 287         440 quotemeta $_->get_name
607             } @opened;
608             }
609             else {
610             #warn __PACKAGE__.':'.__LINE__.": noparse, find content\n";
611             # just search for closing tag
612 44 100       670 if ($after =~ s# (.*?) (\[ / $tag \]) ##ixs) {
613 39         103 my $content = $1;
614 39         84 my $end = $2;
615             #warn __PACKAGE__.':'.__LINE__.": CONTENT $content\n";
616 39         66 my $finished = pop @opened;
617 39         181 $finished->set_content([$content]);
618 39         307 $self->_finish_tag($finished, $end);
619 39         95 $callback_found_tag->($finished);
620             }
621             else {
622             #warn __PACKAGE__.':'.__LINE__.": nope '$after'\n";
623             }
624             }
625             }
626             else {
627 1         11 $callback_found_text->($open->get_start);
628             }
629              
630             }
631             else {
632             # unclosed tag
633 6         19 $callback_found_text->("[$tag$attr_string$end");
634             }
635             }
636             elsif ($tag) {
637             #warn __PACKAGE__.':'.__LINE__.": end\n";
638 1         4 $callback_found_text->("[$tag");
639             }
640 501         3969 $text = $after;
641             #sleep 1;
642             #warn __PACKAGE__.':'.__LINE__.$".Data::Dumper->Dump([\@tags], ['tags']);
643             }
644             # warn __PACKAGE__.':'.__LINE__.$".Data::Dumper->Dump([\@opened], ['opened']);
645 192 100       643 if ($self->get_close_open_tags) {
646 8         59 while (my $opened = pop @opened) {
647 11         67 $self->_add_error('unclosed', $opened);
648 11         78 $self->_finish_tag($opened, '[/' . $opened->get_name . ']', 1);
649 11         27 $callback_found_tag->($opened);
650             }
651             }
652             else {
653 184         1216 while (my $opened = shift @opened) {
654 11         43 my @text = $opened->_reduce;
655 11         102 push @tags, @text;
656             }
657             }
658 192 50       423 if ($scalar_util) {
659 192         3599 Scalar::Util::weaken($callback_found_tag);
660             }
661             else {
662             # just to make sure no memleak if there's no Scalar::Util
663 0         0 undef $callback_found_tag;
664             }
665             #warn __PACKAGE__.':'.__LINE__.": !!!!!!!!!!!! left text: '$text'\n";
666             #warn __PACKAGE__.':'.__LINE__.$".Data::Dumper->Dump([\@tags], ['tags']);
667 192         1411 my $tree = $self->new_tag({
668             name => '',
669             content => [@tags],
670             start => '',
671             class => 'block',
672             attr => [[]],
673             });
674 192         3188 $tree->_init_info({});
675 192         1708 return $tree;
676             }
677              
678             sub new_tag {
679 535     535 1 803 my $self = shift;
680 535         1903 Parse::BBCode::Tag->new(@_)
681             }
682              
683             sub _add_error {
684 25     25   62 my ($self, $error, $tag) = @_;
685 25   100     77 my $errors = $self->get_error || {};
686 25         177 push @{ $errors->{$error} }, $tag;
  25         80  
687 25         88 $self->set_error($errors);
688             }
689              
690             sub error {
691 11     11 1 6300 my ($self, $type) = @_;
692 11   100     35 my $errors = $self->get_error || {};
693 11 100 66     130 if ($type and $errors->{$type}) {
    100          
694 3         9 return $errors->{$type};
695             }
696             elsif (keys %$errors) {
697 6         18 return $errors;
698             }
699 2         7 return 0;
700             }
701              
702             sub render {
703 192     192 1 181955 my ($self, $text, $params) = @_;
704 192 100       616 if (@_ < 2) {
705 1         272 croak ("Missing input - Usage: \$parser->render(\$text)");
706             }
707             #warn __PACKAGE__.':'.__LINE__.": @_\n";
708             #sleep 2;
709 191         719 my $tree = $self->parse($text, $params);
710 191         550 my $out = $self->render_tree($tree, $params);
711 191 100       762 if ($self->get_error) {
712 18         145 $self->set_tree($tree);
713             }
714 191         2658 return $out;
715             }
716              
717             sub render_tree {
718 191     191 1 287 my ($self, $tree, $params) = @_;
719 191   100     773 $params ||= {};
720 191         613 $self->set_params($params);
721 191         1274 my $rendered = $self->_render_tree($tree);
722 191         589 $self->set_params(undef);
723 191         1642 return $rendered;
724             }
725              
726             sub _render_tree {
727 1023     1023   1664 my ($self, $tree, $outer, $info) = @_;
728 1023         1215 my $out = '';
729 1023   100     2814 $info ||= {
730             stack => [],
731             tags => {},
732             classes => {},
733             };
734 1023         2619 my $defs = $self->get_tags;
735 1023 100       4801 if (ref $tree) {
736 502         1207 my $name = $tree->get_name;
737 502         2035 my %tags = %{ $info->{tags} };
  502         1574  
738 502         939 $tags{$name}++;
739 502         581 my @stack = @{ $info->{stack} };
  502         1168  
740 502         776 push @stack, $name;
741 502         547 my %classes = %{ $info->{classes} };
  502         1231  
742 502   50     1342 $classes{ $tree->get_class || '' }++;
743 502         4152 my %info = (
744             tags => \%tags,
745             stack => [@stack],
746             classes => \%classes,
747             );
748 502         1041 my $code = $defs->{$name}->{code};
749 502         757 my $parse = $defs->{$name}->{parse};
750 502   50     1407 my $attr = $tree->get_attr || [];
751 502         2841 $attr = $attr->[0]->[0];
752 502         1156 my $content = $tree->get_content;
753 502         1784 my $fallback;
754 502         747 my $string = '';
755 502 100 100     1215 if (($tree->get_type || 'classic') eq 'classic') {
756 495 100 100     4056 $fallback = (defined $attr and length $attr) ? $attr : $content;
757             }
758             else {
759 7         40 $fallback = $attr;
760 7 100       16 $string = @$content ? '' : $attr;
761             }
762 502 100       1201 if (ref $fallback) {
763             # we have recursive content, we don't want that in
764             # an attribute
765 694         1852 $fallback = join '', grep {
766 401         665 not ref $_
767             } @$fallback;
768             }
769 502 100 50     1306 if ($self->get_strip_linebreaks and ($tree->get_class || '') eq 'block') {
      100        
770 290 100 100     4746 if (@$content == 1 and not ref $content->[0] and defined $content->[0]) {
    100 66        
771 75         238 $content->[0] =~ s/^\r?\n//;
772 75         143 $content->[0] =~ s/\r?\n\z//;
773             }
774             elsif (@$content > 1) {
775 175 100 66     1203 if (not ref $content->[0] and defined $content->[0]) {
776 56         147 $content->[0] =~ s/^\r?\n//;
777             }
778 175 100 66     877 if (not ref $content->[-1] and defined $content->[-1]) {
779 155         321 $content->[-1] =~ s/\r?\n\z//;
780             }
781             }
782             }
783 502 100 100     4404 if (not exists $defs->{$name}->{parse} or $parse) {
784 462         1346 for my $c (@$content) {
785 823         2162 $string .= $self->_render_tree($c, $tree, \%info);
786             }
787             }
788             else {
789 40         119 $string = join '', @$content;
790             }
791 502 100       1056 if ($code) {
792 311         889 my $o = $code->($self, $attr, \$string, $fallback, $tree, \%info);
793 311         1771 $out .= $o;
794             }
795             else {
796 191         833 $out .= $string;
797             }
798             }
799             else {
800             #warn __PACKAGE__.':'.__LINE__.": ==== $tree\n";
801 521         1066 $out .= $self->_render_text($outer, $tree, $info);
802             }
803 1023         5075 return $out;
804             }
805              
806              
807             sub escape_html {
808 533     533 1 1190 my ($str) = @_;
809 533 100       1020 return '' unless defined $str;
810 529         764 $str =~ s/&/&/g;
811 529         670 $str =~ s/"/"/g;
812 529         620 $str =~ s/'/'/g;
813 529         655 $str =~ s/>/>/g;
814 529         648 $str =~ s/
815 529         1380 return $str;
816             }
817              
818             sub parse_attributes {
819 337     337 1 1088 my ($self, %args) = @_;
820 337         502 my $text = $args{text};
821 337         573 my $tagname = $args{tag};
822 337         1506 my $attribute_quote = $self->get_attribute_quote;
823 337         1635 my $attr_string = '';
824 337         552 my $attributes = [];
825 337 100 100     965 if (
      100        
826             ($self->get_direct_attribute and $$text =~ s/^(=[^\]]*)?]//)
827             or
828             ($$text =~ s/^( [^\]]*)?\]//)
829             ) {
830 333         4126 my $attr = $1;
831 333         479 my $end = ']';
832 333 100       763 $attr = '' unless defined $attr;
833 333         421 $attr_string = $attr;
834 333 100       677 unless (length $attr) {
835 235         1173 return (1, [], $attr_string, $end);
836             }
837 98 100       287 if ($self->get_direct_attribute) {
838 97         693 $attr =~ s/^=//;
839             }
840 98 100 100     324 if ($self->get_strict_attributes and not length $attr) {
841 1         12 return (0, [], $attr_string, $end);
842             }
843 97         737 my @array;
844 97 100       223 if (length($attribute_quote) == 1) {
845 96 50       1149 if ($attr =~ s/^(?:$attribute_quote(.+?)$attribute_quote(?:\s+|$)|(.*?)(?:\s+|$))//) {
846 96 100       295 my $val = defined $1 ? $1 : $2;
847 96         258 push @array, [$val];
848             }
849 96         916 while ($attr =~ s/^([a-zA-Z0-9_]+)=(?:$attribute_quote(.+?)$attribute_quote(?:\s+|$)|(.*?)(?:\s+|$))//) {
850 9         22 my $name = $1;
851 9 100       33 my $val = defined $2 ? $2 : $3;
852 9         106 push @array, [$name, $val];
853             }
854             }
855             else {
856 1 50       10 if ($attr =~ s/^(?:(["'])(.+?)\1|(.*?)(?:\s+|$))//) {
857 1 50       5 my $val = defined $2 ? $2 : $3;
858 1         3 push @array, [$val];
859             }
860 1         9 while ($attr =~ s/^([a-zA-Z0-9_]+)=(?:(["'])(.+?)\2|(.*?)(?:\s+|$))//) {
861 1         4 my $name = $1;
862 1 50       5 my $val = defined $3 ? $3 : $4;
863 1         5 push @array, [$name, $val];
864             }
865             }
866 97 50 100     307 if ($self->get_strict_attributes and length $attr and $attr =~ tr/ //c) {
      66        
867 1         19 return (0, [], $attr_string, $end);
868             }
869 96         885 $attributes = [@array];
870 96         515 return (1, $attributes, $attr_string, $end);
871             }
872 4         75 return (0, $attributes, $attr_string, '');
873             }
874              
875             # TODO add callbacks
876             sub _finish_tag {
877 312     312   641 my ($self, $tag, $end, $auto_closed) = @_;
878             #warn __PACKAGE__.':'.__LINE__.": _finish_tag(@_)\n";
879             #warn __PACKAGE__.':'.__LINE__.$".Data::Dumper->Dump([\$tag], ['tag']);
880 312 50       838 unless ($tag->get_finished) {
881 312         1918 $tag->set_end($end);
882 312         2189 $tag->set_finished(1);
883 312   100     2687 $tag->set_auto_closed($auto_closed || 0);
884             }
885 312         1728 return 1;
886             }
887              
888             __END__