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   2017236 use 5.010001;
  25         291  
4 25     25   114 use strict;
  25         36  
  25         462  
5 25     25   113 use warnings;
  25         53  
  25         559  
6 25     25   129 use Carp ();
  25         43  
  25         532  
7              
8 25     25   8490 use HTML::T5::Message;
  25         60  
  25         1007  
9              
10             =head1 NAME
11              
12             HTML::T5 - HTML validation in a Perl object
13              
14             =head1 VERSION
15              
16             Version 0.012
17              
18             =cut
19              
20             our $VERSION = '0.012';
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   150 use base 'Exporter';
  25         33  
  25         1706  
52              
53 25     25   132 use constant TIDY_ERROR => 3;
  25         36  
  25         1609  
54 25     25   125 use constant TIDY_WARNING => 2;
  25         39  
  25         872  
55 25     25   107 use constant TIDY_INFO => 1;
  25         37  
  25         27309  
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 27068 my $class = shift;
94 42   100     182 my $args = shift || {};
95 42         160 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         186 my $self = bless {
106             messages => [],
107             ignore_type => [],
108             ignore_text => [],
109             config_file => '',
110             tidy_options => {},
111             }, $class;
112              
113 42         72 for my $key (keys %{$args} ) {
  42         140  
114 38 100       81 if ($key eq 'config_file') {
115 3         18 $self->{config_file} = $args->{$key};
116 3         8 next;
117             }
118              
119 35         42 my $newkey = $key;
120 35         58 $newkey =~ tr/_/-/;
121              
122 35 100       53 if ( grep {$newkey eq $_} @unsupported_options ) {
  245         332  
123 7         67 Carp::croak( "Unsupported option: $newkey" );
124             }
125              
126 28         80 $self->{tidy_options}->{$newkey} = $args->{$key};
127             }
128              
129 35         124 return $self;
130             }
131              
132             =head2 messages()
133              
134             Returns the messages accumulated.
135              
136             =cut
137              
138             sub messages {
139 28     28 1 5908 my $self = shift;
140              
141 28         46 return @{$self->{messages}};
  28         81  
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 678 my $self = shift;
154              
155 12         31 $self->{messages} = [];
156              
157 12         20 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 6242 my $self = shift;
193 16         39 my @parms = @_;
194              
195 16         43 while ( @parms ) {
196 16         32 my $parm = shift @parms;
197 16         25 my $value = shift @parms;
198 16 100       51 my @values = ref($value) eq 'ARRAY' ? @{$value} : ($value);
  1         2  
199              
200 16 100 100     218 Carp::croak( qq{Invalid ignore type of "$parm"} )
201             unless ($parm eq 'text') or ($parm eq 'type');
202              
203 15         19 push( @{$self->{"ignore_$parm"}}, @values );
  15         78  
204             } # while
205              
206 15         31 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 2614 my $self = shift;
223 22         34 my $filename = shift;
224 22 100       69 if (@_ == 0) {
225 1         13 Carp::croak('Usage: parse($filename,$str [, $str...])');
226             }
227 21         79 my $html = join( '', @_ );
228              
229 21 100       58 utf8::encode($html) if utf8::is_utf8($html);
230 21         8589 my ($errorblock,$newline) = _tidy_messages( $html, $self->{config_file}, $self->{tidy_options} );
231 21 100       115 return 1 unless defined $errorblock;
232              
233 19         69 utf8::decode($errorblock);
234              
235 19         58 return !$self->_parse_errors($filename, $errorblock, $newline);
236             }
237              
238             sub _parse_errors {
239 33     33   98 my $self = shift;
240 33         77 my $filename = shift;
241 33         49 my $errs = shift;
242 33         64 my $newline = shift;
243              
244 33         42 my $parse_errors;
245              
246 33         362 my @lines = split( /$newline/, $errs );
247              
248 33         92 for my $line ( @lines ) {
249 190         254 chomp $line;
250              
251 190         199 my $message;
252 190 100       864 if ( $line =~ /^line (\d+) column (\d+) - (Warning|Error|Info): (.+)$/ ) { ## no critic ( ControlStructures::ProhibitCascadingIfElse )
    100          
    100          
    100          
    50          
    50          
    100          
253 160         504 my ($line, $col, $type, $text) = ($1, $2, $3, $4);
254 160 100       299 $type =
    100          
255             ($type eq 'Warning') ? TIDY_WARNING :
256             ($type eq 'Info') ? TIDY_INFO :
257             TIDY_ERROR;
258 160         388 $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         36 my $text = $1;
265 15         47 $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         15 Carp::carp "HTML::T5: Unknown error type: $line";
288 1         254 ++$parse_errors;
289             }
290 190 100 100     478 push( @{$self->{messages}}, $message )
  152         309  
291             if $message && $self->_is_keeper( $message );
292             } # for
293 33         97 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 5347 my $self = shift;
306              
307 14 100       50 if (@_ == 0) {
308 1         13 Carp::croak('Usage: clean($str [, $str...])');
309             }
310 13         43 my $text = join( '', @_ );
311              
312 13 100       48 utf8::encode($text) if utf8::is_utf8($text);
313 13 50       33 if ( defined $text ) {
314 13         25 $text .= "\n";
315             }
316              
317             my ($cleaned, $errbuf, $newline) = _tidy_clean( $text,
318             $self->{config_file},
319 13         6302 $self->{tidy_options});
320 13         76 utf8::decode($cleaned);
321 13         40 utf8::decode($errbuf);
322              
323 13         47 $self->_parse_errors('', $errbuf, $newline);
324 13         44 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   230 my $self = shift;
331              
332 175         177 my $message = shift;
333              
334 175         179 my @ignore_types = @{$self->{ignore_type}};
  175         264  
335 175 100       289 if ( @ignore_types ) {
336 45 100       60 return 0 if grep { $message->type == $_ } @ignore_types;
  51         81  
337             }
338              
339 157         201 my @ignore_texts = @{$self->{ignore_text}};
  157         193  
340 157 100       226 if ( @ignore_texts ) {
341 15 100       21 return 0 if grep { $message->text =~ $_ } @ignore_texts;
  27         43  
342             }
343              
344 152         365 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 276 my $version_str = _tidy_library_version();
355              
356 3         20 return $version_str;
357             }
358              
359             require XSLoader;
360             XSLoader::load('HTML::T5', $VERSION);
361              
362             1;
363              
364             __END__