#!/usr/bin/perl -w
# 
# $Id: TidyDom.pm,v 1.1 2006/08/26 20:41:34 eric Exp $

use strict;

package W3C::XML::TidyDom;
use vars qw($DefaultTidyCommand $RelativeAttributes);
$DefaultTidyCommand = 'tidy -asxhtml -quiet -raw -wrap 65535';
$RelativeAttributes = {'img' => ['src'], 
		       'link' => ['href'], 
		       'script' => ['src'], 
		       'a' => ['href']};
require LWP::UserAgent;
use XML::DOM;
require W3C::Util::Filter;
require URI::URL;

sub new {
    my ($proto, @parms) = @_;
    my $class = ref($proto) || $proto;
    my $self = {-userAgent => LWP::UserAgent->new, 
		-nonce => 'r'.(int(rand( 65535-10000+1 ) ) + 10000), 
		-tidyCommand => $DefaultTidyCommand, 
		@parms};
    bless ($self, $class);
    return $self;
}
sub toString {
    my ($self) = @_;
    return $self->getResponse->headers->as_string. "\n" .
	$self->getDocumentString();
}
sub getResponse {
    my ($self) = @_;
    return $self->{Response};
}
sub getDOMfromURL {
    my ($self, $url) = @_;
    $self->{Response} = $self->{-userAgent}->get($url);
    if (!$self->{Response}->is_success) {
	return undef;
    }
    my ($tidied, $error) = 
	W3C::Util::Filter->new($self->{-tidyCommand}, $self->{Response}->content())->execute();

    if ($self->{-hackNoEmptyDivs}) {
	$tidied =~ s{</div>}{<?$self->{-nonce} ?></div>}sig;
    }
    if (exists $self->{-hackHideCData}) {
	$tidied =~ s
	{(<!\[CDATA\[.*?\]\]>)}
	{
	    push (@{$self->{-hackHideCData}}, $1);
	    "<?$self->{-nonce}-CDATA-".(scalar @{$self->{-hackHideCData}}-1)."?>";
	}sieg;
    }
    $self->{Doc} = new XML::DOM::Parser->parse($tidied);
    $self->{Doc}->getXMLDecl->setEncoding('utf-8');

    foreach my $elementName (keys %$RelativeAttributes) {
	foreach my $attributeName (@{$RelativeAttributes->{$elementName}}) {
	    $self->fixRelativeAttribute($elementName, $attributeName, $url);
	}
    }
    return $self->{Doc};    
}
sub getDocumentString {
    my ($self) = @_;
    my $out = $self->{Doc}->toString();
    if ($self->{-hackHideCData}) {
	for (my $i = 0; $i < @{$self->{-hackHideCData}}; $i++) {
	    $out =~ s{<\?$self->{-nonce}-CDATA-$i\s*\?>}{$self->{-hackHideCData}[$i]}sieg;
	}
    }
    if ($self->{-hackNoEmptyDivs}) {
	$out =~ s{<\?$self->{-nonce}\s*\?>\s*}{}sg;
    }
    return $out;
}
sub fixRelativeAttribute {
    my ($self, $elementName, $attributeName, $baseURL) = @_;
    my $imgNodes = $self->{Doc}->getElementsByTagName($elementName);
    my $nImgNodes = $imgNodes->getLength;
    for (my $i = 0; $i < $nImgNodes; $i++) {
	my $imgEl = $imgNodes->item($i);
	my $val = $imgEl->getAttribute($attributeName);
	if ($val) {
	    $val = new URI::URL($val, $baseURL)->abs->as_string;
	    $imgEl->removeAttribute($attributeName);
	    $imgEl->setAttribute($attributeName, $val);
	}
    }
}

1;

__END__

=head1 NAME

W3C::XML::TidyDom - GET and tidy a document and return its DOM for manipulation

=head1 SYNOPSIS

  require W3C::XML::TidyDom;
  my $td = new W3C::XML::TidyDom(-hackNoEmptyDivs => 1, -hackHideCData => []);
  my $doc = $td->getDOMfromURL('http://www.cpan.org/');
  if (!$doc) {
      die $td->getResponse->status_line;
  }
  # manipulate $doc and $td->getResponse->headers
  print "Status: 200\n", $td->toString();

=head1 DESCRIPTION

The calling program calls getDOMfromURL. LWP fetches the URL. Tidy
turns the contents into well-formed XML, as HTML-compliant as it
can. When the caller is done manipulating the dom tree and the
response headers, it calls toString to have all of that serialized.

This module is part of the W3C::Rdf CPAN module.

=head1 METHODS

    new(<OPTIONS>), getDOMfromURL($url), toString()


=head2 new(<OPTIONS>)

Create a new TidyDom object.

returns the new TidyDom object.

=head3 OPTIONS

-userAgent: override the default LWP::UserAgent userAgent (must support get($url) API).

-tidyCommand: how TidyDom invokes HTML tidy.

-hackNoEmptyDivs: make sure there are no empty element divs (<div />)
 as they confuse browsers.

-hackHideCData: array in which to store CDATA sections before
 DOMinating the document. As of when this was written, XML::DOM did
 not support <![CDATA[ ... ]]> sections.

-nonce: overrides default hard-to-collide-with generator.

=head2 getDOMfromURL($url)

Fetch, tidy, and DOMinate $url.

returns the dom tree.

=head2 toString()

Create a string of the respones headers and the manipulated DOM.

returns the afore-mentioned string.

=head1 AUTHOR

Eric Prud'hommeaux <eric@w3.org>

=head1 SEE ALSO

W3C::Util::Filter(1).

=cut

 # $Log: TidyDom.pm,v $
 # Revision 1.1  2006/08/26 20:41:34  eric
 # + created from SPellet (which was created from post2get)
 #
