Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Slurping in HTML::Lint 2.02

  • Loading branch information...
commit 43c0c1acb7e27be0be6b31710b6a4942063b115d 0 parents
@petdance authored
Showing with 2,552 additions and 0 deletions.
  1. +177 −0 Changes
  2. +38 −0 MANIFEST
  3. +16 −0 META.yml
  4. +46 −0 Makefile.PL
  5. +15 −0 README
  6. +93 −0 bin/weblint
  7. +510 −0 lib/HTML/Lint.pm
  8. +297 −0 lib/HTML/Lint/Error.pm
  9. +277 −0 lib/HTML/Lint/HTML4.pm
  10. +156 −0 lib/Test/HTML/Lint.pm
  11. +13 −0 t/00-load.t
  12. +16 −0 t/01-coverage.t
  13. +15 −0 t/02-versions.t
  14. +20 −0 t/10-test-html-lint.t
  15. +20 −0 t/11-test-html-lint-overload.t
  16. +17 −0 t/20-error-types-export.t
  17. +62 −0 t/20-error-types-skip.t
  18. +17 −0 t/20-error-types.t
  19. +13 −0 t/30-test-builder.t
  20. +42 −0 t/40-where.t
  21. +46 −0 t/50-multiple-files.t
  22. +67 −0 t/LintTest.pl
  23. +17 −0 t/attr-repeated.t
  24. +19 −0 t/attr-unknown.t
  25. +16 −0 t/doc-tag-required.t
  26. +17 −0 t/elem-empty-but-closed.t
  27. +18 −0 t/elem-img-alt-missing.t
  28. +18 −0 t/elem-img-sizes-missing.t
  29. +18 −0 t/elem-nonrepeatable.t
  30. +19 −0 t/elem-unclosed.t
  31. +20 −0 t/elem-unknown.t
  32. +17 −0 t/elem-unopened.t
  33. +291 −0 t/embed-extensions.t
  34. +8 −0 t/pod-coverage.t
  35. +6 −0 t/pod.t
  36. +20 −0 t/random-nobr.t
  37. +20 −0 t/text-use-entity.t
  38. +55 −0 t/xhtml-html.t
177 Changes
@@ -0,0 +1,177 @@
+Revision history HTML::Lint and Test::HTML::Lint.
+
+2.02 Thu Nov 3 11:49:18 CST 2005
+ [ENHANCEMENTS]
+ * The warnings for missing ALT and HEIGHT/WIDTH on your images
+ now give the SRC attribute.
+
+2.00 Tue Sep 20 23:10:39 CDT 2005
+ [CHANGES THAT COULD BREAK YOUR CODE]
+ * I've changed the object structure. HTML::Lint now has-a
+ HTML::Parser, and no longer is-a HTML::Parser.
+
+ * weblint-cgi and weblint-original are no longer distributed
+ with HTML::Lint.
+
+ * Now requires Perl 5.6.0.
+
+ [FIXES]
+ * Line numbering is now correct if you parse more than one file.
+
+ * Changed t/*.*.t so that they only had one period in the
+ filename. Apparently VMS doesn't like filenames with multiple
+ dots.
+
+1.30 December 3, 2004
+ [ENHANCEMENTS]
+ * Added handling for <HTML xmlns=... xml:lang=...>
+
+ * Fixed <EMBED pluginspace=...>
+
+ * Added handling of <EMBED quality=... and play=...>
+
+1.28 January 27, 2004
+ [ENHANCEMENTS]
+ * Added support for the <EMBED> tag.
+
+ [INTERNALS]
+ * Added more POD coverage.
+
+1.26 December 23, 2003
+ [FIXES]
+ * Fixed warnings if a lint error was found in the first column
+ of the input file. Thanks, Adam Monsen.
+
+1.25 December 19, 2003
+ [ENHANCEMENTS]
+ * html_ok() clears the HTML::Lint object that gets passed
+ in, if any. Thanks to Cees Hek.
+
+ [INTERNALS]
+ * Ran everything through Devel::Cover to see what wasn't
+ getting exercised.
+ * Removed _element_stack() which was never used.
+ * Error.pm's internals now eat their own dog food of methods,
+ rather than accessing internals directly.
+ * line(), column() and file() are no longer setters in
+ HTML::Lint. I sure hope this doesn't break anything for
+ anyone.
+ * Removed some explicit "return undef" stuff.
+
+1.24 September 11, 2003
+ [FIXES]
+ * Removed the check for "input tags can have height &
+ width", because they can't.
+
+1.23 September 02, 2003
+ [ENHANCEMENTS]
+ * The types of errors to find may now be passed in the
+ HTML::Lint constructor.
+ * Can now export the error types from HTML::Lint::Error.
+
+ [DOCUMENTATION]
+ * Fixed a bug in Test::HTML::Lint docs. (Thanks to Leon
+ Brocard and about 90 other people)
+
+1.22 JUNE 11, 2003
+ [ENHANCEMENTS]
+ * Added some docs explaining html_ok()'s optional lint object.
+ * Allowed the NAME attribute in the <IMG> tag.
+
+ [FIXES]
+ * Fixed the version check in t/99.pod.t
+
+1.21 October 9, 2002
+ [ENHANCEMENTS]
+ * Added a test file for pod files
+ * html_ok() now diag()s the test name before spewing the errors.
+ * Made some docs a little more explicit. (Thanks to Ron Savage)
+ * Added the check for missing <HTML>, <HEAD>, <TITLE> and <BODY>.
+ * Added the check for nonrepeatable tags.
+
+ [FIXES]
+ * Ticket #1493: typo in HTML::Lint::HTML4
+ <ADDRESS> element in HTML4.pm was <ADDRESSS>. Thanks to Dominic
+ Mitchell for finding it.
+
+1.20 August 22, 2002
+ [ENHANCEMENTS]
+ * Made it run under 5.5.3
+
+1.13 August 5, 2002
+ [ENHANCEMENTS]
+ * Added text-use-entity for detecting control characters.
+
+1.12 August 2, 2002
+ [FIXES]
+ * No longer squawks on html_ok( undef ). It now specifically fails.
+
+1.11 July 25, 2002
+ [ENHANCEMENTS]
+ * Added the concept of HTML::Lint::Error type, which is one of
+ Structure, Helper or Fluff.
+ * weblint now takes --[no]structure, --[no]helper, --[no]fluff.
+ * Added the ability to overload html_ok()'s HTML::Lint object
+ with one of your own.
+
+ [FIXES]
+ * Fixed line number goof in Test::HTML::Lint
+ * Fixed line positions in weblint on files (URL reading was OK)
+
+1.10 July 17, 2002
+ * Added Test::HTML::Lint
+ * Removed the "use 5.6.0" from everywhere
+ * Included Neil Bowers' original weblint script, in the
+ weblint-original/ directory. Note that it does NOT get
+ installed.
+
+1.02 July 8, 2002
+ * No functional changes. It's all in the test suite.
+ * Added a whole mess of *.t files. They've been in CVS all along,
+ but I forgot to put them in the MANIFEST. Ooops.
+
+1.01 July 3, 2002
+ * Tests use isa_ok() for more stringent checking of return objects
+ * Added <nobr>. Ooopsie.
+
+1.00 June 5, 2002
+ * weblint has a --context option to show the line that the error
+ occurs on.
+ * Removed the original weblint log from this Changes file.
+ * Fixed: Unknown tags wouldn't get put into the stack, so you
+ would get two errors: One complaining that it didn't know the
+ tag, and another that the closing tag didn't make sense.
+
+0.94 May 31, 2002
+ * Moved the %HTML::Lint::Error::errors hash to be a package
+ variable, and not initialize it in an INIT block.
+
+0.93 May 28, 2002
+ * weblint can now read URLs as well as files
+ * Improved the docs in HTML::Lint::Error.
+
+0.92 February 26, 2001
+ * Removed the INIT block in HTML::Error in preparation for
+ Apache::Lint, and so brian's weblint++ can use it.
+ * errors() method respects wantarray (brian d foy)
+ * file(), line(), column(), errcode() and errtext() all return
+ blank instead of undef.
+
+0.91 January 8, 2002
+ * Fixed: Content description tags (<CITE>, <EM>, etc) were
+ not seen as being valid tags.
+
+0.90 July 8, 2001
+ First version of the rewrite as a subclass of HTML::Parser
+
+0.03 May 15, 2001
+ * Fixed t/*.t problems
+
+0.02 May 14, 2001
+ * Packaged and bundled with new namespace
+ * First upload to CPAN
+
+0.01 April 20, 2001
+ * original version; created by h2xs 1.21 with options
+ -n HTML::Lint -X -A
+ * adapted entirely from Neil Bowers' Weblint package
38 MANIFEST
@@ -0,0 +1,38 @@
+Changes
+MANIFEST
+Makefile.PL
+README
+bin/weblint
+lib/HTML/Lint.pm
+lib/HTML/Lint/Error.pm
+lib/HTML/Lint/HTML4.pm
+lib/Test/HTML/Lint.pm
+t/LintTest.pl
+t/00-load.t
+t/01-coverage.t
+t/02-versions.t
+t/10-test-html-lint.t
+t/11-test-html-lint-overload.t
+t/20-error-types-export.t
+t/20-error-types-skip.t
+t/20-error-types.t
+t/30-test-builder.t
+t/40-where.t
+t/50-multiple-files.t
+t/attr-repeated.t
+t/attr-unknown.t
+t/doc-tag-required.t
+t/elem-empty-but-closed.t
+t/elem-img-alt-missing.t
+t/elem-img-sizes-missing.t
+t/elem-nonrepeatable.t
+t/elem-unclosed.t
+t/elem-unknown.t
+t/elem-unopened.t
+t/embed-extensions.t
+t/pod.t
+t/pod-coverage.t
+t/random-nobr.t
+t/text-use-entity.t
+t/xhtml-html.t
+META.yml Module meta-data (added by MakeMaker)
16 META.yml
@@ -0,0 +1,16 @@
+# http://module-build.sourceforge.net/META-spec.html
+#XXXXXXX This is a prototype!!! It will change in the future!!! XXXXX#
+name: HTML-Lint
+version: 2.02
+version_from: lib/HTML/Lint.pm
+installdirs: site
+requires:
+ Exporter: 0
+ File::Find: 0
+ HTML::Parser: 3.20
+ HTML::Tagset: 3.03
+ Test::Builder: 0
+ Test::More: 0
+
+distribution_type: module
+generated_by: ExtUtils::MakeMaker version 6.30
46 Makefile.PL
@@ -0,0 +1,46 @@
+use strict;
+use ExtUtils::MakeMaker;
+use File::Find;
+use 5.6.0;
+
+find( \&filecheck, "." );
+
+sub filecheck {
+ unlink if /~$/; # Remove any vi backup files
+ die "Aborting: Swapfile $_ found" if /\.swp$/;
+}
+
+eval { require LWP::Simple; };
+
+if ( $@ ) {
+ print <<EOF;
+
+NOTE: It seems that you don't have LWP::Simple installed.
+ The weblint program will not be able to retrieve web pages.
+
+EOF
+}
+
+&WriteMakefile(
+ NAME => 'HTML::Lint',
+ DISTNAME => 'HTML-Lint',
+ VERSION_FROM => 'lib/HTML/Lint.pm',
+ ABSTRACT_FROM => 'lib/HTML/Lint.pm',
+ PMLIBDIRS => [qw(lib/)],
+ AUTHOR => 'Andy Lester <andy@petdance.com>',
+ PREREQ_PM => {
+ 'Exporter' => 0,
+ 'Test::More' => 0,
+ 'Test::Builder' => 0,
+ 'HTML::Parser' => '3.20',
+ 'HTML::Tagset' => '3.03',
+ 'File::Find' => 0,
+ },
+ EXE_FILES => [qw(bin/weblint)],
+ dist => {
+ COMPRESS => 'gzip -9f',
+ SUFFIX => 'gz',
+ },
+ clean => { FILES => 'HTML-Lint-*' },
+ );
+
15 README
@@ -0,0 +1,15 @@
+HTML::Lint is a pure-Perl HTML parser and checker for syntactic legitmacy.
+
+The "weblint" script is now pretty much a wrapper around the HTML::Lint
+module.
+
+You can also look into Apache::Lint which is a mod_perl wrapper around
+HTML::Lint.
+
+Finally, for those of you doing automated testing with Test::More and
+its brethren, Test::HTML::Lint lets you automate HTML checking.
+
+Please let me know if any tests fail.
+
+Andy Lester
+andy at petdance.com
93 bin/weblint
@@ -0,0 +1,93 @@
+#!/usr/bin/perl -wT
+# $Id: weblint,v 1.14 2002/08/05 22:03:24 petdance Exp $
+
+use strict;
+
+use Getopt::Long;
+use HTML::Lint;
+use HTML::Lint::HTML4;
+
+my $help;
+my $context;
+
+my $structure = 1;
+my $helper = 1;
+my $fluff = 1;
+
+GetOptions(
+ "help" => \$help,
+ "context:i" => \$context,
+ "only" => sub { $structure = $helper = $fluff = 0; },
+ "structure!" => \$structure,
+ "helper!" => \$helper,
+ "fluff!" => \$fluff,
+) or $help = 1;
+
+if ( !@ARGV || $help ) {
+ print "weblint v$HTML::Lint::VERSION\n";
+ print <DATA>;
+ exit 1;
+}
+
+my @types;
+push( @types, HTML::Lint::Error::STRUCTURE ) if $structure;
+push( @types, HTML::Lint::Error::HELPER ) if $helper;
+push( @types, HTML::Lint::Error::FLUFF ) if $fluff;
+
+my $lint = new HTML::Lint;
+$lint->only_types( @types ) if @types;
+for my $url ( @ARGV ) {
+ my @lines;
+ $lint->newfile( $url );
+ if ( $url =~ /^https?:/ ) {
+ eval { require LWP::Simple };
+ if ( $@ ) {
+ warn "Can't retrieve URLs without LWP::Simple installed";
+ next;
+ }
+
+ my $content = LWP::Simple::get( $url );
+ if ( $content ) {
+ @lines = split( "\n", $content );
+ $_ = "$_\n" for @lines;
+ } else {
+ warn "Unable to fetch $url\n";
+ next;
+ }
+ } else {
+ open( my $fh, $url ) or die "Can't open $url: $!";
+ @lines = <$fh>;
+ close $fh;
+ }
+ $lint->parse( $_ ) for @lines;
+ $lint->eof();
+ for my $error ( $lint->errors() ) {
+ print $error->as_string(), "\n";
+ if ( defined $context ) {
+ $context += 0;
+ my $lineno = $error->line - 1;
+
+ my $start = $lineno-$context;
+ $start = 0 if $start < 0;
+
+ my $end = $lineno+$context;
+ $end = $#lines if $end > $#lines;
+
+ print " $_\n" for @lines[$start..$end];
+ print "\n";
+ }
+ }
+ $lint->clear_errors();
+} # for files
+
+__END__
+Usage: weblint [filename or url]... (filename - reads STDIN)
+ --help This message
+ --context[=n] Show the offending line (and n surrounding lines)
+
+ Error types: (default: all on)
+ --[no]structure Structural issues, like unclosed tag pairs
+ --[no]helper Helper issues, like missing HEIGHT & WIDTH
+ --[no]fluff Fluff that can be removed, like bad tag attributes
+ --only Turns off all other error types, as in --only --fluff
+
510 lib/HTML/Lint.pm
@@ -0,0 +1,510 @@
+package HTML::Lint;
+
+=head1 NAME
+
+HTML::Lint - check for HTML errors in a string or file
+
+=head1 VERSION
+
+Version 2.02
+
+=cut
+
+our $VERSION = '2.02';
+
+=head1 SYNOPSIS
+
+ my $lint = HTML::Lint->new;
+ $lint->only_types( HTML::Lint::STRUCTURE );
+
+ $lint->parse( $data );
+ $lint->parse_file( $filename );
+
+ my $error_count = $lint->errors;
+
+ foreach my $error ( $lint->errors ) {
+ print $error->as_string, "\n";
+ }
+
+HTML::Lint also comes with a wrapper program called F<weblint> that handles
+linting from the command line:
+
+ $ weblint http://www.cnn.com/
+ http://www.cnn.com/ (395:83) <IMG SRC="spacer.gif"> tag has no HEIGHT and WIDTH attributes.
+ http://www.cnn.com/ (395:83) <IMG SRC="goofus.gif"> does not have ALT text defined
+ http://www.cnn.com/ (396:217) Unknown element <nobr>
+ http://www.cnn.com/ (396:241) </nobr> with no opening <nobr>
+ http://www.cnn.com/ (842:7) target attribute in <a> is repeated
+
+And finally, you can also get L<Apache::HTML::Lint> that passes any
+mod_perl-generated code through HTML::Lint and get it dumped into your
+Apache F<error_log>.
+
+ [Mon Jun 3 14:03:31 2002] [warn] /foo.pl (1:45) </p> with no opening <p>
+ [Mon Jun 3 14:03:31 2002] [warn] /foo.pl (1:49) Unknown element <gronk>
+ [Mon Jun 3 14:03:31 2002] [warn] /foo.pl (1:56) Unknown attribute "x" for tag <table>
+
+=cut
+
+use strict;
+use HTML::Lint::Error;
+
+=head1 METHODS
+
+NOTE: Some of these methods mirror L<HTML::Parser>'s methods, but HTML::Lint
+is not a subclass of HTML::Parser.
+
+=head2 new()
+
+Create an HTML::Lint object, which inherits from HTML::Parser.
+You may pass the types of errors you want to check for in the
+C<only_types> parm.
+
+ my $lint = HTML::Lint->new( only_types => HTML::Lint::Error::STRUCTURE );
+
+If you want more than one, you must pass an arrayref:
+
+ my $lint = HTML::Lint->new(
+ only_types => [HTML::Lint::Error::STRUCTURE, HTML::Lint::Error::FLUFF] );
+
+=cut
+
+sub new {
+ my $class = shift;
+ my %args = @_;
+
+ my $self = {
+ _errors => [],
+ _types => [],
+ };
+ $self->{_parser} = HTML::Lint::Parser->new( sub { $self->gripe( @_ ) } );
+ bless $self, $class;
+
+ if ( my $only = $args{only_types} ) {
+ $self->only_types( ref $only eq "ARRAY" ? @$only : $only );
+ delete $args{only_types};
+ }
+
+ warn "Unknown argument $_\n" for keys %args;
+
+ return $self;
+}
+
+=head2 $lint->parse( $chunk )
+
+=head2 $lint->parse( $code_ref )
+
+Passes in a chunk of HTML to be linted, either as a piece of text,
+or a code reference.
+See L<HTML::Parser>'s C<parse_file> method for details.
+
+=cut
+
+sub parse {
+ my $self = shift;
+ return $self->_parser->parse( @_ );
+}
+
+=head2 $lint->parse_file( $file )
+
+Analyzes HTML directly from a file. The C<$file> argument can be a filename,
+an open file handle, or a reference to an open file handle.
+See L<HTML::Parser>'s C<parse_file> method for details.
+
+=cut
+
+sub parse_file {
+ my $self = shift;
+ return $self->_parser->parse_file( @_ );
+}
+
+=head2 $lint->eof
+
+Signals the end of a block of text getting passed in. This must be
+called to make sure that all parsing is complete before looking at errors.
+
+Any parameters (and there shouldn't be any) are passed through to
+HTML::Parser's eof() method.
+
+=cut
+
+sub eof {
+ my $self = shift;
+
+ my $rc;
+ my $parser = $self->_parser;
+ if ( $parser ) {
+ $rc = $self->_parser->eof(@_);
+ delete $self->{_parser};
+ }
+
+ return $rc;
+}
+
+=head2 $lint->errors()
+
+In list context, C<errors> returns all of the errors found in the
+parsed text. Each error is an object of the type L<HTML::Lint::Error>.
+
+In scalar context, it returns the number of errors found.
+
+=cut
+
+sub errors {
+ my $self = shift;
+
+ if ( wantarray ) {
+ return @{$self->{_errors}};
+ }
+ else {
+ return scalar @{$self->{_errors}};
+ }
+}
+
+=head2 $lint->clear_errors()
+
+Clears the list of errors, in case you want to print and clear, print and clear.
+
+=cut
+
+sub clear_errors {
+ my $self = shift;
+
+ $self->{_errors} = [];
+}
+
+=head2 $lint->only_types( $type1[, $type2...] )
+
+Specifies to only want errors of a certain type.
+
+ $lint->only_types( HTML::Lint::Error::STRUCTURE );
+
+Calling this without parameters makes the object return all possible
+errors.
+
+The error types are C<STRUCTURE>, C<HELPER> and C<FLUFF>.
+See L<HTML::Lint::Error> for details on these types.
+
+=cut
+
+sub only_types {
+ my $self = shift;
+
+ $self->{_types} = [@_];
+}
+
+=head2 $lint->gripe( $errcode, [$key1=>$val1, ...] )
+
+Adds an error message, in the form of an L<HTML::Lint::Error> object,
+to the list of error messages for the current object. The file,
+line and column are automatically passed to the L<HTML::Lint::Error>
+constructor, as well as whatever other key value pairs are passed.
+
+For example:
+
+ $lint->gripe( 'attr-repeated', tag => $tag, attr => $attr );
+
+Usually, the user of the object won't call this directly, but just
+in case, here you go.
+
+=cut
+
+sub gripe {
+ my $self = shift;
+
+ my $parser = $self->_parser;
+
+ my $error = new HTML::Lint::Error(
+ $self->{_file}, $parser->{_line}, $parser->{_column}, @_ );
+
+ my @keeps = @{$self->{_types}};
+ if ( !@keeps || $error->is_type(@keeps) ) {
+ push( @{$self->{_errors}}, $error );
+ }
+}
+
+=head2 $lint->newfile( $filename )
+
+Call C<newfile()> whenever you switch to another file in a batch of
+linting. Otherwise, the object thinks everything is from the same file.
+Note that the list of errors is NOT cleared.
+
+Note that I<$filename> does NOT need to match what's put into parse()
+or parse_file(). It can be a description, a URL, or whatever.
+
+=cut
+
+sub newfile {
+ my $self = shift;
+ my $file = shift;
+
+ delete $self->{_parser};
+ $self->{_parser} = HTML::Lint::Parser->new( sub { $self->gripe( @_ ) } );
+ $self->{_file} = $file;
+ $self->{_line} = 0;
+ $self->{_column} = 0;
+ $self->{_first_seen} = {};
+
+ return $self->{_file};
+} # newfile
+
+sub _parser {
+ my $self = shift;
+
+ return $self->{_parser};
+}
+
+=pod
+
+HTML::Lint::Parser is a class only for this module.
+
+=cut
+
+package HTML::Lint::Parser;
+
+use HTML::Parser 3.20;
+use HTML::Tagset 3.03;
+
+use HTML::Lint::HTML4 qw( %isKnownAttribute %isRequired %isNonrepeatable %isObsolete );
+use HTML::Entities qw( %char2entity );
+
+our @ISA = qw( HTML::Parser );
+
+sub new {
+ my $class = shift;
+ my $gripe = shift;
+
+ my $self =
+ HTML::Parser->new(
+ api_version => 3,
+ start_document_h => [ \&_start_document, 'self' ],
+ end_document_h => [ \&_end_document, 'self,line,column' ],
+ start_h => [ \&_start, 'self,tagname,line,column,@attr' ],
+ end_h => [ \&_end, 'self,tagname,line,column,@attr' ],
+ text_h => [ \&_text, 'self,text' ],
+ strict_names => 1,
+ );
+ bless $self, $class;
+
+ $self->{_gripe} = $gripe;
+ $self->{_stack} = [];
+
+ return $self;
+}
+
+sub gripe {
+ my $self = shift;
+
+ return $self->{_gripe}->( @_ );
+}
+
+sub _start_document {
+ my $self = shift;
+}
+
+sub _end_document {
+ my ($self,$line,$column) = @_;
+
+ for my $tag ( keys %isRequired ) {
+ if ( !$self->{_first_seen}->{$tag} ) {
+ $self->gripe( 'doc-tag-required', tag => $tag );
+ }
+ }
+}
+
+sub _start {
+ my ($self,$tag,$line,$column,@attr) = @_;
+
+ $self->{_line} = $line;
+ $self->{_column} = $column;
+
+ my $validattr = $isKnownAttribute{ $tag };
+ if ( $validattr ) {
+ my %seen;
+ my $i = 0;
+ while ( $i < @attr ) {
+ my ($attr,$val) = @attr[$i++,$i++];
+ if ( $seen{$attr}++ ) {
+ $self->gripe( 'attr-repeated', tag => $tag, attr => $attr );
+ }
+
+ if ( $validattr && ( !$validattr->{$attr} ) ) {
+ $self->gripe( 'attr-unknown', tag => $tag, attr => $attr );
+ }
+ } # while attribs
+ }
+ else {
+ $self->gripe( 'elem-unknown', tag => $tag );
+ }
+ $self->_element_push( $tag ) unless $HTML::Tagset::emptyElement{ $tag };
+
+ if ( my $where = $self->{_first_seen}{$tag} ) {
+ if ( $isNonrepeatable{$tag} ) {
+ $self->gripe( 'elem-nonrepeatable',
+ tag => $tag,
+ where => HTML::Lint::Error::where(@$where)
+ );
+ }
+ }
+ else {
+ $self->{_first_seen}{$tag} = [$line,$column];
+ }
+
+ # Call any other overloaded func
+ my $tagfunc = "_start_$tag";
+ if ( $self->can($tagfunc) ) {
+ $self->$tagfunc( $tag, @attr );
+ }
+}
+
+sub _text {
+ my ($self,$text) = @_;
+
+ while ( $text =~ /([^\x09\x0A\x0D -~])/g ) {
+ my $bad = $1;
+ $self->gripe(
+ 'text-use-entity',
+ char => sprintf( '\x%02lX', ord($bad) ),
+ entity => $char2entity{ $bad },
+ );
+ }
+}
+
+sub _end {
+ my ($self,$tag,$line,$column,@attr) = @_;
+
+ $self->{_line} = $line;
+ $self->{_column} = $column;
+
+ if ( $HTML::Tagset::emptyElement{ $tag } ) {
+ $self->gripe( 'elem-empty-but-closed', tag => $tag );
+ }
+ else {
+ if ( $self->_in_context($tag) ) {
+ my @leftovers = $self->_element_pop_back_to($tag);
+ for ( @leftovers ) {
+ my ($tag,$line,$col) = @$_;
+ $self->gripe( 'elem-unclosed', tag => $tag,
+ where => HTML::Lint::Error::where($line,$col) )
+ unless $HTML::Tagset::optionalEndTag{$tag};
+ } # for
+ }
+ else {
+ $self->gripe( 'elem-unopened', tag => $tag );
+ }
+ } # is empty element
+
+ # Call any other overloaded func
+ my $tagfunc = "_end_$tag";
+ if ( $self->can($tagfunc) ) {
+ $self->$tagfunc( $tag, $line );
+ }
+}
+
+sub _element_push {
+ my $self = shift;
+ for ( @_ ) {
+ push( @{$self->{_stack}}, [$_,$self->{_line},$self->{_column}] );
+ } # while
+}
+
+sub _find_tag_in_stack {
+ my $self = shift;
+ my $tag = shift;
+ my $stack = $self->{_stack};
+
+ my $offset = @$stack - 1;
+ while ( $offset >= 0 ) {
+ if ( $stack->[$offset][0] eq $tag ) {
+ return $offset;
+ }
+ --$offset;
+ } # while
+
+ return;
+}
+
+sub _element_pop_back_to {
+ my $self = shift;
+ my $tag = shift;
+
+ my $offset = $self->_find_tag_in_stack($tag) or return;
+
+ my @leftovers = splice( @{$self->{_stack}}, $offset + 1 );
+ pop @{$self->{_stack}};
+
+ return @leftovers;
+}
+
+sub _in_context {
+ my $self = shift;
+ my $tag = shift;
+
+ my $offset = $self->_find_tag_in_stack($tag);
+ return defined $offset;
+}
+
+# Overridden tag-specific stuff
+sub _start_img {
+ my ($self,$tag,%attr) = @_;
+
+ my ($h,$w,$src) = @attr{qw( height width src )};
+ if ( defined $h && defined $w ) {
+ # Check sizes
+ }
+ else {
+ $self->gripe( "elem-img-sizes-missing", src=>$src );
+ }
+ if ( not defined $attr{alt} ) {
+ $self->gripe( "elem-img-alt-missing", src=>$src );
+ }
+}
+
+=head1 BUGS, WISHES AND CORRESPONDENCE
+
+Please feel free to email me at andy@petdance.com. I'm glad to help as
+best I can, and I'm always interested in bugs, suggestions and patches.
+
+Please report any bugs or feature requests to
+C<< <bug-html-lint at rt.cpan.org> >>, or through the web interface at
+L<http://rt.cpan.org>. I will be notified, and then you'll automatically
+be notified of progress on your bug as I make changes.
+
+=head1 TODO
+
+=over 4
+
+=item * Check for attributes that require values
+
+=item * <TABLE>s that have no rows.
+
+=item * Form fields that aren't in a FORM
+
+=item * Check for valid entities, and that they end with semicolons
+
+=item * DIVs with nothing in them.
+
+=item * HEIGHT= that have percents in them.
+
+=item * Check for goofy stuff like:
+
+ <b><li></b><b>Hello Reader - Spanish Level 1 (K-3)</b>
+
+=back
+
+=head1 LICENSE
+
+Copyright 2005 Andy Lester, All Rights Reserved.
+
+This program is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
+Please note that these modules are not products of or supported by the
+employers of the various contributors to the code.
+
+=head1 AUTHOR
+
+Andy Lester, andy at petdance.com
+
+=cut
+
297 lib/HTML/Lint/Error.pm
@@ -0,0 +1,297 @@
+package HTML::Lint::Error;
+
+use strict;
+
+use base 'Exporter';
+our @EXPORT = ();
+our @EXPORT_OK = qw( STRUCTURE HELPER FLUFF );
+our %EXPORT_TAGS = ( types => [@EXPORT_OK] );
+
+our %errors;
+
+=head1 NAME
+
+HTML::Lint::Error - Error object for the Lint functionality
+
+=head1 SYNOPSIS
+
+See L<HTML::Lint> for all the gory details.
+
+=head1 EXPORTS
+
+None. It's all object-based.
+
+=head1 METHODS
+
+Almost everything is an accessor.
+
+=head1 Error types: C<STRUCTURE>, C<HELPER>, C<FLUFF>
+
+Each error has a type. Note that these roughly, but not exactly, go
+from most severe to least severe.
+
+=over 4
+
+=item * C<STRUCTURE>
+
+For problems that relate to the structural validity of the code.
+Examples: Unclosed <TABLE> tags, incorrect values for attributes, and
+repeated attributes.
+
+=item * C<HELPER>
+
+Helpers are notes that will help you with your HTML, or that will help
+the browser render the code better or faster. Example: Missing HEIGHT
+and WIDTH attributes in an IMG tag.
+
+=item * C<FLUFF>
+
+Fluff is for items that don't hurt your page, but don't help it either.
+This is usually something like an unknown attribute on a tag.
+
+=back
+
+=cut
+
+use constant STRUCTURE => 1;
+use constant HELPER => 2;
+use constant FLUFF => 3;
+
+=head2 new()
+
+Create an object. It's not very exciting.
+
+=cut
+
+sub new {
+ my $class = shift;
+
+ my $file = shift;
+ my $line = shift;
+ my $column = shift;
+ my $errcode = shift;
+ my @errparms = @_;
+
+ # Add an element that says what tag caused the error (B, TR, etc)
+ # so that we can match 'em up down the road.
+ my $self = {
+ _file => $file,
+ _line => $line,
+ _column => $column,
+ _errcode => $errcode,
+ _errtext => undef,
+ _type => undef,
+ };
+
+ bless $self, $class;
+
+ $self->_expand_error( $errcode, @errparms );
+
+ return $self;
+}
+
+sub _expand_error {
+ my $self = shift;
+
+ my $errcode = shift;
+
+ my $specs = $errors{$errcode};
+ my $str;
+ if ( $specs ) {
+ ($str, $self->{_type}) = @$specs;
+ }
+ else {
+ $str = "Unknown code: $errcode";
+ }
+
+ while ( @_ ) {
+ my $var = shift;
+ my $val = shift;
+ $str =~ s/\$\{$var\}/$val/g;
+ }
+
+ $self->{_errtext} = $str;
+}
+
+=head2 is_type( $type1 [, $type2 ] )
+
+Tells if any of I<$type1>, I<$type2>... match the error's type.
+Returns the type that matched.
+
+ if ( $err->is_type( HTML::Lint::Error::STRUCTURE ) ) {....
+
+=cut
+
+sub is_type {
+ my $self = shift;
+
+ for my $matcher ( @_ ) {
+ return $matcher if $matcher eq $self->type;
+ }
+
+ return;
+}
+
+=head2 where()
+
+Returns a formatted string that describes where in the file the
+error has occurred.
+
+For example,
+
+ (14:23)
+
+for line 14, column 23.
+
+The terrible thing about this function is that it's both a plain
+ol' formatting function as in
+
+ my $str = where( 14, 23 );
+
+AND it's an object method, as in:
+
+ my $str = $error->where();
+
+I don't know what I was thinking when I set it up this way, but
+it's bad practice.
+
+=cut
+
+sub where {
+ my $line;
+ my $col;
+
+ if ( not ref $_[0] ) {
+ $line = shift;
+ $col = shift;
+ } else {
+ my $self = shift;
+ $line = $self->line;
+ $col = $self->column;
+ }
+ $col ||= 0;
+ return sprintf( "(%s:%s)", $line, $col + 1 );
+}
+
+=head2 as_string()
+
+Returns a nicely-formatted string for printing out to stdout or some similar user thing.
+
+=cut
+
+sub as_string {
+ my $self = shift;
+
+ return sprintf( "%s %s %s", $self->file, $self->where, $self->errtext );
+}
+
+=head2 file()
+
+Returns the filename of the error, as set by the caller.
+
+=head2 line()
+
+Returns the line number of the error.
+
+=head2 column()
+
+Returns the column number, starting from 0
+
+=head2 errcode()
+
+Returns the HTML::Lint error code. Don't rely on this, because it will probably go away.
+
+=head2 errtext()
+
+Descriptive text of the error
+
+=head2 type()
+
+Type of the error
+
+=cut
+
+sub file { my $self = shift; return $self->{_file} || '' }
+sub line { my $self = shift; return $self->{_line} || '' }
+sub column { my $self = shift; return $self->{_column} || '' }
+sub errcode { my $self = shift; return $self->{_errcode} || '' }
+sub errtext { my $self = shift; return $self->{_errtext} || '' }
+sub type { my $self = shift; return $self->{_type} || '' }
+
+
+=head1 TODO
+
+None, other than incorporating more errors, as driven by HTML::Lint.
+
+=head1 LICENSE
+
+This code may be distributed under the same terms as Perl itself.
+
+Please note that these modules are not products of or supported by the
+employers of the various contributors to the code.
+
+=head1 AUTHOR
+
+Andy Lester, C<andy at petdance.com>
+
+=cut
+
+
+# Errors that are commented out have not yet been implemented.
+
+# Generic element stuff
+%errors = (
+ 'elem-unknown' => ['Unknown element <${tag}>', STRUCTURE],
+ 'elem-unopened' => ['</${tag}> with no opening <${tag}>', STRUCTURE],
+ 'elem-unclosed' => ['<${tag}> at ${where} is never closed', STRUCTURE],
+ 'elem-empty-but-closed' => ['<${tag}> is not a container -- </${tag}> is not allowed', STRUCTURE],
+
+ 'elem-img-sizes-missing' => ['<IMG SRC="${src}"> tag has no HEIGHT and WIDTH attributes.', HELPER],
+ 'elem-img-alt-missing' => ['<IMG SRC="${src}"> does not have ALT text defined', HELPER],
+ 'elem-nonrepeatable' => ['<${tag}> is not repeatable, but already appeared at ${where}', STRUCTURE],
+
+ 'doc-tag-required' => ['<${tag}> tag is required', STRUCTURE],
+
+ 'attr-repeated' => ['${attr} attribute in <${tag}> is repeated', STRUCTURE],
+ 'attr-unknown' => ['Unknown attribute "${attr}" for tag <${tag}>', FLUFF],
+
+ 'text-use-entity' => ['Invalid character ${char} should be written as ${entity}', STRUCTURE],
+);
+
+1; # happy
+
+__DATA__
+Errors that haven't been done yet.
+
+#elem-head-only <${tag}> can only appear in the <HEAD> element
+#elem-non-head-element <${tag}> cannot appear in the <HEAD> element
+#elem-obsolete <${tag}> is obsolete
+#elem-nested-element <${tag}> cannot be nested -- one is already opened at ${where}
+#elem-wrong-context Illegal context for <${tag}> -- must appear in <${othertag}> tag.
+#elem-heading-in-anchor <A> should be inside <${tag}>, not <${tag}> inside <A>
+
+#elem-head-missing No <HEAD> element found
+#elem-head-missing-title No <TITLE> in <HEAD> element
+#elem-img-sizes-incorrect <IMG> tag's HEIGHT and WIDTH attributes are incorrect. They should be ${correct}.
+#attr-missing <${tag}> is missing a "${attr}" attribute
+
+#comment-unclosed Unclosed comment
+#comment-markup Markup embedded in a comment can confuse some browsers
+
+#text-literal-metacharacter Metacharacter $char should be represented as "$otherchar"
+#text-title-length The HTML spec recommends that that <TITLE> be no more than 64 characters
+#text-markup Tag <${tag}> found in the <TITLE>, which will not be rendered properly.
+
+#elem-physical-markup <${tag}> is physical font markup. Use logical (such as <${othertag}>) instead.
+#elem-leading-whitespace <${tag}> should not have whitespace between "<" and "${tag}>"
+#'must-follow' => [ ENABLED, MC_ERROR, '<$argv[0]> must immediately follow <$argv[1]>', ],
+# 'empty-container' => [ ENABLED, MC_WARNING, 'empty container element <$argv[0]>.', ],
+# 'directory-index' => [ ENABLED, MC_WARNING, 'directory $argv[0] does not have an index file ($argv[1])', ],
+# 'attribute-delimiter' => [ ENABLED, MC_WARNING, 'use of \' for attribute value delimiter is not supported by all browsers (attribute $argv[0] of tag $argv[1])', ],
+# 'container-whitespace' => [ DISABLED, MC_WARNING, '$argv[0] whitespace in content of container element $argv[1]', ],
+# 'bad-text-context' => [ ENABLED, MC_ERROR, 'illegal context, <$argv[0]>, for text; should be in $argv[1].', ],
+# 'attribute-format' => [ ENABLED, MC_ERROR, 'illegal value for $argv[0] attribute of $argv[1] ($argv[2])', ],
+# 'quote-attribute-value' => [ ENABLED, MC_ERROR, 'value for attribute $argv[0] ($argv[1]) of element $argv[2] should be quoted (i.e. $argv[0]="$argv[1]")', ],
+# 'meta-in-pre' => [ ENABLED, MC_ERROR, 'you should use "$argv[0]" in place of "$argv[1]", even in a PRE element.', ],
+# 'implied-element' => [ ENABLED, MC_WARNING, 'saw <$argv[0]> element, but no <$argv[1]> element', ],
+# 'button-usemap' => [ ENABLED, MC_ERROR, 'illegal to associate an image map with IMG inside a BUTTON', ],
277 lib/HTML/Lint/HTML4.pm
@@ -0,0 +1,277 @@
+package HTML::Lint::HTML4;
+
+use strict;
+
+use base 'Exporter';
+our @EXPORT_OK = qw( %isKnownAttribute %isRequired %isNonrepeatable %isObsolete );
+
+sub _hash(@) { my %hash; $hash{$_} = 1 for @_; return \%hash; }
+
+our @physical = qw( b big code i kbd s small strike sub sup tt u xmp );
+our @content = qw( abbr acronym cite code dfn em kbd samp strong var );
+
+our @core = qw( class id style title );
+our @i18n = qw( dir lang );
+our @events = qw( onclick ondblclick onkeydown onkeypress onkeyup
+ onmousedown onmousemove onmouseout onmouseover onmouseup );
+our @std = (@core,@i18n,@events);
+
+our %isRequired = %{_hash( qw( html body head title ) )};
+our %isNonrepeatable = %{_hash( qw( html head base title body isindex ))};
+our %isObsolete = %{_hash( qw( listing plaintext xmp ) )};
+
+# Some day I might do something with these. For now, they're just comments.
+sub _ie_only { return @_ };
+sub _ns_only { return @_ };
+
+our %isKnownAttribute = (
+ # All the physical markup has the same
+ (map { $_=>_hash(@std) } (@physical, @content) ),
+
+ a => _hash( @std, qw( accesskey charset coords href hreflang name onblur onfocus rel rev shape tabindex target type ) ),
+ address => _hash( @std ),
+ applet => _hash( @std ),
+ area => _hash( @std, qw( accesskey alt coords href nohref onblur onfocus shape tabindex target ) ),
+ base => _hash( qw( href target ) ),
+ basefont => _hash( qw( color face id size ) ),
+ bdo => _hash( @core, @i18n ),
+ blockquote => _hash( @std, qw( cite ) ),
+ body => _hash( @std,
+ qw( alink background bgcolor link marginheight marginwidth onload onunload text vlink ),
+ _ie_only( qw( bgproperties leftmargin topmargin ) )
+ ),
+ br => _hash( @core, qw( clear ) ),
+ button => _hash( @std, qw( accesskey disabled name onblur onfocus tabindex type value ) ),
+ caption => _hash( @std, qw( align ) ),
+ center => _hash( @std ),
+ cite => _hash(),
+ col => _hash( @std, qw( align char charoff span valign width ) ),
+ colgroup => _hash( @std, qw( align char charoff span valign width ) ),
+ del => _hash( @std, qw( cite datetime ) ),
+ div => _hash( @std, qw( align ) ),
+ dir => _hash( @std, qw( compact ) ),
+ dd => _hash( @std ),
+ dl => _hash( @std, qw( compact ) ),
+ dt => _hash( @std ),
+ embed => _hash(
+ qw( align height hidden name palette quality play src units width ),
+ _ns_only( qw( border hspace pluginspage type vspace ) ),
+ ),
+ fieldset => _hash( @std ),
+ font => _hash( @core, @i18n, qw( color face size ) ),
+ form => _hash( @std, qw( accept-charset action enctype method name onreset onsubmit target ) ),
+ frame => _hash( @core, qw( frameborder longdesc marginheight marginwidth name noresize scrolling src ) ),
+ frameset => _hash( @core, qw( cols onload onunload rows ) ),
+ h1 => _hash( @std, qw( align ) ),
+ h2 => _hash( @std, qw( align ) ),
+ h3 => _hash( @std, qw( align ) ),
+ h4 => _hash( @std, qw( align ) ),
+ h5 => _hash( @std, qw( align ) ),
+ h6 => _hash( @std, qw( align ) ),
+ head => _hash( @i18n, qw( profile ) ),
+ hr => _hash( @core, @events, qw( align noshade size width ) ),
+ html => _hash( @i18n, qw( version xmlns xml:lang ) ),
+ iframe => _hash( @core, qw( align frameborder height longdesc marginheight marginwidth name scrolling src width ) ),
+ img => _hash( @std, qw( align alt border height hspace ismap longdesc name src usemap vspace width ) ),
+ input => _hash( @std, qw( accept accesskey align alt border checked disabled maxlength name onblur onchange onfocus onselect readonly size src tabindex type usemap value ) ),
+ ins => _hash( @std, qw( cite datetime ) ),
+ isindex => _hash( @core, @i18n, qw( prompt ) ),
+ label => _hash( @std, qw( accesskey for onblur onfocus ) ),
+ legend => _hash( @std, qw( accesskey align ) ),
+ li => _hash( @std, qw( type value ) ),
+ 'link' => _hash( @std, qw( charset href hreflang media rel rev target type ) ),
+ listing => _hash(),
+ 'map' => _hash( @std, qw( name ) ),
+ menu => _hash( @std, qw( compact ) ),
+ meta => _hash( @i18n, qw( content http-equiv name scheme ) ),
+ nobr => _hash( @std ),
+ noframes => _hash( @std ),
+ noscript => _hash( @std ),
+ object => _hash( @std, qw( align archive border classid codebase codetype data declare height hspace name standby tabindex type usemap vspace width )),
+ ol => _hash( @std, qw( compact start type ) ),
+ optgroup => _hash( @std, qw( disabled label ) ),
+ option => _hash( @std, qw( disabled label selected value ) ),
+ p => _hash( @std, qw( align ) ),
+ param => _hash( qw( id name type value valuetype ) ),
+ plaintext => _hash(),
+ pre => _hash( @std, qw( width ) ),
+ q => _hash( @std, qw( cite ) ),
+ script => _hash( qw( charset defer event for language src type ) ),
+ 'select' => _hash( @std, qw( disabled multiple name onblur onchange onfocus size tabindex ) ),
+ span => _hash( @std ),
+ strong => _hash(),
+ style => _hash( @i18n, qw( media title type ) ),
+ table => _hash( @std,
+ qw( align bgcolor border cellpadding cellspacing datapagesize frame rules summary width ),
+ _ie_only( qw( background bordercolor bordercolordark bordercolorlight ) ),
+ _ns_only( qw( bordercolor cols height hspace vspace ) ),
+ ),
+ tbody => _hash( @std, qw( align char charoff valign ) ),
+ td => _hash( @std,
+ qw( abbr align axis bgcolor char charoff colspan headers height nowrap rowspan scope valign width ),
+ _ie_only( qw( background bordercolor bordercolordark bordercolorlight ) ),
+ ),
+ textarea => _hash( @std, qw( accesskey cols disabled name onblur onchange onfocus onselect readonly rows tabindex ) ),
+ th => _hash( @std,
+ qw( abbr align axis bgcolor char charoff colspan headers height nowrap rowspan scope valign width ),
+ _ie_only( qw( background bordercolor bordercolordark bordercolorlight ) ),
+ ),
+ thead => _hash( @std, qw( align char charoff valign ) ),
+ tfoot => _hash( @std, qw( align char charoff valign ) ),
+ title => _hash( @i18n ),
+ tr => _hash( @std,
+ qw( align bgcolor char charoff valign ),
+ _ie_only( qw( bordercolor bordercolordark bordercolorlight nowrap ) ),
+ _ns_only( qw( nowrap ) ),
+ ),
+ ul => _hash( @std, qw( compact type ) ),
+);
+
+=for oldobsoletestuffthatIwanttokeep
+my %booger = (
+ 'maybePaired' => 'LI DT DD P TD TH TR OPTION COLGROUP THEAD TFOOT TBODY COL',
+
+ 'expectArgsRE' => 'A|FONT',
+
+ 'headTagsRE' => 'TITLE|NEXTID|LINK|BASE|META',
+
+ 'requiredContext' =>
+ {
+ 'AREA' => 'MAP',
+ 'CAPTION' => 'TABLE',
+ 'DD' => 'DL',
+ 'DT' => 'DL',
+ 'FIELDSET' => 'FORM',
+ 'FRAME' => 'FRAMESET',
+ 'INPUT' => 'FORM',
+ 'LABEL' => 'FORM',
+ 'LEGEND' => 'FIELDSET',
+ 'LI' => 'DIR|MENU|OL|UL',
+ 'NOFRAMES' => 'FRAMESET',
+ 'OPTGROUP' => 'SELECT',
+ 'OPTION' => 'SELECT',
+ 'SELECT' => 'FORM',
+ 'TD' => 'TR',
+ 'TEXTAREA' => 'FORM',
+ 'TH' => 'TR',
+ 'TR' => 'TABLE',
+ 'PARAM' => 'APPLET|OBJECT',
+ },
+
+ 'okInHead' =>
+ {
+ 'ISINDEX' => 1,
+ 'TITLE' => 1,
+ 'NEXTID' => 1,
+ 'LINK' => 1,
+ 'BASE' => 1,
+ 'META' => 1,
+ 'RANGE' => 1,
+ 'STYLE' => 1,
+ 'OBJECT' => 1,
+ '!--' => 1,
+ },
+
+
+ ## elements which cannot be nested
+ 'nonNest' => 'A|FORM',
+
+ 'requiredAttributes' =>
+ {
+ APPLET => 'WIDTH|HEIGHT',
+ AREA => 'ALT',
+ BASE => 'HREF',
+ BASEFONT => 'SIZE',
+ BDO => 'DIR',
+ FORM => 'ACTION',
+ IMG => 'SRC|ALT',
+ LINK => 'HREF',
+ MAP => 'NAME',
+ NEXTID => 'N',
+ SELECT => 'NAME',
+ TEXTAREA => 'NAME|ROWS|COLS'
+ },
+
+ 'attributeFormat' =>
+ {
+ 'ALIGN', 'BOTTOM|MIDDLE|TOP|LEFT|CENTER|RIGHT|JUSTIFY|'.
+ 'BLEEDLEFT|BLEEDRIGHT|DECIMAL',
+ 'ALINK' => 'color',
+ 'BGCOLOR' => 'color',
+ 'CLEAR', 'LEFT|RIGHT|ALL|NONE',
+ 'COLOR' => 'color',
+ 'COLS', '\d+|(\d*[*%]?,)*\s*\d*[*%]?',
+ 'COLSPAN', '\d+',
+ 'DIR' => 'LTR|RTL',
+ 'HEIGHT', '\d+',
+ 'INDENT', '\d+',
+ 'LINK' => 'color',
+ 'MAXLENGTH', '\d+',
+ 'METHOD', 'GET|POST',
+ 'ROWS', '\d+|(\d*[*%]?,)*\s*\d*[*%]?',
+ 'ROWSPAN', '\d+',
+ 'SEQNUM', '\d+',
+ 'SIZE', '[-+]?\d+|\d+,\d+',
+ 'SKIP', '\d+',
+ 'TYPE', 'CHECKBOX|HIDDEN|IMAGE|PASSWORD|RADIO|RESET|'.
+ 'SUBMIT|TEXT|[AaIi1]|disc|square|circle|'.
+ 'FILE|.*',
+ 'UNITS', 'PIXELS|EN',
+ 'VALIGN', 'TOP|MIDDLE|BOTTOM|BASELINE',
+ 'VLINK' => 'color',
+ 'WIDTH', '\d+%?',
+ 'WRAP', 'OFF|VIRTUAL|PHYSICAL',
+ 'X', '\d+',
+ 'Y', '\d+'
+ },
+
+ 'badTextContext' =>
+ {
+ 'HEAD', 'BODY, or TITLE perhaps',
+ 'UL', 'LI or LH',
+ 'OL', 'LI or LH',
+ 'DL', 'DT or DD',
+ 'TABLE', 'TD or TH',
+ 'TR', 'TD or TH'
+ },
+
+ 'bodyColorAttributes' =>
+ [
+ qw(BGCOLOR TEXT LINK ALINK VLINK)
+ ],
+
+);
+=cut
+
+1;
+
+__END__
+
+=head1 NAME
+
+HTML::Lint::HTML4.pm -- Rules for HTML 4 as used by HTML::Lint.
+
+=head1 SYNOPSIS
+
+No user serviceable parts inside. Used by HTML::Lint.
+
+=head1 SEE ALSO
+
+=over 4
+
+=item HTML::Lint
+
+=back
+
+=head1 AUTHOR
+
+Andy Lester C<andy at petdance.com>
+
+=head1 COPYRIGHT
+
+Copyright (c) Andy Lester 2005. All Rights Reserved.
+
+This module is free software; you can redistribute it and/or
+modify it under the same terms as Perl itself.
+
+=cut
156 lib/Test/HTML/Lint.pm
@@ -0,0 +1,156 @@
+package Test::HTML::Lint;
+
+use strict;
+
+use Test::Builder;
+use Exporter;
+
+use HTML::Lint 2.02;
+
+use vars qw( @ISA $VERSION @EXPORT );
+
+@ISA = qw( HTML::Parser Exporter );
+
+=head1 NAME
+
+Test::HTML::Lint - Test::More-style wrapper around HTML::Lint
+
+=head1 VERSION
+
+Version 2.02
+
+=cut
+
+$VERSION = '2.02';
+
+my $Tester = Test::Builder->new;
+
+=head1 SYNOPSIS
+
+ use Test::HTML::Lint tests => 4;
+
+ my $table = build_display_table();
+ html_ok( $table, 'Built display table properly' );
+
+=head1 DESCRIPTION
+
+This module provides a few convenience methods for testing exception
+based code. It is built with L<Test::Builder> and plays happily with
+L<Test::More> and friends.
+
+If you are not already familiar with L<Test::More> now would be the time
+to go take a look.
+
+=head1 EXPORT
+
+C<html_ok>
+
+=cut
+
+@EXPORT = qw( html_ok );
+
+sub import {
+ my $self = shift;
+ my $pack = caller;
+
+ $Tester->exported_to($pack);
+ $Tester->plan(@_);
+
+ $self->export_to_level(1, $self, @EXPORT);
+}
+
+=head2 html_ok( [$lint, ] $html, $name )
+
+Checks to see that C<$html> contains valid HTML.
+
+Checks to see if C<$html> contains valid HTML. C<$html> being blank is OK.
+C<$html> being undef is not.
+
+If you pass an HTML::Lint object, C<html_ok()> will use that for its
+settings.
+
+ my $lint = new HTML::Lint( only_types => STRUCTURE );
+ html_ok( $lint, $content, "Web page passes structural tests only" );
+
+Otherwise, it will use the default rules.
+
+ html_ok( $content, "Web page passes ALL tests" );
+
+Note that if you pass in your own HTML::Lint object, C<html_ok()>
+will clear its errors before using it.
+
+=cut
+
+sub html_ok {
+ my $lint;
+
+ if ( ref($_[0]) eq "HTML::Lint" ) {
+ $lint = shift;
+ $lint->newfile();
+ $lint->clear_errors();
+ } else {
+ $lint = HTML::Lint->new;
+ }
+ my $html = shift;
+ my $name = shift;
+
+ my $ok = defined $html;
+ if ( !$ok ) {
+ $Tester->ok( 0, $name );
+ } else {
+ $lint->parse( $html );
+ my $nerr = scalar $lint->errors;
+ $ok = !$nerr;
+ $Tester->ok( $ok, $name );
+ if ( !$ok ) {
+ my $msg = "Errors:";
+ $msg .= " $name" if $name;
+ $Tester->diag( $msg );
+ $Tester->diag( $_->as_string ) for $lint->errors;
+ }
+ }
+
+ return $ok;
+}
+
+=head1 BUGS
+
+Please report any bugs or feature requests to C<bug-html-lint@rt.cpan.org>,
+or through the web interface at L<http://rt.cpan.org>. I will be
+notified, and then you'll automatically be notified of progress on
+your bug as I make changes.
+
+=head1 TO DO
+
+There needs to be a C<html_table_ok()> to check that the HTML is a
+self-contained, well-formed table, and then a comparable one for
+C<html_page_ok()>.
+
+If you think this module should do something that it doesn't do at the
+moment please let me know.
+
+=head1 ACKNOWLEGEMENTS
+
+Thanks to chromatic and Michael G Schwern for the excellent Test::Builder,
+without which this module wouldn't be possible.
+
+Thanks to Adrian Howard for writing Test::Exception, from which most of
+this module is taken.
+
+=head1 LICENSE
+
+Copyright 2003 Andy Lester, All Rights Reserved.
+
+This program is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
+Please note that these modules are not products of or supported by the
+employers of the various contributors to the code.
+
+=head1 AUTHOR
+
+Andy Lester, C<andy@petdance.com>
+
+=cut
+
+1;
13 t/00-load.t
@@ -0,0 +1,13 @@
+#!perl -Tw
+
+use Test::More tests => 2;
+
+BEGIN {
+ use_ok( 'HTML::Lint' );
+}
+
+BEGIN {
+ use_ok( 'Test::HTML::Lint' );
+}
+
+diag( "Testing HTML::Lint $HTML::Lint::VERSION" );
16 t/01-coverage.t
@@ -0,0 +1,16 @@
+#!perl -Tw
+
+use Test::More 'no_plan';
+
+BEGIN {
+ use_ok( 'HTML::Lint::Error' );
+}
+
+my @errors = do { no warnings; keys %HTML::Lint::Error::errors };
+
+isnt( scalar @errors, 0, 'There are at least some errors to be found.' );
+
+for my $error ( @errors ) {
+ my $filename = "t/$error.t";
+ ok( -e $filename, "$filename exists" );
+}
15 t/02-versions.t
@@ -0,0 +1,15 @@
+#!perl -Tw
+
+use warnings;
+use strict;
+
+use Test::More tests => 3;
+
+BEGIN {
+ use_ok( 'HTML::Lint' );
+}
+BEGIN {
+ use_ok( 'Test::HTML::Lint' );
+}
+
+is( $HTML::Lint::VERSION, $Test::HTML::Lint::VERSION, "HTML::Lint and Test::HTML::Lint versions match" );
20 t/10-test-html-lint.t
@@ -0,0 +1,20 @@
+#!perl -Tw
+
+use warnings;
+use strict;
+
+use Test::More tests => 4;
+use Test::HTML::Lint;
+
+BEGIN {
+ use_ok( 'Test::HTML::Lint' );
+}
+
+my $chunk = "<P>This is a fine chunk of code</P>";
+
+TODO: { # undef should fail
+ local $TODO = "This test should NOT succeed";
+ html_ok( undef );
+}
+html_ok( '' ); # Blank is OK
+html_ok( $chunk );
20 t/11-test-html-lint-overload.t
@@ -0,0 +1,20 @@
+#!perl -Tw
+
+use strict;
+use warnings;
+
+use Test::More tests => 4;
+
+BEGIN { use_ok( 'Test::HTML::Lint' ); }
+BEGIN { use_ok( 'HTML::Lint' ); }
+BEGIN { use_ok( 'HTML::Lint::Error' ); }
+
+my $lint = HTML::Lint->new();
+$lint->only_types( HTML::Lint::Error::FLUFF );
+
+# This code is invalid, but the linter should ignore it
+my $chunk = << 'END';
+<P><TABLE>This is a fine chunk of code</P>
+END
+
+html_ok( $lint, $chunk, 'STRUCTUREally naughty code passed' );
17 t/20-error-types-export.t
@@ -0,0 +1,17 @@
+#!perl -Tw
+
+use warnings;
+use strict;
+
+use Test::More tests => 5;
+
+BEGIN { use_ok( 'HTML::Lint::Error', ':types' ); }
+
+my $err = HTML::Lint::Error->new( undef, undef, undef, 'elem-empty-but-closed' );
+
+ok( $err->is_type( STRUCTURE ) );
+ok( !$err->is_type( FLUFF, HELPER ) );
+
+$err = HTML::Lint::Error->new( undef, undef, undef, 'attr-unknown' );
+ok( $err->is_type( FLUFF ) );
+ok( !$err->is_type( STRUCTURE, HELPER ) );
62 t/20-error-types-skip.t
@@ -0,0 +1,62 @@
+#!perl -Tw
+
+use strict;
+use warnings;
+use Test::More tests => 10;
+
+BEGIN { use_ok( 'HTML::Lint' ); }
+BEGIN { use_ok( 'HTML::Lint::Error', ':types' ); }
+
+my $text = do { local $/ = undef; <DATA> };
+
+FUNC_METHOD: {
+ my $lint = HTML::Lint->new();
+ isa_ok( $lint, 'HTML::Lint' );
+ $lint->parse( $text );
+ is( scalar $lint->errors, 1, 'One error with a clean lint' );
+
+ $lint->newfile();
+ $lint->clear_errors();
+ $lint->only_types( HELPER, FLUFF );
+ $lint->parse( $text );
+ is( scalar $lint->errors, 0, 'No errors if helper & fluff' );
+
+ $lint->newfile();
+ $lint->clear_errors();
+ $lint->only_types( STRUCTURE );
+ $lint->parse( $text );
+ my @errors = $lint->errors;
+ if ( !is( scalar @errors, 1, 'One error if we specify STRUCTURE if we turn it off' ) ) {
+ diag( $_->as_string ) for @errors;
+ }
+}
+
+CONSTRUCTOR_METHOD_SCALAR: {
+ my $lint = HTML::Lint->new( only_types => STRUCTURE );
+ isa_ok( $lint, 'HTML::Lint' );
+
+ $lint->parse( $text );
+ my @errors = $lint->errors;
+ if ( !is( scalar @errors, 1, 'One error if we specify STRUCTURE if we turn it off' ) ) {
+ diag( $_->as_string ) for @errors;
+ }
+}
+
+CONSTRUCTOR_METHOD_ARRAYREF: {
+ my $lint = HTML::Lint->new( only_types => [HELPER, FLUFF] );
+ isa_ok( $lint, 'HTML::Lint' );
+ $lint->parse( $text );
+ is( scalar $lint->errors, 0, 'No errors if helper & fluff' );
+}
+
+
+
+__DATA__
+<HTML>
+ <HEAD>
+ <TITLE>Test stuff</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="white">
+ <TABLE>This is my paragraph
+ </BODY>
+</HTML>
17 t/20-error-types.t
@@ -0,0 +1,17 @@
+#!perl -Tw
+
+use warnings;
+use strict;
+
+use Test::More tests => 5;
+
+BEGIN { use_ok( 'HTML::Lint::Error' ); }
+
+my $err = HTML::Lint::Error->new( undef, undef, undef, 'elem-empty-but-closed' );
+
+ok( $err->is_type( HTML::Lint::Error::STRUCTURE ) );
+ok( !$err->is_type( HTML::Lint::Error::FLUFF, HTML::Lint::Error::HELPER ) );
+
+$err = HTML::Lint::Error->new( undef, undef, undef, 'attr-unknown' );
+ok( $err->is_type( HTML::Lint::Error::FLUFF ) );
+ok( !$err->is_type( HTML::Lint::Error::STRUCTURE, HTML::Lint::Error::HELPER ) );
13 t/30-test-builder.t
@@ -0,0 +1,13 @@
+#!perl -Tw
+
+use warnings;
+use strict;
+
+# The test is not that html_ok() works, but that the tests=>1 gets
+# acts as it should.
+
+use Test::HTML::Lint tests=>1;
+
+my $chunk = "<P>This is a fine chunk of code</P>";
+
+html_ok( $chunk );
42 t/40-where.t
@@ -0,0 +1,42 @@
+#!perl -Tw
+
+use warnings;
+use strict;
+
+use Test::More tests => 4;
+
+BEGIN { use_ok( 'HTML::Lint' ); }
+
+my $lint = HTML::Lint->new();
+isa_ok( $lint, "HTML::Lint" );
+$lint->parse( '</body>' );
+
+my @errors = $lint->errors;
+my $error = shift @errors;
+is( $error->as_string, " (1:1) </body> with no opening <body>", "Got expected error" );
+is( scalar @errors, 0, "No more errors" );
+
+__DATA__
+This doesn't test the error finding as much as the where() method.
+It fixes the following bug:
+
+Date: Mon, 22 Dec 2003 22:07:54 -0800
+From: Adam Monsen <adamm@wazamatta.com>
+To: Andy Lester <andy@petdance.com>
+Subject: HTML::Lint::Error bug
+
+The following demonstrates a bug in HTML::Lint that is seen when an
+offending tag is flush left ...
+
+use HTML::Lint;
+my $lint = HTML::Lint->new();
+$lint->parse('</body>');
+warn $_->as_string."\n" for $lint->errors;
+
+The warning I'm getting looks like this:
+Argument "" isn't numeric in addition (+) at /usr/lib/perl5/site_perl/5.8.1/HTML/Lint/Error.pm line 176.
+
+If I change the parse() call as follows (by adding a leading space):
+$lint->parse(' </body>');
+
+the warning disappears.
46 t/50-multiple-files.t
@@ -0,0 +1,46 @@
+use warnings;
+use strict;
+
+require 't/LintTest.pl';
+
+my @files = get_paragraphed_files();
+
+checkit( [
+ [ 'elem-unopened' => qr/<\/p> with no opening <P>/i ],
+
+ [ 'elem-unclosed' => qr/<b> at \(6:5\) is never closed/i ],
+ [ 'elem-unclosed' => qr/<i> at \(7:5\) is never closed/i ],
+
+ [ 'elem-unopened' => qr/<\/b> with no opening <B>/i ],
+], @files );
+
+__DATA__
+<HTML> <!-- for elem-unopened -->
+ <HEAD>
+ <TITLE>Test stuff</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="white">
+ This is my paragraph</P>
+ </BODY>
+</HTML>
+
+<HTML>
+ <HEAD> <!-- Checking for elem-unclosed -->
+ <TITLE>Test stuff</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="white">
+ <P><B>This is my paragraph</P>
+ <P><I>This is another paragraph</P>
+ </BODY>
+</HTML>
+
+<!-- based on doc-tag-required -->
+<HTML>
+ <HEAD>
+ <TITLE>Test stuff</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="white">
+ </B>Gratuitous unnecessary closing tag that does NOT match to the opening [B] above.
+ <P>This is my paragraph</P>
+ </BODY>
+</HTML>
67 t/LintTest.pl
@@ -0,0 +1,67 @@
+use Test::More;
+use HTML::Lint;
+
+sub checkit {
+ my @expected = @{+shift};
+ my @linesets = @_;
+
+ plan( tests => 3*(scalar @expected) + 4 );
+
+ my $lint = new HTML::Lint;
+ isa_ok( $lint, 'HTML::Lint', 'Created lint object' );
+
+ my $n;
+ for my $set ( @linesets ) {
+ ++$n;
+ $lint->newfile( "Set #$n" );
+ $lint->parse( $_ ) for @$set;
+ $lint->eof;
+ }
+
+ my @errors = $lint->errors();
+ is( scalar @errors, scalar @expected, 'Right # of errors' );
+
+ while ( @errors && @expected ) {
+ my $error = shift @errors;
+ isa_ok( $error, 'HTML::Lint::Error' );
+
+ my $expected = shift @expected;
+
+ is( $error->errcode, $expected->[0], 'Error codes match' );
+ my $match = $expected->[1];
+ if ( ref $match eq "Regexp" ) {
+ like( $error->as_string, $match, 'Error matches regex' );
+ }
+ else {
+ is( $error->as_string, $match, 'Error matches string' );
+ }
+ }
+
+ my $dump;
+
+ is( scalar @errors, 0, 'No unexpected errors found' ) or $dump = 1;
+ is( scalar @expected, 0, 'No expected errors missing' ) or $dump = 1;
+
+ if ( $dump && @errors ) {
+ diag( "Leftover errors..." );
+ diag( $_->as_string ) for @errors;
+ }
+}
+
+# Read in a set of sets of lines, where each "file" is separated by a
+# blank line in <DATA>
+sub get_paragraphed_files {
+ local $/ = "";
+
+ my @sets;
+
+ while ( my $paragraph = <DATA> ) {
+ my @lines = split /\n/, $paragraph;
+ @lines = map { "$_\n" } @lines;
+ push( @sets, [@lines] );
+ }
+
+ return @sets;
+}
+
+1; # happy
17 t/attr-repeated.t
@@ -0,0 +1,17 @@
+use strict;
+use warnings;
+require 't/LintTest.pl';
+
+checkit( [
+ [ 'attr-repeated' => qr/ALIGN attribute in <P> is repeated/i ],
+], [<DATA>] );
+
+__DATA__
+<HTML>
+ <HEAD>
+ <TITLE>Test stuff</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="white">
+ <P ALIGN=LEFT ALIGN=RIGHT>This is my paragraph</P>
+ </BODY>
+</HTML>
19 t/attr-unknown.t
@@ -0,0 +1,19 @@
+use warnings;
+use strict;
+require 't/LintTest.pl';
+
+checkit( [
+ [ 'attr-unknown' => qr/Unknown attribute "FOOD" for tag <P>/i ],
+ [ 'attr-unknown' => qr/Unknown attribute "Yummy" for tag <I>/i ],
+], [<DATA>] );
+
+__DATA__
+<HTML>
+ <HEAD>
+ <TITLE>Test stuff</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="white">
+ <P FOOD="Burrito" ALIGN=RIGHT>This is my paragraph about burritos</P>
+ <I YUMMY="Spanish Rice">This is my paragraph about refried beans</I>
+ </BODY>
+</HTML>
16 t/doc-tag-required.t
@@ -0,0 +1,16 @@
+use warnings;
+use strict;
+require 't/LintTest.pl';
+
+checkit( [
+ [ 'doc-tag-required' => qr/<html> tag is required/ ],
+], [<DATA>] );
+
+__DATA__
+<!-- doc-tag-required -->
+ <HEAD>
+ <TITLE>Test stuff</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="white">
+ <P>This is my paragraph</P>
+ </BODY>
17 t/elem-empty-but-closed.t
@@ -0,0 +1,17 @@
+use warnings;
+use strict;
+require 't/LintTest.pl';
+
+checkit( [
+ [ 'elem-empty-but-closed' => qr/<hr> is not a container -- <\/hr> is not allowed/ ],
+], [<DATA>] );
+
+__DATA__
+<HTML>
+ <HEAD>
+ <TITLE>Test stuff</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="white">
+ <HR>This is a bad paragraph</HR>
+ </BODY>
+</HTML>
18 t/elem-img-alt-missing.t
@@ -0,0 +1,18 @@
+use warnings;
+use strict;
+require 't/LintTest.pl';
+
+checkit( [
+ [ 'elem-img-alt-missing' => qr/<IMG SRC="whizbang\.jpg"> does not have ALT text defined/i ],
+], [<DATA>] );
+
+__DATA__
+<HTML>
+ <HEAD>
+ <TITLE>Test stuff</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="white">
+ <P ALIGN=RIGHT>This is my paragraph</P>
+ <IMG SRC="whizbang.jpg" BORDER=3 HEIGHT=4 WIDTH=921>
+ </BODY>
+</HTML>
18 t/elem-img-sizes-missing.t
@@ -0,0 +1,18 @@
+use warnings;
+use strict;
+require 't/LintTest.pl';
+
+checkit( [
+ [ 'elem-img-sizes-missing' => qr/<IMG SRC="randal-thong\.jpg"> tag has no HEIGHT and WIDTH attributes./i ],
+], [<DATA>] );
+
+__DATA__
+<HTML>
+ <HEAD>
+ <TITLE>Test stuff</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="white">
+ <P ALIGN=RIGHT>This is my paragraph</P>
+ <IMG BORDER=3 HSPACE=12 SRC="randal-thong.jpg" ALT="Randal Schwartz in a thong">
+ </BODY>
+</HTML>
18 t/elem-nonrepeatable.t
@@ -0,0 +1,18 @@
+use warnings;
+use strict;
+require 't/LintTest.pl';
+
+checkit( [
+ [ 'elem-nonrepeatable' => qr/<title> is not repeatable, but already appeared at \(3:2\)/i ],
+], [<DATA>] );
+
+__DATA__
+<HTML>
+ <HEAD>
+ <TITLE>Test stuff</TITLE>
+ <TITLE>As if one title isn't enough</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="white">
+ <P>This is my paragraph</P>
+ </BODY>
+</HTML>
19 t/elem-unclosed.t
@@ -0,0 +1,19 @@
+use warnings;
+use strict;
+require 't/LintTest.pl';
+
+checkit( [
+ [ 'elem-unclosed' => qr/\Q<b> at (6:5) is never closed/i ],
+ [ 'elem-unclosed' => qr/\Q<i> at (7:5) is never closed/i ],
+], [<DATA>] );
+
+__DATA__
+<HTML>
+ <HEAD> <!-- Checking for elem-unclosed -->
+ <TITLE>Test stuff</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="white">
+ <P><B>This is my paragraph</P>
+ <P><I>This is another paragraph</P>
+ </BODY>
+</HTML>
20 t/elem-unknown.t
@@ -0,0 +1,20 @@
+use warnings;
+use strict;
+require 't/LintTest.pl';
+
+checkit( [
+ [ 'elem-unknown' => qr/unknown element <donky>/i ],
+ [ 'elem-unclosed' => qr/<donky> at \(\d+:\d+\) is never closed/i ],
+], [<DATA>] );
+
+__DATA__
+<HTML>
+ <HEAD>
+ <TITLE>Test stuff</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="white">
+ <P ALIGN=RIGHT>This is my paragraph</P>
+ <DONKY>
+ <IMG SRC="http://www.petdance.com/random/whizbang.jpg" BORDER=3 HEIGHT=4 WIDTH=921 ALT="whizbang!">
+ </BODY>
+</HTML>
17 t/elem-unopened.t
@@ -0,0 +1,17 @@
+use warnings;
+use strict;
+require 't/LintTest.pl';
+
+checkit( [
+ [ 'elem-unopened' => qr/<\/p> with no opening <P>/i ],
+], [<DATA>] );
+
+__DATA__
+<HTML> <!-- for elem-unopened -->
+ <HEAD>
+ <TITLE>Test stuff</TITLE>
+ </HEAD>
+ <BODY BGCOLOR="white">
+ This is my paragraph</P>
+ </BODY>
+</HTML>
291 t/embed-extensions.t
@@ -0,0 +1,291 @@
+use warnings;
+use strict;
+require 't/LintTest.pl';
+
+checkit( [
+], [<DATA>] );
+
+__DATA__
+<?php show_parallel_page() ?>
+
+<html>
+<head>
+ <meta name="description" content="Follett Library Resources supplies books, eBooks, and audiovisual materials to more K-12 schools than any other wholesaler or publisher. We understand the needs of today's K-12 students and educators and continually strive to provide the best products and services available.">
+ <meta http-equiv="content-type" content="text/html;charset=ISO-8859-1">
+ <title>Welcome to Follett Library Resources</title>
+</head>
+
+<body bgcolor="#ffffff" link=white vlink=white leftmargin=1 topmargin=1 marginheight=1 marginwidth=1>
+<table width="100%" border="0" cellspacing="0" cellpadding="0" bgcolor="white" height="122" HSPACE="1">
+ <tr>
+ <td align="left" valign="top" nowrap width="750" height="122"><img src="intro/graphics/hometop.jpg" alt="" height="122" width="750" usemap="#hometopb8e5aa3d" border="0"></td>
+ <td align="right" valign="top" width="100%" height="122" background="intro/graphics/topright.jpg">&nbsp;</td>
+ </tr>
+</table>
+ <table width="100%" border="0" cellspacing="0" cellpadding="0" align="left" bgcolor="white" height="100%">
+ <tr>
+ <td rowspan="2" align="left" valign="top" bgcolor="#006699" height="488" background="intro/graphics/leftbeigespacerbkgd.jpg">
+ <div align="left">
+ <img src="intro/graphics/leftcurve.jpg" alt="" height="71" width="213" align="top" border="0"><br>
+ <table width="143" border="0" cellspacing="0" cellpadding="4" bgcolor="#006699">
+ <tr>
+ <td valign="top" nowrap width="13"></td>
+ <td valign="top"><a href="intro/titletips.html"><img src="intro/graphics/titletips.gif" alt="" height="40" width="100" border="0"></a><br>
+ <br>
+ </td>
+ </tr>
+ <tr>
+ <td valign="top" nowrap width="13"></td>
+ <td valign="top"><font face="Arial,Helvetica,Geneva,Swiss,SunSans-Regular" size="3" color="white"><b><a href="/intro/sandcastle.html"><span Style="color: WHITE">SandCastle Early Literacy Program<br>
+ <br>
+ </span></a></b></font></td>
+ </tr>
+ <tr>
+ <td valign="top" nowrap width="13"></td>
+ <td valign="top"><font face="Arial,Helvetica,Geneva,Swiss,SunSans-Regular" size="3" color="white"><b><a href="/intro/keepyou.html"><span Style="color: WHITE">Library Products &amp; Services<br><br></span></a></b></font></td>
+ </tr>
+ <tr>
+ <td width="13"></td>
+ <td><font face="Arial,Helvetica,Geneva,Swiss,SunSans-Regular" size="3" color="white"><b><a href="/intro/classprodserv.html"><span Style="color: WHITE">Classroom Products &amp; Services<br><br></span></a></b></font></td>
+ </tr>
+ <tr>
+ <td width="13"></td>
+ <td><font size="3" color="#93ccff" face="Arial,Helvetica,Geneva,Swiss,SunSans-Regular"><b><a href="/intro/grantfunding.html"><span Style="color:white">Grant &amp; Funding Resources<br><br></span></a></b></font></td>
+ </tr>
+ <tr>
+ <td width="13"></td>
+ <td><font size="3" color="white" face="Arial,Helvetica,Geneva,Swiss,SunSans-Regular"><b><a href="/intro/curmap.html"><span Style="color: WHITE">Curriculum Mapping<br><br></span></a></b></font></td>
+ </tr>
+ <tr>
+ <td valign="top" width="13"><nobr><font size="3" face="Arial,Helvetica,Geneva,Swiss,SunSans-Regular" color="white">&nbsp;</font></nobr></td>
+ <td><font size="3" color="white" face="Arial,Helvetica,Geneva,Swiss,SunSans-Regular"><b><a href="/intro/newseve.html"><span Style="color:WHITE">Industry News &amp; Trends<br><br></span></a></b></font></td>
+ </tr>
+ <tr>
+ <td valign="top" width="13"></td>
+ <td><font size="3" color="white" face="Arial,Helvetica,Geneva,Swiss,SunSans-Regular"><b><a href="/intro/rli.html"><span Style="color: WHITE">Renaissance Learning Products<br><br></span></a></b></font></td>
+ </tr>
+ </table>
+ </div>
+ </td>
+ <td align="right" valign="top" bgcolor="white" width="100%" height="280">
+ <table width="100%" border="0" cellspacing="0" cellpadding="0" bgcolor="white" height="100%">
+ <tr>
+ <td width="100%" height="2"></td>
+ <td align="right" valign="top" width="12" height="2"></td>
+ <td rowspan="2" valign="top" bgcolor="white" width="142">
+ <div align="right">
+ <table width="130" border="0" cellspacing="0" cellpadding="0">
+ <tr>
+ <td colspan="3" width="142"><img src="intro/graphics/rightpaneltop.jpg" alt="" height="98" width="142" border="0"></td>
+ </tr>
+ <tr>
+ <td align="left" valign="top" bgcolor="#cccc99" width="2" height="102%"><img src="/intro/graphics/beigepixels.jpg" alt="" height="2" width="2" border="0"></td>
+ <td valign="top" bgcolor="#e7eef2" width="138" height="102%">
+ <div align="center">
+ <br>
+ <table width="128" border="0" cellspacing="2" cellpadding="0">
+ <tr>
+ <td colspan="2"><a href="/login/"><img src="login/graphics/libtwbutt.gif" alt="" height="32" width="86" border="0"></a><br>
+ <br>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2"><a href="/login/?side=C"><img src="login/graphics/curtwbutt.gif" alt="" height="32" width="86" border="0"></a><br>
+ <br>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2"><a href="/login/?side=W"><img src="login/graphics/twisebutt.gif" alt="TitleWise login" border="0" height=43 width=82></a></td>
+ </tr>
+ <tr>
+ <td colspan="2"><br>
+ <font size="2" face="Arial,Helvetica,Geneva,Swiss,SunSans-Regular">Use TITLEWAVE to search for books and audiovisual materials, build and store lists, and order online.<br>
+ <br>Use TitleWise to identify the strengths &amp; weaknesses of school &amp; district collections. <br>
+ <br>
+</font></td>
+ </tr>
+ </table>
+ </div>
+ </td>
+ <td align="right" valign="top" bgcolor="#cccc99" width="2" height="102%"><img src="/intro/graphics/beigepixels.jpg" alt="" height="2" width="1" border="0"></td>
+ </tr>
+ <tr>
+ <td colspan="3" align="left" valign="top" width="142"><img src="intro/graphics/rightpanelbottom.jpg" alt="" height="13" width="142" border="0"></td>
+ </tr>
+ </table>
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td align="left" valign="top" bgcolor="white" width="100%" height="100%">
+ <table width="100%" border="0" cellspacing="0" cellpadding="0">
+ <tr>
+ <td rowspan="2" align="left" valign="top" width="12" height="12"><img src="intro/graphics/beigecornertl.gif" alt="" height="12" width="12" border="0"></td>
+ <td valign="top" bgcolor="#cccc99" width="50%" height="2"><img src="/intro/graphics/beigepixels.jpg" height="1" width="2" ALT=""></td>
+ <td valign="top" bgcolor="#cccc99" width="10" height="2"><img src="/intro/graphics/beigepixels.jpg" height="1" width="2" ALT=""></td>
+ <td valign="top" bgcolor="#cccc99" width="50%" height="2"><img src="/intro/graphics/beigepixels.jpg" height="1" width="2" ALT=""></td>
+ <td rowspan=