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   1971658 use 5.010001;
  25         311  
4 25     25   107 use strict;
  25         39  
  25         451  
5 25     25   107 use warnings;
  25         45  
  25         513  
6 25     25   101 use Carp ();
  25         45  
  25         471  
7              
8 25     25   8346 use HTML::T5::Message;
  25         53  
  25         1014  
9              
10             =head1 NAME
11              
12             HTML::T5 - HTML validation in a Perl object
13              
14             =head1 VERSION
15              
16             Version 0.013
17              
18             =cut
19              
20             our $VERSION = '0.013';
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   130 use base 'Exporter';
  25         44  
  25         1815  
52              
53 25     25   133 use constant TIDY_ERROR => 3;
  25         42  
  25         1687  
54 25     25   126 use constant TIDY_WARNING => 2;
  25         33  
  25         873  
55 25     25   110 use constant TIDY_INFO => 1;
  25         39  
  25         27194  
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 26184 my $class = shift;
94 42   100     177 my $args = shift || {};
95 42         145 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         198 my $self = bless {
106             messages => [],
107             ignore_type => [],
108             ignore_text => [],
109             config_file => '',
110             tidy_options => {},
111             }, $class;
112              
113 42         81 for my $key (keys %{$args} ) {
  42         138  
114 41 100       83 if ($key eq 'config_file') {
115 6         26 $self->{config_file} = $args->{$key};
116 6         14 next;
117             }
118              
119 35         42 my $newkey = $key;
120 35         62 $newkey =~ tr/_/-/;
121              
122 35 100       47 if ( grep {$newkey eq $_} @unsupported_options ) {
  245         335  
123 7         65 Carp::croak( "Unsupported option: $newkey" );
124             }
125              
126 28         78 $self->{tidy_options}->{$newkey} = $args->{$key};
127             }
128              
129 35         136 return $self;
130             }
131              
132             =head2 messages()
133              
134             Returns the messages accumulated.
135              
136             =cut
137              
138             sub messages {
139 28     28 1 5419 my $self = shift;
140              
141 28         38 return @{$self->{messages}};
  28         84  
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 641 my $self = shift;
154              
155 12         32 $self->{messages} = [];
156              
157 12         21 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 5607 my $self = shift;
193 16         38 my @parms = @_;
194              
195 16         50 while ( @parms ) {
196 16         28 my $parm = shift @parms;
197 16         26 my $value = shift @parms;
198 16 100       53 my @values = ref($value) eq 'ARRAY' ? @{$value} : ($value);
  1         2  
199              
200 16 100 100     230 Carp::croak( qq{Invalid ignore type of "$parm"} )
201             unless ($parm eq 'text') or ($parm eq 'type');
202              
203 15         22 push( @{$self->{"ignore_$parm"}}, @values );
  15         74  
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 2239 my $self = shift;
223 22         37 my $filename = shift;
224 22 100       71 if (@_ == 0) {
225 1         15 Carp::croak('Usage: parse($filename,$str [, $str...])');
226             }
227 21         72 my $html = join( '', @_ );
228              
229 21 100       59 utf8::encode($html) if utf8::is_utf8($html);
230 21         8647 my ($errorblock,$newline) = _tidy_messages( $html, $self->{config_file}, $self->{tidy_options} );
231 21 100       121 return 1 unless defined $errorblock;
232              
233 19         71 utf8::decode($errorblock);
234              
235 19         58 return !$self->_parse_errors($filename, $errorblock, $newline);
236             }
237              
238             sub _parse_errors {
239 33     33   530 my $self = shift;
240 33         48 my $filename = shift;
241 33         55 my $errs = shift;
242 33         55 my $newline = shift;
243              
244 33         45 my $parse_errors;
245              
246 33         361 my @lines = split( /$newline/, $errs );
247              
248 33         103 for my $line ( @lines ) {
249 190         264 chomp $line;
250              
251 190         240 my $message;
252 190 100       898 if ( $line =~ /^line (\d+) column (\d+) - (Warning|Error|Info): (.+)$/ ) { ## no critic ( ControlStructures::ProhibitCascadingIfElse )
    100          
    100          
    100          
    50          
    50          
    100          
253 160         489 my ($line, $col, $type, $text) = ($1, $2, $3, $4);
254 160 100       302 $type =
    100          
255             ($type eq 'Warning') ? TIDY_WARNING :
256             ($type eq 'Info') ? TIDY_INFO :
257             TIDY_ERROR;
258 160         399 $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         38 my $text = $1;
265 15         56 $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         19 Carp::carp "HTML::T5: Unknown error type: $line";
288 1         322 ++$parse_errors;
289             }
290 190 100 100     525 push( @{$self->{messages}}, $message )
  152         297  
291             if $message && $self->_is_keeper( $message );
292             } # for
293 33         100 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 5627 my $self = shift;
306              
307 14 100       48 if (@_ == 0) {
308 1         14 Carp::croak('Usage: clean($str [, $str...])');
309             }
310 13         47 my $text = join( '', @_ );
311              
312 13 100       49 utf8::encode($text) if utf8::is_utf8($text);
313 13 50       47 if ( defined $text ) {
314 13         28 $text .= "\n";
315             }
316              
317             my ($cleaned, $errbuf, $newline) = _tidy_clean( $text,
318             $self->{config_file},
319 13         6436 $self->{tidy_options});
320 13         107 utf8::decode($cleaned);
321 13         44 utf8::decode($errbuf);
322              
323 13         52 $self->_parse_errors('', $errbuf, $newline);
324 13         39 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   202 my $self = shift;
331              
332 175         179 my $message = shift;
333              
334 175         174 my @ignore_types = @{$self->{ignore_type}};
  175         263  
335 175 100       282 if ( @ignore_types ) {
336 45 100       60 return 0 if grep { $message->type == $_ } @ignore_types;
  51         92  
337             }
338              
339 157         165 my @ignore_texts = @{$self->{ignore_text}};
  157         222  
340 157 100       233 if ( @ignore_texts ) {
341 15 100       19 return 0 if grep { $message->text =~ $_ } @ignore_texts;
  27         44  
342             }
343              
344 152         346 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 277 my $version_str = _tidy_library_version();
355              
356 3         22 return $version_str;
357             }
358              
359             require XSLoader;
360             XSLoader::load('HTML::T5', $VERSION);
361              
362             1;
363              
364             __END__