File Coverage

blib/lib/HTML/Acid.pm
Criterion Covered Total %
statement 246 247 99.6
branch 75 76 98.6
condition 21 21 100.0
subroutine 32 32 100.0
pod 3 3 100.0
total 377 379 99.4


line stmt bran cond sub pod time code
1             package HTML::Acid;
2 8     8   280778 use base HTML::Parser;
  8         22  
  8         7105  
3              
4 8     8   45374 use warnings;
  8         19  
  8         351  
5 8     8   134 use strict;
  8         86  
  8         279  
6 8     8   47 use Carp;
  8         14  
  8         820  
7 8     8   10272 use Readonly;
  8         20845  
  8         449  
8 8     8   6996 use HTML::Acid::Buffer;
  8         27  
  8         293  
9 8     8   18904 use String::Dirify qw(dirify);
  8         13831  
  8         791  
10              
11 8     8   203 use version; our $VERSION = qv('0.0.3');
  8         14  
  8         56  
12              
13             # Module implementation here
14              
15             Readonly my %START_HANDLERS => (
16             img=>\&_img_start,
17             h1=>\&_h_start,
18             h2=>\&_h_start,
19             h3=>\&_h_start,
20             h4=>\&_h_start,
21             h5=>\&_h_start,
22             h6=>\&_h_start,
23             p=>\&_p_start,
24             a=>\&_a_start,
25             );
26             Readonly my %END_HANDLERS => (
27             h1=>\&_h_end,
28             h2=>\&_h_end,
29             h3=>\&_h_end,
30             h4=>\&_h_end,
31             h5=>\&_h_end,
32             h6=>\&_h_end,
33             p=>\&_p_end,
34             a=>\&_a_end,
35             );
36              
37             Readonly my $URL_REGEX => qr{
38             \A # start of string
39             / # internal URLs only by default
40             \w # at least one normal character
41             [\w\-/]* #
42             (?:\.\w{1,5})? # optional file extension
43             (?:\#[\w\-]+)? # optional anchor
44             \z # end of string
45             }xms;
46              
47             Readonly my $ALT_REGEX => qr{
48             \A # start of string
49             [\w\s\.\,]+ #
50             \z # end of string
51             }xms;
52              
53             sub new {
54 4     4 1 13595 my $class = shift;
55 4         27 my %args = @_;
56 4         18 my $tag_hierarchy = delete $args{tag_hierarchy};
57 4 100       33 if (not $tag_hierarchy) {
58 1         9 $tag_hierarchy = $class->default_tag_hierarchy;
59             }
60 4         11 my $url_regex = delete $args{url_regex};
61 4 100       16 if (not $url_regex) {
62 1         5 $url_regex = $URL_REGEX;
63             }
64              
65             # Configue HTML::Parser options
66 4         29 my @tags = keys %$tag_hierarchy;
67 4         110 my $self = HTML::Parser->new(
68             api_version => 3,
69             empty_element_tags=>1,
70             strict_comment=>1,
71             utf8_mode=>1,
72             handlers => {
73             text=>['_text_process', 'self,dtext'],
74             start=>['_start_process', 'self,tagname,attr'],
75             end=>['_end_process', 'self,tagname'],
76             end_document=>['_end_document', 'self'],
77             start_document=>['_reset', 'self'],
78             },
79             ignore_elements=>['script','style'],
80             report_tags=>[@tags, 'br'],
81             );
82              
83 4         653 bless $self, $class;
84              
85             # Calculate depths and normalize hierarchy
86 4         38 $self->{_acid_depths} = {''=>0};
87 4         14 $self->{_acid_tag_hierarchy} = {};
88 4         39 $self->{_acid_preferred_parent} = {};
89 4         13 my %pending = ();
90 4         21 $self->_process_tags(\%pending, $tag_hierarchy, @tags);
91              
92 4         11 $self->{_acid_url_regex} = $url_regex;
93 4         16 foreach my $arg (keys %args) {
94 10         35 $self->{"_acid_$arg"} = $args{$arg};
95             }
96              
97 4         28 return $self;
98             }
99              
100             sub _process_tags {
101 37     37   154 my ($self, $pending, $tag_hierarchy, @tags) = @_;
102              
103             TAG:
104 37         63 foreach my $tag (@tags) {
105              
106             # Get a list of parents for this tag
107 16         48 my @parents = (ref $tag_hierarchy->{$tag} eq 'ARRAY')
108 45 100       140 ? @{$tag_hierarchy->{$tag}}
109             : ( $tag_hierarchy->{$tag} );
110              
111             # Get the maximum depth of the parents
112             # If this is not possible dump the problem tag, parent on the
113             # pending queue
114 45         56 my $depth = undef;
115 45         159 my $preferred_parent = undef;
116             PARENT:
117 45         64 foreach my $p (@parents) {
118 55 100       128 if (exists $self->{_acid_depths}->{$p}) {
119 43         82 my $p_depth = $self->{_acid_depths}->{$p};
120 43 100       85 if (not defined $depth) {
    50          
121 35         43 $depth = $p_depth;
122 35         48 $preferred_parent = $p;
123             }
124             elsif ($p_depth < $depth) {
125 0         0 $preferred_parent = $p;
126             }
127             else {
128 8         12 $depth = $p_depth;
129             }
130 43         160 $self->{_acid_tag_hierarchy}->{$tag}->{$p} = 1;
131             }
132             else {
133 12         33 _push_tag($pending, $p, $tag);
134 12         33 next TAG;
135             }
136             }
137 33         148 $self->{_acid_depths}->{$tag} = $depth+1;
138 33         67 $self->{_acid_preferred_parent}->{$tag} = $preferred_parent;
139              
140             # If we get this far we know the depth of $tag.
141             # So we can go back and look at all the tags
142             # that were waiting for $tag.
143 33         83 my @heldback = _pop_tag($pending, $tag);
144 33         112 $self->_process_tags($pending, $tag_hierarchy, @heldback);
145             }
146 37         97 return;
147             }
148              
149             sub _push_tag {
150 12     12   18 my $pending = shift;
151 12         47 my $parent = shift;
152 12         18 my $tag = shift;
153 12 100       34 if ($pending->{$parent}) {
154 4         27 push @{$pending->{$parent}}, $tag;
  4         12  
155             }
156             else {
157 8         22 $pending->{$parent} = [$tag];
158             }
159 12         22 return;
160             }
161              
162             sub _pop_tag {
163 33     33   44 my $pending = shift;
164 33         51 my $parent = shift;
165 33 100       115 return if not exists $pending->{$parent};
166 8         16 my $array = delete $pending->{$parent};
167 8         29 return @$array;
168             }
169              
170             sub _text_process {
171 6649     6649   10537 my $self = shift;
172 6649         19478 my $dtext = shift;
173 6649         19685 my $text_nontrivial = $dtext =~ /\S/;
174              
175             # New text clears a single
tag
176 6649 100 100     30314 if ($self->{_acid_br} and $text_nontrivial) {
177 112         187 $self->{_acid_br} = 0;
178             }
179              
180             # To add to the buffer unhindered we must not be in the
181             # start state.
182 6649 100 100     45932 if ($self->_get_state eq '' and $text_nontrivial) {
183 60         161 $self->_start_process('p', {});
184             }
185              
186 6649         12749 my $otext = $dtext;
187 6649 100       20910 if ($self->{_acid_text_manip}) {
188 5002         5454 $otext = &{$self->{_acid_text_manip}}($dtext);
  5002         13625  
189             }
190 6649         147422 $self->_buffer($otext);
191 6649         62358 return;
192             }
193              
194             sub _start_process {
195 3973     3973   5707 my $self = shift;
196 3973         13248 my $tagname = shift;
197 3973         4875 my $attr = shift;
198              
199 3973         8115 my $actual_state = $self->_get_state;
200              
201             # Two br tags in a row means 'new paragraph'.
202 3973 100       10874 if ($tagname eq 'br') {
203 256 100       1201 return if $actual_state ne 'p';
204 128 100       300 if ($self->{_acid_br}) {
205 8         16 $self->{_acid_br} = 0;
206 8         21 $self->_end_process('p');
207             }
208             else {
209 120         387 $self->{_acid_br} = 1;
210             }
211 128         1920 return;
212             }
213 3717         5495 $self->{_acid_br} = 0;
214              
215             # To call _start_process unhindered
216             # the parent tag of $tagname must be the
217             # current state.
218 3717 100       12797 if (not exists $self->{_acid_tag_hierarchy}->{$tagname}->{$actual_state}) {
219 454         943 my $required_state = $self->{_acid_preferred_parent}->{$tagname};
220 454         780 my $required_depth = $self->{_acid_depths}->{$tagname};
221 454         730 my $actual_depth = $self->{_acid_depths}->{$actual_state};
222 454 100       971 if ($actual_depth >= $required_depth) {
223 290         530 $self->_end_process($actual_state);
224             }
225 454 100       1141 if ($required_state ne '') {
226 170         455 $self->_start_process($required_state, {});
227             }
228             }
229              
230 3717 100       16661 if (exists $START_HANDLERS{$tagname}) {
231 2578         27525 my $callback = $START_HANDLERS{$tagname};
232 2578         22688 $self->$callback($tagname,$attr);
233             }
234             else {
235 1139         11070 $self->_buffer("<$tagname>");
236             }
237              
238             # State shifts to the current tag.
239             # The 'img' end tag does not get called in some cases.
240 3717 100       14776 $self->_push_state($tagname) if $tagname ne 'img';
241              
242 3717         24074 return;
243             }
244              
245             sub _end_process {
246 4027     4027   6499 my $self = shift;
247 4027         4823 my $tagname = shift;
248 4027 100       10893 return if $tagname eq 'br';
249              
250             # To call _start_process unhindered
251             # $tagname must be the current state.
252 3771         9421 my $actual_state = $self->_get_state;
253 3771 100       9783 if ($tagname ne $actual_state) {
254 432         900 my $tag_depth = $self->{_acid_depths}->{$tagname};
255 432         722 my $actual_depth = $self->{_acid_depths}->{$actual_state};
256 432 100       2277 return if $tag_depth >= $actual_depth;
257 116         279 $self->_end_process($actual_state);
258             }
259              
260 3455 100       22736 if (exists $END_HANDLERS{$tagname}) {
261 2316         65027 my $callback = $END_HANDLERS{$tagname};
262 2316         14711 $self->$callback($tagname);
263             }
264             else {
265 1139         10562 $self->_buffer("");
266             }
267              
268             # State shifts to the parent tag.
269 3455         10296 $self->_pop_state;
270              
271 3455         41145 return;
272             }
273              
274             sub _end_document {
275 297     297   5259 my $self = shift;
276              
277             # We want to end in the start state.
278 297 100       598 if ($self->_get_state ne '') {
279 94         201 $self->_end_process('p');
280 94         190 $self->_buffer("\n");
281             }
282              
283 297         919 return;
284             }
285              
286             sub _img_start {
287 262     262   389 my $self = shift;
288 262         382 my $tagname = shift;
289 262         323 my $attr = shift;
290              
291 262 100       783 return if not my $alt = $attr->{alt};
292 258         662 my $src = $self->_url($attr->{src});
293 258   100     821 my $width = $attr->{width} || $self->{_acid_img_width_default};
294 258   100     680 my $height = $attr->{height} || $self->{_acid_img_height_default};
295 258 100 100     2393 if ($src and $height and $width and my $title = $attr->{title}) {
    100 100        
    100 100        
296 236         1237 $self->_buffer("\"$alt\"
297             ."title=\"$title\" width=\"$width\" />");
298             }
299             elsif ($self->{_acid_text_manip}) {
300 9         17 my $otext = $alt;
301 9         14 $otext = &{$self->{_acid_text_manip}}($alt);
  9         31  
302 9         244 $self->_buffer($self->_text_container($otext));
303             }
304             elsif ($alt =~ $ALT_REGEX) {
305 12         165 $self->_buffer($self->_text_container($alt));
306             }
307 258         558 return;
308             }
309              
310             sub _text_container {
311 21     21   33 my $self = shift;
312 21         35 my $text = shift;
313 21 100       56 if ($self->{_acid_text_container}) {
314 3         4 $text = &{$self->{_acid_text_container}}($text);
  3         12  
315             }
316             else {
317 18         48 $text = " $text ";
318             }
319 21         80 return $text;
320             }
321              
322             sub _url {
323 545     545   693 my $self = shift;
324 545         732 my $url = shift;
325 545 100       1166 return if not $url;
326 537 100       5440 return if $url !~ $self->{_acid_url_regex};
327 512         1371 return $url;
328             }
329              
330             sub _a_start {
331 319     319   474 my $self = shift;
332 319         427 my $tagname = shift;
333 319         378 my $attr = shift;
334 319         4647 my $buffer = HTML::Acid::Buffer->new($tagname);
335 319         1508 $buffer->set_attr($attr);
336 319         508 unshift @{$self->{_acid_buffer}}, $buffer;
  319         772  
337 319         600 return;
338             }
339              
340             sub _a_end {
341 319     319   505 my $self = shift;
342 319         501 my $tagname = shift;
343 319         361 my $buffer = shift @{$self->{_acid_buffer}};
  319         683  
344 319         1057 my $attr = $buffer->get_attr;
345 319         1367 my $text = $buffer->state;
346 319 100       793 return if not $text;
347 311 100       1593 return if $text !~ /\S/;
348 287         932 my $href = $self->_url($attr->{href});
349 287 100       613 if (not $href) {
350 17         67 $self->_buffer(" $text ");
351 17         67 return;
352             }
353 270         769 my $new_attr = {href=>$href};
354 270 100       787 if ($attr->{title}) {
355 246         542 $new_attr->{title} = $attr->{title};
356             }
357 270         820 $buffer->set_attr($new_attr);
358 270         667 $self->_buffer($buffer->stop);
359 270         1244 return;
360             }
361              
362             sub _h_start {
363 768     768   13368 my $self = shift;
364 768         1023 my $tagname = shift;
365 768         867 my $attr = shift;
366 768         2332 my $buffer = HTML::Acid::Buffer->new($tagname);
367 768         6989 $buffer->set_attr($attr);
368 768         1237 unshift @{$self->{_acid_buffer}}, $buffer;
  768         2069  
369 768         1497 return;
370             }
371              
372             sub _h_end {
373 768     768   1061 my $self = shift;
374 768         1032 my $tagname = shift;
375 768         810 my $buffer = shift @{$self->{_acid_buffer}};
  768         7250  
376 768         2350 my $attr = $buffer->get_attr;
377 768         2826 my $text = $buffer->state;
378 768 100       1888 return if not $text;
379 752 100       2054 my $id = exists $attr->{id} ? $attr->{id} : dirify($text,'-');
380 752         6403 $buffer->set_attr({id=>$id});
381 752         2099 $self->_buffer($buffer->stop);
382 752         2796 return;
383             }
384              
385             sub _p_start {
386 1229     1229   1561 my $self = shift;
387 1229         1457 my $tagname = shift;
388 1229         1207 unshift @{$self->{_acid_buffer}}, HTML::Acid::Buffer->new($tagname);
  1229         4319  
389 1229         2401 return;
390             }
391              
392             sub _p_end {
393 1229     1229   1412 my $self = shift;
394 1229         10202 my $tagname = shift;
395 1229         1439 my $buffer = shift @{$self->{_acid_buffer}};
  1229         28842  
396 1229 100       7666 if ($buffer->state =~ /\S/) {
397 1184         4109 $self->_buffer($buffer->stop);
398             }
399 1229         3611 return;
400             }
401              
402             sub _buffer {
403 11501     11501   24225 my $self = shift;
404 11501         17575 my $text = shift;
405 11501         44449 $self->{_acid_buffer}->[0]->add($text);
406 11501         34818 return;
407             }
408              
409             sub _reset {
410 297     297   493 my $self = shift;
411 297         2037 $self->{_acid_buffer} = [HTML::Acid::Buffer->new];
412 297         1194 $self->{_acid_state} = [""];
413 297         665 $self->{_acid_br} = 0;
414 297         3410 return;
415             }
416              
417             sub _get_state {
418 14690     14690   23072 my $self = shift;
419 14690         133668 return $self->{_acid_state}->[0];
420             }
421              
422             sub _push_state {
423 3455     3455   8416 my $self = shift;
424 3455         4377 my $state = shift;
425 3455         3607 unshift @{$self->{_acid_state}}, $state;
  3455         12624  
426 3455         6306 return;
427             }
428              
429             sub _pop_state {
430 3455     3455   4155 my $self = shift;
431 3455         3668 return shift @{$self->{_acid_state}};
  3455         8625  
432             }
433              
434             sub burn {
435 297     297 1 925845 my $self = shift;
436 297         571 my $text = shift;
437 297         2747 $self->parse($text);
438 297         4183 $self->eof;
439 297         1024 return $self->{_acid_buffer}->[0]->stop;
440             }
441              
442             sub default_tag_hierarchy {
443             return {
444 1     1 1 10 h3 => '',
445             p => '',
446             a => 'p',
447             img => 'p',
448             em => 'p',
449             strong => 'p',
450             };
451             }
452              
453             1; # Magic true value required at end of module
454             __END__