File Coverage

blib/lib/HTML/T5.pm
Criterion Covered Total %
statement 116 116 100.0
branch 45 48 93.7
condition 8 8 100.0
subroutine 18 18 100.0
pod 7 7 100.0
total 194 197 98.4


line stmt bran cond sub pod time code
1             package HTML::T5;
2              
3 25     25   1950803 use 5.010001;
  25         260  
4 25     25   105 use strict;
  25         39  
  25         469  
5 25     25   118 use warnings;
  25         49  
  25         552  
6 25     25   103 use Carp ();
  25         54  
  25         487  
7              
8 25     25   8401 use HTML::T5::Message;
  25         47  
  25         978  
9              
10             =head1 NAME
11              
12             HTML::T5 - HTML validation in a Perl object
13              
14             =head1 VERSION
15              
16             Version 0.011
17              
18             =cut
19              
20             our $VERSION = '0.011';
21              
22             =head1 SYNOPSIS
23              
24             use HTML::T5;
25              
26             my $tidy = HTML::T5->new( {config_file => 'path/to/config'} );
27             $tidy->ignore( type => TIDY_WARNING, type => TIDY_INFO );
28             $tidy->parse( "foo.html", $contents_of_foo );
29              
30             for my $message ( $tidy->messages ) {
31             print $message->as_string;
32             }
33              
34             =head1 DESCRIPTION
35              
36             C is an HTML checker in a handy dandy object. It's meant
37             as a replacement for L. If you're currently
38             an L user looking to migrate, see the section
39             L.
40              
41             C was forked from L by Andy Lester (PETDANCE), thanks.
42              
43             =head1 EXPORTS
44              
45             Message types C, C and C.
46              
47             Everything else is an object method.
48              
49             =cut
50              
51 25     25   127 use base 'Exporter';
  25         32  
  25         1840  
52              
53 25     25   189 use constant TIDY_ERROR => 3;
  25         36  
  25         1477  
54 25     25   128 use constant TIDY_WARNING => 2;
  25         37  
  25         828  
55 25     25   102 use constant TIDY_INFO => 1;
  25         37  
  25         26060  
56              
57             our @EXPORT = qw( TIDY_ERROR TIDY_WARNING TIDY_INFO );
58              
59             =head1 METHODS
60              
61             =head2 new()
62              
63             Create an HTML::T5 object.
64              
65             my $tidy = HTML::T5->new();
66              
67             Optionally you can give a hashref of configuration parms.
68              
69             my $tidy = HTML::T5->new( {config_file => 'path/to/tidy.cfg'} );
70              
71             This configuration file will be read and used when you clean or parse an HTML file.
72              
73             You can also pass options directly to tidy.
74              
75             my $tidy = HTML::T5->new( {
76             output_xhtml => 1,
77             tidy_mark => 0,
78             } );
79              
80             See C for the list of options supported by tidy.
81              
82             The following options are not supported by C:
83              
84             =over 4
85              
86             =item * quiet
87              
88             =back
89              
90             =cut
91              
92             sub new {
93 42     42 1 28146 my $class = shift;
94 42   100     181 my $args = shift || {};
95 42         139 my @unsupported_options = qw(
96             force-output
97             gnu-emacs-file
98             gnu-emacs
99             keep-time
100             quiet
101             slide-style
102             write-back
103             ); # REVIEW perhaps a list of supported options would be better
104              
105 42         189 my $self = bless {
106             messages => [],
107             ignore_type => [],
108             ignore_text => [],
109             config_file => '',
110             tidy_options => {},
111             }, $class;
112              
113 42         73 for my $key (keys %{$args} ) {
  42         136  
114 40 100       78 if ($key eq 'config_file') {
115 5         30 $self->{config_file} = $args->{$key};
116 5         14 next;
117             }
118              
119 35         53 my $newkey = $key;
120 35         53 $newkey =~ tr/_/-/;
121              
122 35 100       52 if ( grep {$newkey eq $_} @unsupported_options ) {
  245         303  
123 7         69 Carp::croak( "Unsupported option: $newkey" );
124             }
125              
126 28         70 $self->{tidy_options}->{$newkey} = $args->{$key};
127             }
128              
129 35         114 return $self;
130             }
131              
132             =head2 messages()
133              
134             Returns the messages accumulated.
135              
136             =cut
137              
138             sub messages {
139 28     28 1 5880 my $self = shift;
140              
141 28         37 return @{$self->{messages}};
  28         86  
142             }
143              
144             =head2 clear_messages()
145              
146             Clears the list of messages, in case you want to print and clear, print
147             and clear. If you don't clear the messages, then each time you call
148             L you'll be accumulating more in the list.
149              
150             =cut
151              
152             sub clear_messages {
153 12     12 1 656 my $self = shift;
154              
155 12         42 $self->{messages} = [];
156              
157 12         22 return;
158             }
159              
160             =head2 ignore( parm => value [, parm => value ] )
161              
162             Specify types of messages to ignore. Note that the ignore flags must be
163             set B calling C. You can call C as many times
164             as necessary to set up all your restrictions; the options will stack up.
165              
166             =over 4
167              
168             =item * type => TIDY_INFO|TIDY_WARNING|TIDY_ERROR
169              
170             Specifies the type of messages you want to ignore, either info or warnings
171             or errors. If you wanted, you could call ignore on all three and get
172             no messages at all.
173              
174             $tidy->ignore( type => TIDY_WARNING );
175              
176             =item * text => qr/regex/
177              
178             =item * text => [ qr/regex1/, qr/regex2/, ... ]
179              
180             Checks the text of the message against the specified regex or regexes,
181             and ignores the message if there's a match. The value for the I
182             parm may be either a regex, or a reference to a list of regexes.
183              
184             $tidy->ignore( text => qr/DOCTYPE/ );
185             $tidy->ignore( text => [ qr/unsupported/, qr/proprietary/i ] );
186              
187             =back
188              
189             =cut
190              
191             sub ignore {
192 16     16 1 5836 my $self = shift;
193 16         36 my @parms = @_;
194              
195 16         38 while ( @parms ) {
196 16         26 my $parm = shift @parms;
197 16         24 my $value = shift @parms;
198 16 100       51 my @values = ref($value) eq 'ARRAY' ? @{$value} : ($value);
  1         2  
199              
200 16 100 100     185 Carp::croak( qq{Invalid ignore type of "$parm"} )
201             unless ($parm eq 'text') or ($parm eq 'type');
202              
203 15         21 push( @{$self->{"ignore_$parm"}}, @values );
  15         71  
204             } # while
205              
206 15         28 return;
207             } # ignore
208              
209             =head2 parse( $filename, $str [, $str...] )
210              
211             Parses a string, or list of strings, that make up a single HTML file.
212              
213             The I<$filename> parm is only used as an identifier for your use.
214             The file is not actually read and opened.
215              
216             Returns true if all went OK, or false if there was some problem calling
217             tidy, or parsing tidy's output.
218              
219             =cut
220              
221             sub parse {
222 22     22 1 2472 my $self = shift;
223 22         33 my $filename = shift;
224 22 100       68 if (@_ == 0) {
225 1         10 Carp::croak('Usage: parse($filename,$str [, $str...])');
226             }
227 21         79 my $html = join( '', @_ );
228              
229 21 100       71 utf8::encode($html) if utf8::is_utf8($html);
230 21         7928 my ($errorblock,$newline) = _tidy_messages( $html, $self->{config_file}, $self->{tidy_options} );
231 21 100       117 return 1 unless defined $errorblock;
232              
233 19         108 utf8::decode($errorblock);
234              
235 19         65 return !$self->_parse_errors($filename, $errorblock, $newline);
236             }
237              
238             sub _parse_errors {
239 33     33   90 my $self = shift;
240 33         63 my $filename = shift;
241 33         66 my $errs = shift;
242 33         54 my $newline = shift;
243              
244 33         41 my $parse_errors;
245              
246 33         428 my @lines = split( /$newline/, $errs );
247              
248 33         96 for my $line ( @lines ) {
249 190         269 chomp $line;
250              
251 190         197 my $message;
252 190 100       821 if ( $line =~ /^line (\d+) column (\d+) - (Warning|Error|Info): (.+)$/ ) { ## no critic ( ControlStructures::ProhibitCascadingIfElse )
    100          
    100          
    100          
    50          
    50          
    100          
253 160         461 my ($line, $col, $type, $text) = ($1, $2, $3, $4);
254 160 100       291 $type =
    100          
255             ($type eq 'Warning') ? TIDY_WARNING :
256             ($type eq 'Info') ? TIDY_INFO :
257             TIDY_ERROR;
258 160         394 $message = HTML::T5::Message->new( $filename, $type, $line, $col, $text );
259              
260             }
261             elsif ( $line =~ m/^Info: (.+)$/ ) {
262             # Info line we don't want
263              
264 15         34 my $text = $1;
265 15         48 $message = HTML::T5::Message->new( $filename, TIDY_INFO, undef, undef, $text );
266             }
267             elsif ( $line =~ /^Tidy found \d+ warnings? and \d+ errors?!/ ) {
268             # Summary line we don't want
269             # We should take these counts from the summary and make sure they match what we parsed.
270             }
271             elsif ( $line eq 'No warnings or errors were found.' ) {
272             # Summary line we don't want
273              
274             }
275             elsif ( $line eq 'This document has errors that must be fixed before' ) {
276             # Summary line we don't want
277              
278             }
279             elsif ( $line eq 'using HTML Tidy to generate a tidied up version.' ) {
280             # Summary line we don't want
281              
282             }
283             elsif ( $line =~ m/^\s*$/ ) {
284             # Blank line we don't want
285             }
286             else {
287 1         17 Carp::carp "HTML::T5: Unknown error type: $line";
288 1         249 ++$parse_errors;
289             }
290 190 100 100     507 push( @{$self->{messages}}, $message )
  152         278  
291             if $message && $self->_is_keeper( $message );
292             } # for
293 33         94 return $parse_errors;
294             }
295              
296             =head2 clean( $str [, $str...] )
297              
298             Cleans a string, or list of strings, that make up a single HTML file.
299              
300             Returns the cleaned string as a single string.
301              
302             =cut
303              
304             sub clean {
305 14     14 1 5476 my $self = shift;
306              
307 14 100       47 if (@_ == 0) {
308 1         14 Carp::croak('Usage: clean($str [, $str...])');
309             }
310 13         43 my $text = join( '', @_ );
311              
312 13 100       58 utf8::encode($text) if utf8::is_utf8($text);
313 13 50       38 if ( defined $text ) {
314 13         25 $text .= "\n";
315             }
316              
317             my ($cleaned, $errbuf, $newline) = _tidy_clean( $text,
318             $self->{config_file},
319 13         6171 $self->{tidy_options});
320 13         75 utf8::decode($cleaned);
321 13         81 utf8::decode($errbuf);
322              
323 13         49 $self->_parse_errors('', $errbuf, $newline);
324 13         45 return $cleaned;
325             }
326              
327             # Tells whether a given message object is one that we should keep.
328              
329             sub _is_keeper {
330 175     175   187 my $self = shift;
331              
332 175         182 my $message = shift;
333              
334 175         170 my @ignore_types = @{$self->{ignore_type}};
  175         280  
335 175 100       284 if ( @ignore_types ) {
336 45 100       57 return 0 if grep { $message->type == $_ } @ignore_types;
  51         81  
337             }
338              
339 157         172 my @ignore_texts = @{$self->{ignore_text}};
  157         189  
340 157 100       230 if ( @ignore_texts ) {
341 15 100       15 return 0 if grep { $message->text =~ $_ } @ignore_texts;
  27         43  
342             }
343              
344 152         335 return 1;
345             }
346              
347             =head2 tidy_library_version()
348              
349             Returns the version of the underling tidy library.
350              
351             =cut
352              
353             sub tidy_library_version {
354 3     3 1 259 my $version_str = _tidy_library_version();
355              
356 3         21 return $version_str;
357             }
358              
359             require XSLoader;
360             XSLoader::load('HTML::T5', $VERSION);
361              
362             1;
363              
364             __END__