File Coverage

blib/lib/Parse/BBCode.pm
Criterion Covered Total %
statement 513 517 99.2
branch 198 212 93.4
condition 102 125 81.6
subroutine 31 31 100.0
pod 10 10 100.0
total 854 895 95.4


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