File Coverage

blib/lib/HTML/Lint/Parser.pm
Criterion Covered Total %
statement 191 192 99.4
branch 71 72 98.6
condition 26 30 86.6
subroutine 27 27 100.0
pod 2 2 100.0
total 317 323 98.1


line stmt bran cond sub pod time code
1             package HTML::Lint::Parser;
2              
3 39     39   53137 use warnings;
  39         71  
  39         1019  
4 39     39   179 use strict;
  39         54  
  39         774  
5              
6 39     39   17538 use HTML::Parser 3.20;
  39         180467  
  39         1112  
7 39     39   14843 use HTML::Tagset 3.03;
  39         40807  
  39         1088  
8              
9 39     39   575 use HTML::Lint::Error ();
  39         71  
  39         971  
10 39     39   14334 use HTML::Lint::HTML4 qw( %isKnownAttribute %isRequired %isNonrepeatable %isObsolete );
  39         104  
  39         5708  
11 39     39   241 use HTML::Entities qw( %char2entity %entity2char );
  39         70  
  39         2755  
12              
13 39     39   198 use parent 'HTML::Parser';
  39         56  
  39         135  
14              
15             =head1 NAME
16              
17             HTML::Lint::Parser - Parser for HTML::Lint. No user-serviceable parts inside.
18              
19             =head1 VERSION
20              
21             Version 2.30
22              
23             =cut
24              
25             our $VERSION = '2.30';
26              
27             =head1 SYNOPSIS
28              
29             See L for all the gory details.
30              
31             =head1 METHODS
32              
33             =head2 new( $gripe )
34              
35             Constructor for the main parsing object. The I<$gripe> argument
36             is a coderef to a function that can handle errors from the parser.
37             It is only ever (so far) C.
38              
39             =cut
40              
41             sub new {
42 47     47 1 109 my $class = shift;
43 47         86 my $gripe = shift;
44              
45 47         685 my $self =
46             HTML::Parser->new(
47             api_version => 3,
48             start_document_h => [ \&_start_document, 'self' ],
49             end_document_h => [ \&_end_document, 'self,line,column' ],
50             start_h => [ \&_start, 'self,tagname,line,column,@attr' ],
51             end_h => [ \&_end, 'self,tagname,line,column,tokenpos,@attr' ],
52             comment_h => [ \&_comment, 'self,tagname,line,column,text' ],
53             text_h => [ \&_text, 'self,text' ],
54             strict_names => 0,
55             empty_element_tags => 1,
56             attr_encoded => 1,
57             );
58 47         4663 bless $self, $class;
59              
60 47         211 $self->{_gripe} = $gripe;
61 47         121 $self->{_stack} = [];
62 47         99 $self->{_directives} = {};
63              
64 47         133 return $self;
65             }
66              
67             =head2 $parser->gripe( $errorcode, [ arg1=>val1, ...] )
68              
69             Calls the passed-in gripe function.
70              
71             If a given directive has been set to turn off a given message, then
72             the parent gripe never gets called.
73              
74             =cut
75              
76             sub gripe {
77 96     96 1 134 my $self = shift;
78 96         127 my $errorcode = shift;
79              
80 96 100       218 if ( $self->_displayable( $errorcode ) ) {
81 91         215 $self->{_gripe}->( $errorcode, @_ );
82             }
83              
84 96         200 return;
85             }
86              
87             sub _displayable {
88 96     96   135 my $self = shift;
89 96         128 my $errorcode = shift;
90              
91 96         137 my $directives = $self->{_directives};
92 96 100       199 if ( not defined $directives->{$errorcode} ) {
93 87         215 return 1;
94             }
95             else {
96 9         16 return $directives->{$errorcode};
97             }
98             }
99              
100             sub _start_document {
101 47     47   372 return;
102             }
103              
104             sub _end_document {
105 44     44   111 my ($self,$line,$column) = @_;
106              
107 44         231 for my $tag ( sort keys %isRequired ) {
108 176 100       359 if ( !$self->{_first_seen}->{$tag} ) {
109 29         52 $self->gripe( 'doc-tag-required', tag => $tag );
110             }
111             }
112              
113 44         132 return;
114             }
115              
116             sub _start {
117 528     528   1165 my ($self,$tag,$line,$column,@attr) = @_;
118              
119 528         633 $self->{_line} = $line;
120 528         580 $self->{_column} = $column;
121              
122 528         774 my $validattr = $isKnownAttribute{ $tag };
123 528 100       868 if ( $validattr ) {
124 525         529 my %seen;
125 525         572 my $i = 0;
126 525         1023 while ( $i < @attr ) {
127 630         1033 my ($attr,$val) = @attr[$i++,$i++];
128 630 100       1128 if ( $seen{$attr}++ ) {
129 1         4 $self->gripe( 'attr-repeated', tag => $tag, attr => $attr );
130             }
131              
132 630 100       960 if ( !$validattr->{$attr} ) {
133 4         11 $self->gripe( 'attr-unknown', tag => $tag, attr => $attr );
134             }
135              
136 630         877 $self->_entity($val, 'attr');
137             } # while attribs
138             }
139             else {
140 3         16 $self->gripe( 'elem-unknown', tag => $tag );
141             }
142 528 100       1340 $self->_element_push( $tag ) unless $HTML::Tagset::emptyElement{ $tag };
143              
144 528 100       845 if ( my $where = $self->{_first_seen}{$tag} ) {
145 291 100       432 if ( $isNonrepeatable{$tag} ) {
146             $self->gripe( 'elem-nonrepeatable',
147             tag => $tag,
148 1         2 where => HTML::Lint::Error::where( @{$where} )
  1         4  
149             );
150             }
151             }
152             else {
153 237         459 $self->{_first_seen}{$tag} = [$line,$column];
154             }
155              
156             # Call any other overloaded func
157 528         789 my $tagfunc = "_start_$tag";
158 528 100       1893 if ( $self->can($tagfunc) ) {
159 53         115 $self->$tagfunc( $tag, @attr );
160             }
161              
162 528         1795 return;
163             }
164              
165             sub _text {
166 761     761   1299 my ($self,$text) = @_;
167              
168 761         1347 $self->_entity($text, 'text');
169              
170 761         3056 return;
171             }
172              
173             sub _entity {
174 1391     1391   1842 my ($self,$text,$type) = @_;
175              
176 1391 100       2106 if ( not $self->{_entity_lookup} ) {
177 44         5433 my @entities = sort keys %HTML::Entities::entity2char;
178             # Strip his semicolons
179 44         5928 s/;$// for @entities;
180 44         164 $self->{_entity_lookup} = { map { ($_,1) } @entities };
  11132         16953  
181             }
182              
183 1391         3554 while ( $text =~ /([^\x09\x0A\x0D -~])/g ) {
184 4         9 my $bad = $1;
185             $self->gripe(
186             $type . '-use-entity',
187             char => sprintf( '\x%02lX', ord($bad) ),
188 4   66     33 entity => $char2entity{ $bad } || '&#' . ord($bad) . ';',
189             );
190             }
191              
192 1391         2310 while ( $text =~ /&([^ ;]*;?)/g ) {
193 90         245 my $match = $1;
194              
195 90 100       371 if ( $match eq '' ) {
    100          
    100          
    100          
196 4         18 $self->gripe( $type . '-use-entity', char => '&', entity => '&' );
197             }
198             elsif ( $match !~ m/;$/ ) {
199 6 100 100     36 if ( exists $self->{_entity_lookup}->{$match}
      66        
200             || $match =~ m/^#(\d+)$/ || $match =~ m/^#x[\dA-F]+$/i) {
201 5         22 $self->gripe( $type . '-unclosed-entity', entity => "&$match;" );
202             }
203             else {
204 1         4 $self->gripe( $type . '-unknown-entity', entity => "&$match" );
205             }
206             }
207             elsif ( $match =~ m/^#(\d+);$/ ) {
208             # All numeric entities are OK. We used to check that they were in a given range.
209             }
210             elsif ( $match =~ m/^#x([\dA-F]+);$/i ) {
211             # All hex entities OK. We used to check that they were in a given range.
212             }
213             else {
214 65         153 $match =~ s/;$//;
215 65 100       206 if ( !exists $self->{_entity_lookup}->{$match} ) {
216 4         18 $self->gripe( $type . '-unknown-entity', entity => "&$match;" );
217             }
218             }
219             }
220              
221 1391         2024 return;
222             }
223              
224             sub _comment {
225 29     29   114 my ($self,$tagname,$line,$column,$text) = @_;
226              
227             # Look for the html-lint directives
228 29 100       112 if ( $tagname =~ m/^\s*html-lint\s*(.+)\s*$/ ) {
229 14         30 my $text = $1;
230              
231 14         35 my @commands = split( /\s*,\s*/, $text );
232              
233 14         21 for my $command ( @commands ) {
234 15         48 my ($directive,$value) = split( /\s*:\s*/, $command, 2 );
235 15         57 _trim($_) for ($directive,$value);
236              
237 15 100 100     57 if ( ($directive ne 'all') &&
238             ( not exists $HTML::Lint::Error::errors{ $directive } ) ) {
239 1         5 $self->gripe( 'config-unknown-directive',
240             directive => $directive,
241             where => HTML::Lint::Error::where($line,$column)
242             );
243 1         3 next;
244             }
245              
246 14         23 my $normalized_value = _normalize_value( $value );
247 14 100       27 if ( !defined($normalized_value) ) {
248 1         4 $self->gripe( 'config-unknown-value',
249             directive => $directive,
250             value => $value,
251             where => HTML::Lint::Error::where($line,$column)
252             );
253 1         2 next;
254             }
255              
256 13 100       26 if ( $directive eq 'all' ) {
257 3         21 for my $err ( keys %HTML::Lint::Error::errors ) {
258 63         68 $self->_set_directive( $err, $normalized_value );
259             }
260             }
261             else {
262 10         20 $self->_set_directive( $directive, $normalized_value );
263             }
264             }
265             }
266              
267 29         147 return;
268             }
269              
270             sub _set_directive {
271 73     73   70 my $self = shift;
272 73         66 my $which = shift;
273 73         64 my $what = shift;
274              
275 73         87 $self->{_directives}{$which} = $what;
276              
277 73         92 return;
278             }
279              
280              
281             sub _normalize_value {
282 14     14   21 my $what = shift;
283              
284 14         19 $what = _trim( $what );
285 14 100 100     69 return 1 if $what eq '1' || $what eq 'on' || $what eq 'true';
      100        
286 9 100 100     38 return 0 if $what eq '0' || $what eq 'off' || $what eq 'false';
      100        
287 1         2 return undef;
288             }
289              
290             sub _trim {
291 44     44   62 $_[0] =~ s/^\s+//;
292 44         69 $_[0] =~ s/\s+$//;
293              
294 44         74 return $_[0];
295             }
296              
297             sub _end { ## no critic ( Subroutines::ProhibitManyArgs ) I have no choice in what these args are.
298 391     391   1242 my ($self,$tag,$line,$column,$tokenpos,@attr) = @_;
299              
300 391         541 $self->{_line} = $line;
301 391         452 $self->{_column} = $column;
302              
303 391 100       774 if ( !$tokenpos ) {
    100          
304             # This is a dummy end event for something like .
305             # Do nothing.
306             }
307             elsif ( $HTML::Tagset::emptyElement{ $tag } ) {
308 1         3 $self->gripe( 'elem-empty-but-closed', tag => $tag );
309             }
310             else {
311 378 100       631 if ( $self->_in_context($tag) ) {
312 374         617 my @leftovers = $self->_element_pop_back_to($tag);
313 374         580 for ( @leftovers ) {
314 26         43 my ($tag,$line,$col) = @{$_};
  26         41  
315             $self->gripe( 'elem-unclosed', tag => $tag,
316             where => HTML::Lint::Error::where($line,$col) )
317 26 100       99 unless $HTML::Tagset::optionalEndTag{$tag};
318             } # for
319             }
320             else {
321 4         10 $self->gripe( 'elem-unopened', tag => $tag );
322             }
323             } # is empty element
324              
325             # Call any other overloaded func
326 391         646 my $tagfunc = "_end_$tag";
327 391 50       1312 if ( $self->can($tagfunc) ) {
328 0         0 $self->$tagfunc( $tag, $line );
329             }
330              
331 391         1482 return;
332             }
333              
334             sub _element_push {
335 401     401   497 my $self = shift;
336 401         592 for ( @_ ) {
337 401         411 push( @{$self->{_stack}}, [$_,$self->{_line},$self->{_column}] );
  401         1005  
338             } # while
339              
340 401         486 return;
341             }
342              
343             sub _find_tag_in_stack {
344 752     752   789 my $self = shift;
345 752         761 my $tag = shift;
346 752         790 my $stack = $self->{_stack};
347              
348 752         718 my $offset = @{$stack} - 1;
  752         962  
349 752         1143 while ( $offset >= 0 ) {
350 808 100       1249 if ( $stack->[$offset][0] eq $tag ) {
351 748         1222 return $offset;
352             }
353 60         100 --$offset;
354             } # while
355              
356 4         5 return;
357             }
358              
359             sub _element_pop_back_to {
360 374     374   414 my $self = shift;
361 374         417 my $tag = shift;
362              
363 374 100       490 my $offset = $self->_find_tag_in_stack($tag) or return;
364              
365 330         359 my @leftovers = splice( @{$self->{_stack}}, $offset + 1 );
  330         566  
366 330         343 pop @{$self->{_stack}};
  330         384  
367              
368 330         540 return @leftovers;
369             }
370              
371             sub _in_context {
372 378     378   423 my $self = shift;
373 378         441 my $tag = shift;
374              
375 378         580 my $offset = $self->_find_tag_in_stack($tag);
376 378         685 return defined $offset;
377             }
378              
379             # Overridden tag-specific stuff
380             sub _start_img { ## no critic ( Subroutines::ProhibitUnusedPrivateSubroutines ) # Called by parser based on tag name.
381 43     43   134 my ($self,$tag,%attr) = @_;
382              
383 43         87 my ($h,$w,$src) = @attr{qw( height width src )};
384 43 100 66     125 if ( defined $h && defined $w ) {
385             # Check sizes
386             }
387             else {
388 7         14 $self->gripe( 'elem-img-sizes-missing', src=>$src );
389             }
390 43 100       71 if ( not defined $attr{alt} ) {
391 8         16 $self->gripe( 'elem-img-alt-missing', src=>$src );
392             }
393              
394 43         79 return;
395             }
396              
397             sub _start_input { ## no critic ( Subroutines::ProhibitUnusedPrivateSubroutines ) # Called by parser based on tag name.
398 10     10   25 my ($self,$tag,%attr) = @_;
399              
400 10         21 my ($type,$alt) = @attr{qw( type alt )};
401 10 100 66     37 if ( defined($type) && (lc($type) eq 'image') ) {
402 9         14 my $ok = defined($alt);
403 9 100       12 if ( $ok ) {
404 5         9 $alt =~ s/^ +//;
405 5         11 $alt =~ s/ +$//;
406 5         6 $ok = ($alt ne '');
407             }
408 9 100       14 if ( !$ok ) {
409 6         8 my $name = $attr{name};
410 6 100       14 $name = '' unless defined $name;
411 6         9 $self->gripe( 'elem-input-alt-missing', name => $name );
412             }
413             }
414              
415 10         16 return;
416             }
417              
418             1;