=head1 NAME

TAP::Formatter::HTML - TAP Test Harness output delegate for html output

=head1 SYNOPSIS

 ##
 ## command-line usage (alpha):
 ##
 prove -m -Q -P HTML=outfile:out.html,css_uri:style.css,js_uri:foo.js,force_inline_css:0

 # backwards compat usage:
 prove -m -Q --formatter=TAP::Formatter::HTML >output.html

 # for more detail:
 perldoc App::Prove::Plugin::HTML

 ##
 ## perl usage:
 ##
 use TAP::Harness;

 my @tests = glob( 't/*.t' );
 my $harness = TAP::Harness->new({ formatter_class => 'TAP::Formatter::HTML',
                                   merge => 1 });
 $harness->runtests( @tests );
 # prints HTML to STDOUT by default

 # or if you really don't want STDERR merged in:
 my $harness = TAP::Harness->new({ formatter_class => 'TAP::Formatter::HTML' });

 # to use a custom formatter:
 my $fmt = TAP::Formatter::HTML->new;
 $fmt->css_uris([])->inline_css( $my_css )
     ->js_uris(['http://mysite.com/jquery.js', 'http://mysite.com/custom.js'])
     ->inline_js( '$(div.summary).hide()' );

 my $harness = TAP::Harness->new({ formatter => $fmt, merge => 1 });

 # to output HTML to a file[handle]:
 $fmt->output_fh( $fh );
 $fmt->output_file( '/tmp/foo.html' );

 # you can use your own customized templates too:
 $fmt->template('custom.tt2')
     ->template_processor( Template->new )
     ->force_inline_css(0)
     ->force_inline_js(0);

=cut

package TAP::Formatter::HTML;

use strict;
use warnings;

use URI;
use URI::file;
use Template;
use POSIX qw( ceil );
use IO::File;
use File::Temp qw( tempfile tempdir );
use File::Spec::Functions qw( catdir catfile file_name_is_absolute rel2abs );

use TAP::Formatter::HTML::Session;

# DEBUG:
#use Data::Dumper 'Dumper';

use base qw( TAP::Base );
use accessors qw( verbosity stdout output_fh escape_output tests session_class sessions
		  template_processor template html html_id_iterator minify color
		  css_uris js_uris inline_css inline_js abs_file_paths force_inline_css force_inline_js );

use constant default_session_class => 'TAP::Formatter::HTML::Session';
use constant default_template      => 'TAP/Formatter/HTML/default_report.tt2';
use constant default_js_uris       => ['file:TAP/Formatter/HTML/jquery-1.4.2.min.js',
				       'file:TAP/Formatter/HTML/jquery.tablesorter-2.0.3.min.js',
				       'file:TAP/Formatter/HTML/default_report.js'];
use constant default_css_uris      => ['file:TAP/Formatter/HTML/default_page.css',
				       'file:TAP/Formatter/HTML/default_report.css'];

use constant severity_map => {
			      ''          => 0,
			      'very-low'  => 1,
			      'low'       => 2,
			      'med'       => 3,
			      'high'      => 4,
			      'very-high' => 5,
			      0 => '',
			      1 => 'very-low',
			      2 => 'low',
			      3 => 'med',
			      4 => 'high',
			      5 => 'very-high',
			     };

our $VERSION = '0.11';
our $FAKE_WIN32_URIS = 0; # for testing only

sub _initialize {
    my ($self, $args) = @_;

    $args ||= {};
    $self->SUPER::_initialize($args);

    my $stdout_fh = IO::File->new_from_fd( fileno(STDOUT), 'w' )
      or die "Error opening STDOUT for writing: $!";

    $self->verbosity( 0 )
         ->stdout( $stdout_fh )
         ->output_fh( $stdout_fh )
	 ->minify( 1 )
	 ->escape_output( 0 )
         ->abs_file_paths( 1 )
         ->abs_file_paths( 1 )
         ->force_inline_css( 1 )
         ->force_inline_js( 0 )
         ->session_class( $self->default_session_class )
         ->template_processor( $self->default_template_processor )
         ->template( $self->default_template )
         ->js_uris( $self->default_js_uris )
         ->css_uris( $self->default_css_uris )
         ->inline_js( '' )
	 ->inline_css( '' )
	 ->sessions( [] );

    $self->check_for_overrides_in_env;

    # Laziness...
    # trust the user knows what they're doing with the args:
    foreach my $key (keys %$args) {
	$self->$key( $args->{$key} ) if ($self->can( $key ));
    }

    $self->html_id_iterator( $self->create_iterator( $args ) );

    return $self;
}

sub check_for_overrides_in_env {
    my $self = shift;

    if (my $file = $ENV{TAP_FORMATTER_HTML_OUTFILE}) {
	$self->output_file( $file );
    }

    my $force_css = $ENV{TAP_FORMATTER_HTML_FORCE_INLINE_CSS};
    if (defined( $force_css )) {
	$self->force_inline_css( $force_css );
    }

    my $force_js = $ENV{TAP_FORMATTER_HTML_FORCE_INLINE_JS};
    if (defined( $force_js )) {
	$self->force_inline_js( $force_js );
    }

    if (my $uris = $ENV{TAP_FORMATTER_HTML_CSS_URIS}) {
	my $list = [ split( ':', $uris ) ];
	$self->css_uris( $list );
    }

    if (my $uris = $ENV{TAP_FORMATTER_HTML_JS_URIS}) {
	my $list = [ split( ':', $uris ) ];
	$self->js_uris( $list );
    }

    if (my $file = $ENV{TAP_FORMATTER_HTML_TEMPLATE}) {
	$self->template( $file );
    }

    return $self;
}

sub default_template_processor {
    my $path = __FILE__;
    $path =~ s/.TAP.Formatter.HTML.pm$//;
    return Template->new(
        # arguably shouldn't compile as this is only used once
        COMPILE_DIR  => catdir( tempdir( CLEANUP => 1 ), 'TAP-Formatter-HTML' ),
        COMPILE_EXT  => '.ttc',
        INCLUDE_PATH => $path,
    );
}


sub output_file {
    my ($self, $file) = @_;
    my $fh = IO::File->new( $file, 'w' )
      or die "Error opening '$file' for writing: $!";
    $self->output_fh( $fh );
}

sub create_iterator {
    my $self = shift;
    my $args = shift || {};
    my $prefix = $args->{html_id_prefix} || 't';
    my $i = 0;
    my $iter = sub { return $prefix . $i++ };
}

sub verbose {
    my $self = shift;
    # emulate a classic accessor for compat w/TAP::Formatter::Console:
    if (@_) { $self->verbosity(1) }
    return $self->verbosity >= 1;
}

sub quiet {
    my $self = shift;
    # emulate a classic accessor for compat w/TAP::Formatter::Console:
    if (@_) { $self->verbosity(-1) }
    return $self->verbosity <= -1;
}

sub really_quiet {
    my $self = shift;
    # emulate a classic accessor for compat w/TAP::Formatter::Console:
    if (@_) { $self->verbosity(-2) }
    return $self->verbosity <= -2;
}

sub silent {
    my $self = shift;
    # emulate a classic accessor for compat w/TAP::Formatter::Console:
    if (@_) { $self->verbosity(-3) }
    return $self->verbosity <= -3;
}

# Called by Test::Harness before any test output is generated.
sub prepare {
    my ($self, @tests) = @_;
    # warn ref($self) . "->prepare called with args:\n" . Dumper( \@tests );
    $self->info( 'running ', scalar @tests, ' tests' );
    $self->tests( [@tests] );
}

# Called to create a new test session. A test session looks like this:
#
#    my $session = $formatter->open_test( $test, $parser );
#    while ( defined( my $result = $parser->next ) ) {
#        $session->result($result);
#        exit 1 if $result->is_bailout;
#    }
#    $session->close_test;
sub open_test {
    my ($self, $test, $parser) = @_;
    #warn ref($self) . "->open_test called with args: " . Dumper( [$test, $parser] );
    my $session = $self->session_class->new({ test => $test,
					      parser => $parser,
					      formatter => $self });
    push @{ $self->sessions }, $session;
    return $session;
}

# $str = $harness->summary( $aggregate );
#
# C<summary> produces the summary report after all tests are run.  The argument is
# an aggregate.
sub summary {
    my ($self, $aggregate) = @_;
    #warn ref($self) . "->summary called with args: " . Dumper( [$aggregate] );

    # farmed out to make sub-classing easy:
    my $report = $self->prepare_report( $aggregate );
    $self->generate_report( $report );

    # if silent is set, only print HTML if we're not printing to stdout
    if (! $self->silent or $self->output_fh->fileno != fileno(STDOUT)) {
	print { $self->output_fh } ${ $self->html };
	$self->output_fh->flush;
    }

    return $self;
}

sub generate_report {
    my ($self, $r) = @_;

    $self->check_uris;
    $self->slurp_css if $self->force_inline_css;
    $self->slurp_js if $self->force_inline_js;

    my $params = {
		  report => $r,
		  js_uris  => $self->js_uris,
		  css_uris => $self->css_uris,
		  inline_js  => $self->inline_js,
		  inline_css => $self->inline_css,
		  formatter => { class => ref( $self ),
				 version => $self->VERSION },
		 };

    my $html = '';
    $self->template_processor->process( $self->template, $params, \$html )
      || die $self->template_processor->error;

    $self->html( \$html );
    $self->minify_report if $self->minify;

    return $self;
}

# try and reduce the size of the report
sub minify_report {
    my $self = shift;
    my $html_ref = $self->html;
    $$html_ref =~ s/^\t+//mg;
    return $self;
}

# convert all uris to URI objs
# check file uris (if relative & not found, try & find them in @INC)
sub check_uris {
    my ($self) = @_;

    foreach my $uri_list ($self->js_uris, $self->css_uris) {
	# take them out of the list to verify, push them back on later
	my @uris = splice( @$uri_list, 0, scalar @$uri_list );
	foreach my $uri (@uris) {
	    if (($^O =~ /win32/i or $FAKE_WIN32_URIS)
		and $uri =~ /^(?:(?:file)|(?:\w:)?\\)/) {
		$uri = URI::file->new($uri, 'win32');
	    } else {
	        $uri = URI->new( $uri );
    	    }
	    if ($uri->scheme && $uri->scheme eq 'file') {
		my $path = $uri->path;
		unless (file_name_is_absolute($path)) {
		    my $new_path;
		    if (-e $path) {
			$new_path = rel2abs( $path ) if ($self->abs_file_paths);
		    } else {
			$new_path = $self->find_in_INC( $path );
		    }
                    if ($new_path) {
                        if (($^O =~ /win32/i or $FAKE_WIN32_URIS)) {
                            $uri = URI::file->new("file://$new_path", 'win32');
                        } else {
                            $uri->path( $new_path );
                        }
                    }
		}
	    }
	    push @$uri_list, $uri;
	}
    }

    return $self;
}

sub prepare_report {
    my ($self, $a) = @_;

    my $r = {
	     tests => [],
	     start_time => '?',
	     end_time => '?',
	     elapsed_time => $a->elapsed_timestr,
	    };


    # add aggregate test info:
    for my $key (qw(
		    total
		    has_errors
		    has_problems
		    failed
		    parse_errors
		    passed
		    skipped
		    todo
		    todo_passed
		    wait
		    exit
		   )) {
	$r->{$key} = $a->$key;
    }

    # do some other handy calcs:
    $r->{actual_passed} = $r->{passed} + $r->{todo_passed};
    if ($r->{total}) {
	$r->{percent_passed} = sprintf('%.1f', $r->{actual_passed} / $r->{total} * 100);
    } else {
	$r->{percent_passed} = 0;
    }

    # estimate # files (# sessions could be different?):
    $r->{num_files} = scalar @{ $self->sessions };

    # add test results:
    my $total_time = 0;
    foreach my $s (@{ $self->sessions }) {
	my $sr = $s->as_report;
	push @{$r->{tests}}, $sr;
	$total_time += $sr->{elapsed_time} || 0;
    }
    $r->{total_time} = $total_time;

    # estimate total severity:
    my $smap = $self->severity_map;
    my $severity = 0;
    $severity += $smap->{$_->{severity} || ''} for @{$r->{tests}};
    my $avg_severity = 0;
    if (scalar @{$r->{tests}}) {
	$avg_severity = ceil($severity / scalar( @{$r->{tests}} ));
    }
    $r->{severity} = $smap->{$avg_severity};

    # TODO: coverage?

    return $r;
}

# adapted from Test::TAP::HTMLMatrix
# always return abs file paths if $self->abs_file_paths is on
sub find_in_INC {
    my ($self, $file) = @_;

    foreach my $path (grep { not ref } @INC) {
	my $target = catfile($path, $file);
	if (-e $target) {
	    $target = rel2abs($target) if $self->abs_file_paths;
	    return $target;
	}
    }

    # non-fatal
    $self->log("Warning: couldn't find $file in \@INC");
    return;
}

# adapted from Test::TAP::HTMLMatrix
# slurp all 'file' uris, if possible
# note: doesn't remove them from the css_uris list, just in case...
sub slurp_css {
    my ($self) = shift;
    $self->info("slurping css files inline");

    my $inline_css = '';
    $self->_slurp_uris( $self->css_uris, \$inline_css );

    # append any inline css so it gets interpreted last:
    $inline_css .= "\n" . $self->inline_css if $self->inline_css;

    $self->inline_css( $inline_css );
}

sub slurp_js {
    my ($self) = shift;
    $self->info("slurping js files inline");

    my $inline_js = '';
    $self->_slurp_uris( $self->js_uris, \$inline_js );

    # append any inline js so it gets interpreted last:
    $inline_js .= "\n" . $self->inline_js if $self->inline_js;

    $self->inline_js( $inline_js );
}

sub _slurp_uris {
    my ($self, $uris, $slurp_to_ref) = @_;

    foreach my $uri (@$uris) {
	my $scheme = $uri->scheme;
	if ($scheme && $scheme eq 'file') {
	    my $path = $uri->path;
	    if (-e $path) {
		if (open my $fh, $path) {
		    local $/ = undef;
		    $$slurp_to_ref .= <$fh>;
		    $$slurp_to_ref .= "\n";
		} else {
		    $self->log("Warning: couldn't open $path: $!");
		}
	    } else {
		$self->log("Warning: couldn't read $path: file does not exist!");
	    }
	} else {
	    $self->log("Warning: can't include $uri inline: not a file uri");
	}
    }

    return $slurp_to_ref;
}



sub log {
    my $self = shift;
    push @_, "\n" unless grep {/\n/} @_;
    $self->_output( @_ );
    return $self;
}

sub info {
    my $self = shift;
    return unless $self->verbose;
    return $self->log( @_ );
}

sub log_test {
    my $self = shift;
    return if $self->really_quiet;
    return $self->log( @_ );
}

sub log_test_info {
    my $self = shift;
    return if $self->quiet;
    return $self->log( @_ );
}

sub _output {
    my $self = shift;
    return if $self->silent;
    if (ref($_[0]) && ref( $_[0]) eq 'SCALAR') {
	# DEPRECATED: printing HTML:
	print { $self->stdout } ${ $_[0] };
    } else {
	unshift @_, '# ' if $self->escape_output;
	print { $self->stdout } @_;
    }
}


1;


__END__

=head1 DESCRIPTION

This module provides HTML output formatting for L<TAP::Harness> (a replacement
for L<Test::Harness>.  It is largely based on ideas from
L<TAP::Test::HTMLMatrix> (which was built on L<Test::Harness> and thus had a
few limitations - hence this module).  For sample output, see:

L<http://www.spurkis.org/TAP-Formatter-HTML/test-output.html>

This module is targeted at all users of automated test suites.  It's meant to
make reading test results easier, giving you a visual summary of your test suite
and letting you drill down into individual failures (which will hopefully make
testing more likely to happen at your organization ;-).

The design goals are:

=over 4

=item *

I<easy to use>

Once you've got your test report, it should be obvious how to use it.

=item *

I<helpful>

It should be helpful by pointing out I<where> & I<why> your test suite is
breaking.  If you've written your tests well, it should give you enough info to
start tracking down the issue.

=item *

I<easy to install>

Eg: should be a clean install from CPAN, and you shouldn't need to modify your
existing test suite to get up & running, though I<you will need to stop using
L<Test::Harness> unfortunately>.

=item *

I<work out of the box>

You shouldn't need to do any custom-coding to get it working - the default
configuration & templates should be enough to get started with.  Once installed
it should be a matter of running:

 % prove -m -Q --formatter=TAP::Formatter::HTML >output.html

From your project's home dir, and opening the resulting file.

=item *

I<easy to configure>

You should be able to configure & customize it to suit your needs.  As such,
css, javascript and templates are all configurable.

=back

=head1 METHODS

=head2 CONSTRUCTOR

=head3 new

  my $fmt = $class->new({ %args });

=head2 ACCESSORS

All chaining L<accessors>:

=head3 verbosity

  $fmt->verbosity( [ $v ] )

Verbosity level, as defined in L<TAP::Harness/new>:

     1   verbose        Print individual test results (and more) to STDOUT.
     0   normal
    -1   quiet          Suppress some test output (eg: test failures).
    -2   really quiet   Suppress everything to STDOUT but the HTML report.
    -3   silent         Suppress all output to STDOUT, including the HTML report.

Note that the report is also available via L</html>.  You can also provide a
custom L</output_fh> (aka L</output_file>) that will be used instead of
L</stdout>, even if I<silent> is on.

=head3 stdout

  $fmt->stdout( [ \*FH ] );

An L<IO::Handle> filehandle for catching standard output.  Defaults to C<STDOUT>.

=head3 output_fh

  $fmt->output_fh( [ \*FH ] );

An L<IO::Handle> filehandle for printing the HTML report to.  Defaults to the
same object as L</stdout>.

B<Note:> If L</verbosity> is set to C<silent>, printing to C<output_fh> will
still occur.  (that is, assuming you've opened a different file, B<not>
C<STDOUT>).

=head3 output_file

  $fmt->output_file( $file_name )

Not strictly an accessor - this is a shortcut for setting L</output_fh>,
equivalent to:

  $fmt->output_fh( IO::File->new( $file_name, 'w' ) );

You can set this with the C<TAP_FORMATTER_HTML_OUTFILE=/path/to/file>
environment variable

=head3 escape_output

  $fmt->escape_output( [ $boolean ] );

If set, all output to L</stdout> is escaped.  This is probably only useful
if you're testing the formatter.
Defaults to C<0>.

=head3 html

  $fmt->html( [ \$html ] );

This is a reference to the scalar containing the html generated on the last
test run.  Useful if you have L</verbosity> set to C<silent>, and have not
provided a custom L</output_fh> to write the report to.

=head3 tests

  $fmt->tests( [ \@test_files ] )

A list of test files we're running, set by L<TAP::Parser>.

=head3 session_class

  $fmt->session_class( [ $class ] )

Class to use for L<TAP::Parser> test sessions.  You probably won't need to use
this unless you're hacking or sub-classing the formatter.
Defaults to L<TAP::Formatter::HTML::Session>.

=head3 sessions

  $fmt->sessions( [ \@sessions ] )

Test sessions added by L<TAP::Parser>.  You probably won't need to use this
unless you're hacking or sub-classing the formatter.

=head3 template_processor

  $fmt->template_processor( [ $processor ] )

The template processor to use.
Defaults to a TT2 L<Template> processor with the following config:

  COMPILE_DIR  => catdir( tempdir(), 'TAP-Formatter-HTML' ),
  COMPILE_EXT  => '.ttc',
  INCLUDE_PATH => parent directory TAP::Formatter::HTML was loaded from

Note: INCLUDE_PATH used to be set to: C<join(':', @INC)> but this was causing
issues on systems with > 64 dirs in C<@INC>.  See RT #74364 for details.

=head3 template

  $fmt->template( [ $file_name ] )

The template file to load.
Defaults to C<TAP/Formatter/HTML/default_report.tt2>.

You can set this with the C<TAP_FORMATTER_HTML_TEMPLATE=/path/to.tt> environment
variable.

=head3 css_uris

  $fmt->css_uris( [ \@uris ] )

A list of L<URI>s (or strings) to include as external stylesheets in <style>
tags in the head of the document.
Defaults to:

  ['file:TAP/Formatter/HTML/default_report.css'];

You can set this with the C<TAP_FORMATTER_HTML_CSS_URIS=/path/to.css:/another/path.css>
environment variable.

If you're using Win32, please see L</WIN32 URIS>.

=head3 js_uris

  $fmt->js_uris( [ \@uris ] )

A list of L<URI>s (or strings) to include as external stylesheets in <script>
tags in the head of the document.
Defaults to:

  ['file:TAP/Formatter/HTML/jquery-1.2.6.pack.js'];

You can set this with the C<TAP_FORMATTER_HTML_JS_URIS=/path/to.js:/another/path.js>
environment variable.

If you're using Win32, please see L</WIN32 URIS>.

=head3 inline_css

  $fmt->inline_css( [ $css ] )

If set, the formatter will include the CSS code in a <style> tag in the head of
the document.

=head3 inline_js

  $fmt->inline_js( [ $javascript ] )

If set, the formatter will include the JavaScript code in a <script> tag in the
head of the document.

=head3 minify

  $fmt->minify( [ $boolean ] )

If set, the formatter will attempt to reduce the size of the generated report,
they can get pretty big if you're not careful!  Defaults to C<1> (true).

B<Note:> This currently just means... I<remove tabs at start of a line>.  It
may be extended in the future.

=head3 abs_file_paths

  $fmt->abs_file_paths( [ $ boolean ] )

If set, the formatter will attempt to convert any relative I<file> JS & css
URI's listed in L</css_uris> & L</js_uris> to absolute paths.  This is handy if
you'll be sending moving the HTML output around on your harddisk, (but not so
handy if you move it to another machine - see L</force_inline_css>).
Defaults to I<1>.

=head3 force_inline_css

  $fmt->force_inline_css( [ $boolean ] )

If set, the formatter will attempt to slurp in any I<file> css URI's listed in
L</css_uris>, and append them to L</inline_css>.  This is handy if you'll be
sending the output around - that way you don't have to send a CSS file too.
Defaults to I<1>.

You can set this with the C<TAP_FORMATTER_HTML_FORCE_INLINE_CSS=0|1> environment
variable.

=head3 force_inline_js( [ $boolean ] )

If set, the formatter will attempt to slurp in any I<file> javascript URI's listed in
L</js_uris>, and append them to L</inline_js>.  This is handy if you'll be
sending the output around - that way you don't have to send javascript files too.

Note that including jquery inline doesn't work with some browsers, haven't
investigated why.  Defaults to I<0>.

You can set this with the C<TAP_FORMATTER_HTML_FORCE_INLINE_JS=0|1> environment
variable.

=head3 color

This method is for C<TAP::Harness> API compatibility only.  It does nothing.

=head2 API METHODS

=head3 summary

  $html = $fmt->summary( $aggregator )

C<summary> produces a summary report after all tests are run.  C<$aggregator>
should be a L<TAP::Parser::Aggregator>.

This calls:

  $fmt->template_processor->process( $params )

Where C<$params> is a data structure containing:

  report      => %test_report
  js_uris     => @js_uris
  css_uris    => @js_uris
  inline_js   => $inline_js
  inline_css  => $inline_css
  formatter   => %formatter_info

The C<report> is the most complicated data structure, and will sooner or later
be documented in L</CUSTOMIZING>.

=head1 CUSTOMIZING

This section is not yet written.  Please look through the code if you want to
customize the templates, or sub-class.

You can use environment variables to customize the behaviour of TFH:

  TAP_FORMATTER_HTML_OUTFILE=/path/to/file
  TAP_FORMATTER_HTML_FORCE_INLINE_CSS=0|1
  TAP_FORMATTER_HTML_FORCE_INLINE_JS=0|1
  TAP_FORMATTER_HTML_CSS_URIS=/path/to.css:/another/path.css
  TAP_FORMATTER_HTML_JS_URIS=/path/to.js:/another/path.js
  TAP_FORMATTER_HTML_TEMPLATE=/path/to.tt

This should save you from having to write custom code for simple cases.

=head1 WIN32 URIS

This module tries to do the right thing when fed Win32 File I<paths> as File
URIs to both L</css_uris> and L</js_uris>, eg:

  C:\some\path
  file:///C:\some\path

While I could lecture you what a valid file URI is and point you at:

http://blogs.msdn.com/ie/archive/2006/12/06/file-uris-in-windows.aspx

Which basically says the above are invalid URIs, and you should use:

  file:///C:/some/path
  # ie: no backslashes

I also realize it's convenient to chuck in a Win32 file path, as you can on
Unix.  So if you're running under Win32, C<TAP::Formatter::HTML> will look for
a signature C<'X:\'>, C<'\'> or C<'file:'> at the start of each URI to see if
you are referring to a file or another type of URI.

Note that you must use 'C<file:///C:\blah>' with I<3 slashes> otherwie 'C<C:>'
will become your I<host>, which is probably not what you want.  See
L<URI::file> for more details.

I realize this is a pretty basic algorithm, but it should handle most cases.
If it doesn't work for you, you can always construct a valid File URI instead.

=head1 BUGS

Please use http://rt.cpan.org to report any issues.  Patches are welcome.

=head1 CONTRIBUTING

Use github:

L<https://github.com/spurkis/TAP-Formatter-HTML>

=head1 AUTHOR

Steve Purkis <spurkis@cpan.org>

=head1 COPYRIGHT

Copyright (c) 2008-2012 Steve Purkis <spurkis@cpan.org>, S Purkis Consulting Ltd.
All rights reserved.

This module is released under the same terms as Perl itself.

=head1 SEE ALSO

Examples in the C<examples> directory and here:

L<http://www.spurkis.org/TAP-Formatter-HTML/test-output.html>,
L<http://www.spurkis.org/TAP-Formatter-HTML/DBD-SQLite-example.html>,
L<http://www.spurkis.org/TAP-Formatter-HTML/Template-example.html>

L<prove> - L<TAP::Harness>'s new cmdline utility.  It's great, use it!

L<App::Prove::Plugin::HTML> - the prove interface for this module.

L<Test::TAP::HTMLMatrix> - the inspiration for this module.  Many good ideas
were borrowed from it.

L<TAP::Formatter::Console> - the default TAP formatter used by L<TAP::Harness>

=cut