The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package HTML::XHTML::Lite;

use 5.006;
use strict;
use warnings;

require Exporter;

our @ISA = qw(Exporter);

# Items to export into callers namespace by default. Note: do not export
# names by default without a very good reason. Use EXPORT_OK instead.
# Do not simply export all your public functions/methods/constants.

# This allows declaration	use HTML::XHTML::Lite ':all';
# If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
# will save memory.
our %EXPORT_TAGS = ( 'all' => [ qw(

) ] );

our @EXPORT_OK = qw( start_page end_page getvars );

our @EXPORT = qw(
start_page end_page getvars
);

our $VERSION = '0.06';


# Preloaded methods go here.

sub start_page
{
use HTTP::Date;
use Time::Local;

$_[0]={} unless defined $_[0];

my %p=%{$_[0]};
my $page;

$p{content_type}='text/html' unless defined $p{content_type};
my $charset=(defined $p{charset} ? uc($p{charset}) : 'UTF-8');
$p{title}='Untitled Document' unless defined $p{title};
$p{dctitle}=$p{title} unless defined $p{dctitle};
$p{lang}=(defined $p{lang} ? $p{lang} : 'en');
$p{foaftitle}='FOAF' unless defined $p{foaftitle};

if ($p{feed})
{
	$p{feedtype}="application/rss+xml" unless defined $p{feedtype};
	$p{feedtitle}="RSS Feed for $p{title}" unless defined $p{feedtitle};
}

my $now=time2str(time());
my $expires=(defined $p{expires} ? time2str(iso2time($p{expires})) : $now);

unless ($p{isfile})
{
	$page.="Expires: $expires\n";
	$page.="Date: $now\n";
	$page.="Content-type: $p{content_type}; charset=$charset\n\n";
}

$page.="<?xml version=\"1.0\" encoding=\"" . lc($charset) . "\"?>\n" unless $p{noxml} || $p{nohead};
$page.="<!DOCTYPE html\n\tPUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n\t\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n" unless $p{nodoctype} || $p{nohead};

$page.=<<EOT;
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="$p{lang}" lang="$p{lang}">
<head>
<title>$p{title}</title>
<link rel="schema.DC" href="http://purl.org/dc/elements/1.1/" />
<link rel="schema.DCTERMS" href="http://purl.org/dc/terms/" />
<meta name="DC.language" scheme="DCTERMS.RFC1766" content="$p{lang}" />
<meta name="DC.type" scheme="DCTERMS.DCMIType" content="Text" />
<meta name="DC.format" scheme="DCTERMS.IMT" content="text/html; charset=$charset" />
<meta name="DC.title" lang="$p{lang}" content="$p{dctitle}" />
EOT

$page.="<meta name=\"DC.description\" lang=\"$p{lang}\" content=\"$p{description}\" />\n" if defined $p{description};
$page.="<meta name=\"DC.creator\" content=\"$p{creator}\" />\n" if defined $p{creator};
$page.="<meta name=\"DC.identifier\" content=\"$p{identifier}\" />\n" if defined $p{identifier};
$page.="<meta name=\"DC.subject\" lang=\"$p{lang}\" content=\"$p{subject}\" />\n" if defined $p{subject};
$page.="<meta name=\"DC.rights\" content=\"$p{rights}\" />\n" if defined $p{rights};
$page.="<meta name=\"DCTERMS.created\" scheme=\"DCTERMS.W3CDTF\" content=\"$p{created}\" />\n" if defined $p{created};
$page.="<meta name=\"DCTERMS.modified\" scheme=\"DCTERMS.W3CDTF\" content=\"$p{modified}\" />\n" if defined $p{modified};
$page.="<meta name=\"DC.date\" content=\"$p{date}\" />\n" if defined $p{date};

if (defined $p{legacy} && defined $p{description} && defined $p{subject})
{
	my $kwds=$p{subject};
	$kwds=~s/;/,/g;
	$page.="<meta name=\"description\" content=\"$p{description}\" />\n";
	$page.="<meta name=\"keywords\" content=\"$kwds\" />\n";
}

$page.="<link rel=\"stylesheet\" type=\"text/css\" href=\"$p{csssrc}\" />\n" if defined $p{csssrc};
$page.="<style type=\"text/css\">$p{css}</style>\n" if defined $p{css};
$page.="<link rel=\"alternate\" type=\"$p{feedtype}\" title=\"$p{feedtitle}\" href=\"$p{feed}\" />\n" 
	if defined $p{feed};
$page.="<link rel=\"meta\" type=\"application/rdf+xml\" title=\"$p{foaftitle}\" href=\"$p{foaf}\" />\n" 
	if defined $p{foaf};
$page.=$p{extras} if defined $p{extras};
$page.="</head><body>\n";

if (defined $p{body})
{
	$page.=$p{body};
	my %footp=%p;
	$footp{string}=1;
	$page.=end_page(\%footp);
}

if ($p{string})
{
	return $page;
}
else
{
	print $page;
}

} #</start_page>

sub end_page
{
	my $page;

	$_[0]={} unless defined $_[0];

	my %p=%{$_[0]};
	$page.="\n</body></html>\n";

	if ($p{string})
	{
		return $page;
	}
	else
	{
		print $page;
	}
}

sub iso2time
{
	$_[0] =~ /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/ or return undef;
	return timelocal($6,$5,$4,$3,$2-1,$1-1900);
}

sub getvars
{
	# A 'Lite' version of CGI.pm's param function
	# Returns a reference to a hash of arrays of
	# name/value pairs.

	my (@nvps,%vars);

	# Look after anything coming from a
	# POST form
	if (lc($ENV{REQUEST_METHOD}) eq 'post')
	{
		read(STDIN, my $postdata, $ENV{CONTENT_LENGTH});
		push(@nvps,split(/&/,$postdata));
	}

	# Pick up anything passed through the 
	# query string, either by a GET form
	# or direct by URI
	my $qs=$ENV{QUERY_STRING}; 
	$qs=~s/&/;/g;
	push(@nvps,split(/;/,$qs));
	
	foreach my $nv (@nvps)
	{
		my @a=split(/=/,$nv);
		$a[0]=~tr/+/ /;
		$a[0]=~s/%([\da-f][\da-f])/chr(hex($1))/egi;
		$a[1]="" unless defined $a[1];
		$a[1]=~tr/+/ /;
		$a[1]=~s/%([\da-f][\da-f])/chr(hex($1))/egi;
		push @{$vars{$a[0]}},$a[1];
	}
	return %vars;
}

1;
__END__
# Below is stub documentation for your module. You'd better edit it!

=head1 NAME

HTML::XHTML::Lite - Light-weight Perl module for XHTML CGI applications

=head1 SYNOPSIS

  use HTML::XHTML::Lite;
  
     start_page({
  	title=>'my_title',
	description=>'my_description',
	...
     });

  OR pass a reference to a hash:

     start_page(\%my_page_data);
  
  
  end_page();

  my %vars=getvars();

=head1 DESCRIPTION

This module provides a light-weight alternative to the Perl built-in, CGI.pm, for
those who wish for an easy way to produce a well-formed XHTML <head></head>,
with Dublin Core metadata. A function to create a footer is provided for 
completeness and it is even possible to provide body text to return a complete 
document.  Provision is made for the inclusion of links to RSS (or other) feeds 
and FOAF data.  The 'extras' property allows for the insertion of arbitrary elements 
into the document header.

In addition to the generation of XHTML, the function getvars() is included to
populate a hash with data from the query string and POST data.  This is an
unsophisticated equivalent to CGI.pm's $q->para('xyz') functionality and does
NOT work with forms where enc="multipart/form-data" - in other words, forms that
upload files.  You will need to use CGI.pm to handle these until such time 
as that functionality is added to this module.

The aim of this module is to help produce content that is both accessible and
machine-parseable.  One of the methods of start_page(), nodoctype=>1, is provided
purely for the purpose of being able to leave out required components to check
that any validation systems being used are actually working.

=head2 FUNCTIONS

=head3 start_page()

=head4 Example

   use strict;
   use HTML::XHTML::Lite;

   start_page({
   	title=>'My Web Page',
	description=>'About My Web Page',
	creator=>'Matthew Smith',
	identifier=>'http://www.mss.cx',
	foaf=>'http://www.mss.cx/foaf.rdf',
	foaftitle=>'FOAF data',
	});

=head4 Methods

   string	Return the XHTML created as a scalar variable; the
   default behaviour is to print the XHTML to STDOUT or the
   currently selected handle.

   	my $foo=start_page({string=>1, ....., });

   isfile	Target is a file, so we don't want to create any
   headers.  This example writes to a file, without any headers.

   	open (OUT,">foo.out");
	select OUT;
	start_page({isfile=>, ....., });
	select STDOUT;
	close OUT;

   legacy	In addition to the Dublin Core dc.description and
   dc.subject, "legacy" metadata, description and keywords, are
   inserted.  The description is a straight copy of dc.description
   and keywords is dc.subject, with the semicolons (;) replaced by
   commas (,).

   	start_page({
		legacy=>1, 
		description=>$description,
		subject=>$subject,
		.....,
		});

   nodoctype	Produce a "broken" document with no Doctype declaration.
   This should only be used for test purposes.

   noxml	Do not put the <?xml ... ?> processing instruction at
   the start of the document.

   nohead	Combines nodoctype and noxml methods.

=head4 Properties

   title	The title of the page; defaults to 'Untitled Document'
   if not provided.

   dctitle	This property allows for dc.title to take a different
   value to the page title; defaults to the page title if not provided.

   identifier	Value for dc.identifier

   description	Value for dc.description

   subject	Value for dc.subject

   rights	Value for dc.rights
    
   creator	Value for dc.creator

   created	Value for dc.date.created (ISO8601 format)

   updated	Value for dc.date.updated (ISO8601 format)

   date		Value for dc.date (ISO8601 format)

   lang		Default page language (defaults to 'en')

   charset	Page character set (defaults to 'utf=8')

   content_type MIME type for the document - defaults to text/html
   		but other values, such as application/xml, may be
		used.
   
   expires	Date and time for the expiry date in the HTTP header
   		(ISO8601 format). As this module was written for CGI
		applications where data returned was seldom the same,
		this defaults to the current time, thus immediate
		expiry.

   css		CSS to be included in the document header; takes
   		prececence over csssrc styling instructions, if both
		css and csssrc are used.  For more complex situations
		where more flexibility is required, it is best to
		provide any CSS elements through the 'extras' property.

   csssrc	URI of an external CSS file

   feed		URI of an RSS (or other) feed

   feedtype	MIME type of feed - defaults to application/rss+xml

   feedtitle	Title for feed - defaults to "RSS Feed for [page title]"

   foaf		URI of a FOAF document; MIME type is provided as
   		application/rdf+xml

   foaftitle	Title for FOAF document - defaults to "FOAF"

   extras	Everything else!  If this module doesn't provide the
   		property you want, just put it in here.

   body		Providing a value for 'body' will cause an entire
   		page to be created, the <head>, body (as provided)
		and a call to end_page.

=head3 end_page()

   Close <body> and <html> elements.  Takes a hash ref [like start_page()]
   with arguments.  Currently, only 'string' is supported; returns a scalar
   if true (value is 1).

=head4 EXAMPLES

   end_page();

   - prints "\n</body></html>\n"

   end_page({string=>1});

   - returns scalar "\n</body></html\n"

=head1 DEPENDENCIES

   This module requires the following modules to be installed:

   HTTP::Date
   Time::Local

   These are used to create time/date strings for the HTTP headers.


=head1 TO DO

   * Inclusion of dc.accessibility, when more mature
   * Links to EARL assertions about the document
   * You tell me...

=head1 APPLICATION EXAMPLE

#!/usr/bin/perl

# Programme to create XHTML template
# through command line interaction.

use strict;
use warnings;
use HTML::XHTML::Lite;

my @tnow=localtime(time());
my $yearnow=1900+$tnow[5];
my $datenow="$yearnow-$tnow[4]-$tnow[3]";

print "C R E A T E   X H T M L   D O C U M E N T\n";
print "-----------------------------------------\n\n";

my $myname=ui('Your name','Fred Bloggs');
my $myname_=$myname;
$myname_=~s/\s/_/g; $myname_=~s/\.//g;
my $defrights="(C) Copyright $yearnow $myname";
my $filename=ui('File name',"${datenow}_${myname_}.html");
my $title=ui('dc:title','Untitled');
my $description=ui('dc:description','My Document');
my $subject=ui('dc:subject','');
my $creator=ui('dc:creator',$myname);
my $created=ui('dc:date.created',$datenow);
my $updated=ui('dc:date.updated',$datenow);
my $rights=ui('dc:rights',$defrights);
my $identifier=ui('dc:identifier',"file://$filename");
my $stylesheet=ui('Stylesheet source URI','default.css');

open (OUT,">$filename") or die "Can't write to $filename: $!";
select OUT;
start_page({
	isfile=>1,
	title=>$title, description=>$description, subject=>$subject,
	creator=>$creator, created=>$created, updated=>$updated,
	rights=>$rights, identifier=>$identifier,
	csssrc=>$stylesheet,
	body=>"<h1>$title</h1>",
	});
select STDOUT;
close OUT;

sub ui
{
	my ($prompt,$def)=@_;
	$prompt.=" [$def]" if defined $def;
	$prompt.=':';
	print $prompt;
	$_=<STDIN>;
	chomp;
	if (defined $def) { return $_ ? $_ : $def; }
	else { return $_; }
}


=head1 SEE ALSO

A web page for this module may be found here:
http://www.mss.cx/xhtmllite/

The alternative:	man CGI.pm
Dublin Core Metadata:	http://www.dublincore.org
XHTML Specification:	http://www.w3.org/TR/xhtml1/

=head1 AUTHOR

Matthew Smith, smiffy@cpan.org

Matthew welcomes feedback and suggestions regarding this module.

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2005 by Matthew Smith

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.5 or,
at your option, any later version of Perl 5 you may have available.

=cut