changeset 1:bc8961a81af6 release

2007-08-13 22:53:14 by steve Initial revision
author steve
date Mon, 13 Aug 2007 22:53:14 +0000
parents 30c6796bded8
children 76908e95f398
files .cvsignore Makefile README bin/chronicle blog/.cvsignore chroniclerc etc/README etc/entry.template etc/index.template etc/index.xml.template etc/month.template etc/month.xml.template etc/style.css etc/tags.template etc/tags.xml.template tests/Makefile tests/modules.sh tests/modules.t tests/no-tabs.t tests/perl-syntax.t tests/pod.t
diffstat 21 files changed, 2197 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.cvsignore	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,1 @@
+output
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,93 @@
+#
+#  Utility makefile for people working with chronicle
+#
+#  The targets are intended to be useful for people who are using
+# the CVS repository - but it also contains other useful targets.
+#
+# Steve
+# --
+# http://www.steve.org.uk/
+#
+# $Id: Makefile,v 1.1.1.1 2007-08-13 22:53:14 steve Exp $
+
+
+#
+#  Only used to build distribution tarballs.
+#
+DIST_PREFIX = ${TMP}
+VERSION     = 0.1
+BASE        = chronicle
+
+
+#
+#  Installation prefix, useful for the Debian package.
+#
+prefix=
+
+
+nop:
+	@echo "Valid targets are (alphabetically) :"
+	@echo " "
+	@echo " clean         = Remove bogus files and any local output."
+	@echo " diff          = Run a 'cvs diff'."
+	@echo " test          = Run our simple test cases."
+	@echo " test-verbose  = Run our simple test cases, verbosely."
+	@echo " update        = Update from the CVS repository."
+	@echo " "
+
+
+#
+#  Delete all temporary files, recursively.
+#
+clean:
+	@find . -name '.*~' -exec rm \{\} \;
+	@find . -name '.#*' -exec rm \{\} \;
+	@find . -name '*~' -exec rm \{\} \;
+	@find . -name '*.bak' -exec rm \{\} \;
+	@find . -name '*.tmp' -exec rm \{\} \;
+	@if [ -d output ]; then rm -rf output; mkdir output; fi
+#
+#  Show what has been changed in the local copy vs. the CVS repository.
+#
+diff:
+	cvs diff --unified 2>/dev/null
+
+
+#
+#  Make a new release tarball, and make a GPG signature.
+#
+release: clean
+	rm -rf $(DIST_PREFIX)/$(BASE)-$(VERSION)
+	rm -f $(DIST_PREFIX)/$(BASE)-$(VERSION).tar.gz
+	cp -R . $(DIST_PREFIX)/$(BASE)-$(VERSION)
+	find  $(DIST_PREFIX)/$(BASE)-$(VERSION) -name "CVS" -print | xargs rm -rf
+	rm -rf $(DIST_PREFIX)/$(BASE)-$(VERSION)/debian
+	cd $(DIST_PREFIX) && tar --exclude=.cvsignore -cvf $(DIST_PREFIX)/$(BASE)-$(VERSION).tar $(BASE)-$(VERSION)/
+	gzip $(DIST_PREFIX)/$(BASE)-$(VERSION).tar
+	mv $(DIST_PREFIX)/$(BASE)-$(VERSION).tar.gz .
+	rm -rf $(DIST_PREFIX)/$(BASE)-$(VERSION)
+	gpg --armour --detach-sign $(BASE)-$(VERSION).tar.gz
+
+
+#
+#  Run the test suite.
+#
+test:
+	prove --shuffle tests/
+
+
+#
+#  Run the test suite verbosely.
+#
+test-verbose:
+	prove --shuffle --verbose tests/
+
+
+
+#
+#  Update the local copy from the CVS repository.
+#
+#  NOTE: Removes empty local directories.
+#
+update: 
+	cvs -z3 update -A -P -d 2>/dev/null
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,52 @@
+
+chronicle - A blog compiler
+---------------------------
+
+  Chronicle is a tool which will convert a directory of text files
+ into a static HTML weblog, or blog.
+
+  The system supports tagged entries, and several other useful features,
+ but it is primarily designed to be as simple to possible to install
+ and use.
+
+
+Installation
+------------
+
+  No installation is currently required; simply create your blog entries
+ in the data/ directory and run "./bin/chronicle" to create HTML files
+ in the output/ directory.
+
+  Each output page will be re-created from scratch at this point, so
+ if you've edited any of your files they will be regenerated to include
+ your updated text.
+
+
+
+Blog Format
+-----------
+
+  The blog format is very simple.  Each file should start like this:
+
+   --
+   title:  The title of my post
+   date: 12 August 2007
+   tags: foo, bar, baz
+   ...
+   ...
+   ...
+   --
+
+  The tags are optional, but recommended, similarly the date of the post
+ is used in preference to the current date.
+
+
+Customisation
+-------------
+
+  Templates are used to create the output, and you will find those
+ located within the ./etc/ directory.
+
+
+Steve
+--
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/chronicle	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,1277 @@
+#!/usr/bin/perl -w
+
+=head1 NAME
+
+chronicle - A blog compiler.
+
+=cut
+
+=head1 SYNOPSIS
+
+
+  Help Options:
+
+   --help        Show the help information for this script.
+
+   --manual      Read the manual for this script.
+
+   --verbose     Show useful debugging information.
+
+   --version     Show the version number and exit.
+
+=cut
+
+
+=head1 ABOUT
+
+  Chronicle is a simple tool to convert a collection of text files,
+ located in a single directory, into a static collection of HTML
+ pages which comprise a blog.
+
+  It supports only the bare minimum of features which are required
+ to be useful:
+
+   * Tagging support.
+
+   * RSS support.
+
+   * Archive support.
+
+  The obvious deficiencies are:
+
+   * Lack of support for commenting.
+
+   * Lack of pingback/trackback support.
+
+  Having said that it is a robust, stable, and useful system.
+
+=cut
+
+
+=head1 AUTHOR
+
+ Steve
+ --
+ http://www.steve.org.uk/
+
+ $Id: chronicle,v 1.1.1.1 2007-08-13 22:53:14 steve Exp $
+
+=cut
+
+=head1 LICENSE
+
+Copyright (c) 2007 by Steve Kemp.  All rights reserved.
+
+This module is free software;
+you can redistribute it and/or modify it under
+the same terms as Perl itself.
+The LICENSE file contains the full text of the license.
+
+=cut
+
+
+use strict;
+use warnings;
+use Date::Parse;
+use File::Copy;
+use File::Path;
+use Getopt::Long;
+use HTML::Template;
+use Pod::Usage;
+
+
+
+
+#
+#  Configuration values read initially from the global configuration
+# file, then optionally overridden by the command line.
+#
+my %CONFIG;
+
+
+
+#
+#  Setup default options.
+#
+setupDefaultOptions();
+
+
+#
+#  Read the per-user configuration file.
+#
+readConfigurationFile();
+
+
+#
+#  Parse the command line arguments.
+#
+parseCommandLineArguments();
+
+
+#
+#  Should we run something before we start?
+#
+if ( $CONFIG{'pre-build'} )
+{
+    $CONFIG{'verbose'} && print "Running command: $CONFIG{'pre-build'}\n";
+
+    system($CONFIG{'pre-build'});
+}
+
+#
+#  Make sure our output directory exists.
+#
+mkpath( $CONFIG{'output'}, 0, 0755 ) if ( ! -d $CONFIG{'output'} );
+
+
+
+#
+#  Parse each of the given text files, and build up a datastructure
+# we can use to create our pages.
+#
+#  The data-structure is a hash of arrays.  The hash key is the blog
+# entry's filename, and the array stored as the hash's value has
+# keys such as:
+#
+#   tags  => [ 'test', 'testing' ]
+#   date  => '1st july 2007'
+#   title => 'Some title'
+#
+#
+my %data = createDataStructure();
+
+#
+#  Find each unique tag which is used within our text.
+#
+my %all_tags;
+%all_tags = findAllTags() unless( $CONFIG{'no-tags'} );
+
+
+#
+#  Find each unique month + year we've used.
+#
+my %all_dates = findAllMonths();
+%all_dates = findAllMonths() unless( $CONFIG{'no-archive'} );
+
+
+#
+#  Now create the global tag + date loops which are used for our
+# sidebar.
+#
+my %CLOUD;
+$CLOUD{'tag'}     = createTagCloud( %all_tags ) unless( $CONFIG{'no-tags'} );
+$CLOUD{'archive'} = createDateCloud( %all_dates ) unless( $CONFIG{'no-archive'} );;
+
+
+#
+#  Output each static page.
+#
+$CONFIG{'verbose'} && print "Creating static pages:\n";
+foreach my $file ( keys %data )
+{
+    outputStaticPage( $file );
+}
+
+
+
+#
+#  Build an output page for each tag which we've discovered.
+#
+foreach my $tagName ( sort keys %all_tags )
+{
+    $CONFIG{'verbose'} && print "Creating tag page: $tagName\n";
+
+    outputTagPage( $tagName );
+}
+
+
+
+#
+#  Now build the archives.
+#
+foreach my $date ( keys( %all_dates ) )
+{
+    $CONFIG{'verbose'} && print "Creating archive page: $date\n";
+
+    outputArchivePage( $date );
+}
+
+
+
+#
+#  Now output the most recent entries for our front-page.
+#
+outputIndexPage();
+
+
+
+#
+#  Copy the stylesheet into place.
+#
+copyStyleSheet();
+
+
+#
+#  Post-build command?
+#
+if ( $CONFIG{'post-build'} )
+{
+    $CONFIG{'verbose'} && print "Running command: $CONFIG{'post-build'}\n";
+
+    system($CONFIG{'post-build'});
+}
+
+#
+#  All done.
+#
+exit;
+
+
+
+
+
+=begin doc
+
+  Setup the default options we'd expect into our global configuration hash.
+
+=end doc
+
+=cut
+
+sub setupDefaultOptions
+{
+    $CONFIG{'input'}      = "./blog";
+    $CONFIG{'output'}     = "./output";
+    $CONFIG{'template'}   = "./etc";
+    $CONFIG{'url-prefix'} = "";
+}
+
+
+
+=begin doc
+
+  Parse the command line arguments this script was given.
+
+=end doc
+
+=cut
+
+sub parseCommandLineArguments
+{
+    my $HELP    = 0;
+    my $MANUAL  = 0;
+    my $VERSION = 0;
+
+    #
+    #  Parse options.
+    #
+    GetOptions(
+               # Help options
+               "help",         \$HELP,
+               "manual",       \$MANUAL,
+               "verbose",      \$CONFIG{'verbose'},
+               "version",      \$VERSION,
+
+               # paths
+               "input=s",      \$CONFIG{'input'},
+               "output=s",     \$CONFIG{'output'},
+               "templates=s",  \$CONFIG{'templates'},
+
+               # optional
+               "pattern=s",    \$CONFIG{'pattern'},
+               "no-tags",      \$CONFIG{'no-tags'},
+               "no-archive",   \$CONFIG{'no-archive'},
+
+               # commands
+               "pre-build=s",  \$CONFIG{'pre-build'},
+               "post-build=s", \$CONFIG{'post-build'},
+
+              );
+
+    pod2usage(1) if $HELP;
+    pod2usage(-verbose => 2 ) if $MANUAL;
+
+    if ( $VERSION )
+    {
+        my $REVISION      = '$Revision: 1.1.1.1 $';
+        if ( $REVISION =~ /1.([0-9.]+) / )
+        {
+            $REVISION = $1;
+        }
+
+        logprint( "chronicle release $REVISION\n" );
+        exit;
+    }
+}
+
+
+
+=begin doc
+
+  Create our global datastructure, by reading each of the blog
+ files and extracting:
+
+  1.  The title of the entry.
+
+  2.  Any tags which might be present.
+
+  3.  The date upon which it was made.
+
+=end doc
+
+=cut
+
+sub createDataStructure
+{
+    my %results;
+
+    if ( ! -d $CONFIG{'input'} )
+    {
+        print <<EOF;
+
+  The blog input directory $CONFIG{'input'} does not exist.
+
+  Aborting.
+EOF
+
+        exit
+    }
+
+    #
+    #  Did the user override the default pattern?
+    #
+    my $pattern = $CONFIG{'pattern'} || "*";
+    my $count   = 0;
+
+    foreach my $file ( sort( glob( $CONFIG{'input'} . "/" . $pattern ) ) )
+    {
+        my $title   = '';
+        my $date    = '';
+        my $private = 0;
+
+        my @tags;
+
+        open( INPUT, "<", $file ) or die "Failed to open blog file $file - $!";
+        while( my $line = <INPUT> )
+        {
+            if ( $line =~ /^tags: (.*)/i )
+            {
+                my $tag .= $1;
+                foreach my $t ( split( /,/, $tag ) )
+                {
+                    # strip leading and trailing space.
+                    $t =~ s/^\s+//;
+                    $t =~ s/\s+$//;
+
+                    # skip empty tags.
+                    next if ( !length($t) );
+
+                    # lowercase and store the tags.
+                    $tag = lc($tag);
+                    push ( @tags, $t );
+                }
+            }
+            elsif (( $line =~ /^title: (.*)/i ) && !length($title) )
+            {
+                $title = $1;
+            }
+            elsif (( $line =~ /^date: (.*)/i ) && !length($date) )
+            {
+                $date = $1;
+            }
+            elsif ( $line =~ /^status: (.*)/i )
+            {
+                my $level = $1;
+                $private = 1 if ( $level =~ /private/i);
+            }
+        }
+        close( INPUT );
+
+        $results{$file} = { tags => \@tags,
+                            title => $title,
+                            date  => $date } unless( $private );
+
+        $count += 1;
+    }
+
+    #
+    #  Make sure we found some entries.
+    #
+    if ( $count < 1 )
+    {
+        print <<EOF;
+
+  There were no text files found in the input directory
+ $CONFIG{'input'} which matched the pattern '$pattern'.
+
+  Aborting.
+
+EOF
+        exit;
+    }
+
+    return %results;
+}
+
+
+
+=begin doc
+
+  Find each distinct tag which has been used within blog entries,
+ and the number of times each one has been used.
+
+=end doc
+
+=cut
+
+sub findAllTags
+{
+    my %allTags;
+
+    foreach my $f ( keys %data )
+    {
+        my $h = $data{$f};
+        my $tags = $h->{'tags'} || undef;
+        foreach my $t ( @$tags )
+        {
+            $allTags{$t}+=1;
+        }
+    }
+
+    return( %allTags );
+}
+
+
+
+=begin doc
+
+  Create a structure for a tag cloud.
+
+=end doc
+
+=cut
+
+sub createTagCloud
+{
+    my( %unique ) = ( @_ );
+
+    my $results;
+
+    foreach my $key ( sort keys( %unique ) )
+    {
+        push( @$results,
+            { tag => $key,
+              count => $unique{$key} } );
+    }
+    return $results;
+
+}
+
+
+
+=begin doc
+
+  Find each of the distinct Month + Year pairs for entries which
+ have been created.
+
+=end doc
+
+=cut
+
+sub findAllMonths
+{
+    my %allDates;
+    foreach my $f ( keys %data )
+    {
+        my $h    = $data{$f};
+        next if ( !$h );
+
+        my $date = $h->{'date'} || undef;
+        next if ( !$date );
+
+        #
+        #  Strip to month
+        #
+        my ($ss,$mm,$hh,$day,$month,$year,$zone) = strptime($date);
+        my @abbr = qw( January February March April May June July August September October November December );
+        $month = $abbr[$month];
+        $year += 1900;
+        $date = $month . " " . $year;
+
+        $allDates{$date}+=1;
+    }
+
+    return( %allDates );
+}
+
+
+
+=begin doc
+
+  Create a data structure which can be used for our archive layout.
+
+  This is a little messy too.  It mostly comes because we want to
+ have a nested loop so that we can place our entries in a nice manner.
+
+  TODO: FIXME.
+=end doc
+
+=cut
+
+sub createDateCloud
+{
+    my( %unique ) = ( @_ );
+
+    my $results;
+
+    #
+    #  First find the distinct years.
+    #
+    my %years;
+    foreach my $key ( sort keys %unique  )
+    {
+        if ( $key =~ /([0-9]+)/ )
+        {
+            my $year = $1;
+            $years{$year} += 1;
+        }
+    }
+
+    #
+    #  Now for each year we want to push on the number of
+    # months
+    #
+    foreach my $year ( keys %years )
+    {
+        my $months;
+
+        foreach my $key ( keys %unique )
+        {
+            if ( $key =~ /(.*) ([0-9]+)/ )
+            {
+                my $y  = $2;
+                my $m = $1;
+                if ( $year eq $y )
+                {
+                    my $count = $unique{ $key };
+                    my $month = $m;
+
+                    push( @$months, { month => $m,
+                                      count => $count } );
+                }
+            }
+        }
+
+        push( @$results, { year => $year,
+                           months => $months } );
+    }
+    return $results;
+}
+
+
+
+=begin doc
+
+  This function will return a hash containing our tag information,
+ the values of the hash will be an array of filenames which contain
+ that entry.
+
+=end doc
+
+=cut
+
+sub readTagInformation
+{
+    my( @files ) = (@_);
+
+    my %results;
+
+    foreach my $file ( @files )
+    {
+        my $tag;
+        open( FILE, "<", $file ) or die "Failed to read: $file - $!";
+        foreach my $line ( <FILE> )
+        {
+            next unless $line =~ /^tags:(.*)/i;
+
+            my $tags = $1;
+
+            foreach my $t ( split( /,/ , $tags ) )
+            {
+                # strip leading and trailing space.
+                $t =~ s/^\s+//;
+                $t =~ s/\s+$//;
+
+                # skip empty tags.
+                next if ( !length($t) );
+
+                # lowercase all tags
+                $t = lc($t);
+
+                # Store the filename in the hash for this tag.
+                my $cur = $results{$t};
+                push @$cur, $file;
+                $results{$t} = $cur;
+            }
+        }
+        close( FILE );
+    }
+    return %results;
+}
+
+
+
+=begin doc
+
+
+=end doc
+
+=cut
+
+sub readDateInformation
+{
+    my( @files ) = (@_);
+
+    my %results;
+
+    foreach my $file ( @files )
+    {
+        my $tag;
+        open( FILE, "<", $file ) or die "Failed to read: $file - $!";
+        foreach my $line ( <FILE> )
+        {
+            next unless $line =~ /^date:(.*)/i;
+            my ($ss,$mm,$hh,$day,$month,$year,$zone) = strptime($1 );
+
+            my @abbr = qw( January February March April May June July August September October November December );
+
+            $year += 1900;
+            $month = $abbr[$month];
+
+            # Store the filename in the hash for this tag.
+            my $cur = $results{$year}{$month};
+            push @$cur, $file;
+            $results{$year}{$month} = $cur;
+        }
+        close( FILE );
+    }
+    return %results;
+}
+
+
+
+
+=begin doc
+
+  Sort by date.
+
+=end doc
+
+=cut
+
+sub bywhen
+{
+    my ($ss,$mm,$hh,$day,$month,$year,$zone) = strptime($a->{'date'});
+    my ($ss2,$mm2,$hh2,$day2,$month2,$year2,$zone2) = strptime($b->{'date'});
+
+    if ( !defined($year ) || ( !defined( $year2 ) ) )
+    {
+        return 0;
+    }
+
+    return "$year2$month2$day2" <=> "$year$month$day";
+}
+
+
+
+
+=begin doc
+
+  Output the index page + index RSS feed.
+
+=end doc
+
+=cut
+
+sub outputIndexPage
+{
+
+    #
+    #  Holder for the blog entries.
+    #
+    my $entries;
+
+    #
+    #  Find all the entries and sort them to be most recent first.
+    #
+    my $tmp;
+    foreach my $file ( keys ( %data ) )
+    {
+        my $blog = readBlogEntry( $file );
+        push( @$tmp, $blog ) if (keys( %$blog ) );
+    }
+    my @tmp2 = sort bywhen @$tmp;
+
+
+    #
+    #  The number of entries to display upon the index.
+    #
+    my $max = $CONFIG{'entry-count'} || 10;
+    foreach my $f ( @tmp2 )
+    {
+        push( @$entries, $f ) if ( $max > 0 );
+        $max -= 1;
+    }
+
+    #
+    #  Open the index template.
+    #
+    my $template = loadTemplate( "index.template" );
+
+    $template->param( entries => $entries )     if ( $entries );
+    $template->param( tagcloud => $CLOUD{'tag'} )   if ( $CLOUD{'tag'} );
+    $template->param( datecloud => $CLOUD{'archive'} ) if ( $CLOUD{'archive'} );
+
+    #
+    #  Page to use
+    #
+    my $index = $CONFIG{'filename'} || "index.html";
+
+    open( OUTPUT, ">", "$CONFIG{'output'}/$index" );
+    print OUTPUT $template->output();
+    close( OUTPUT );
+
+    #
+    #  Output the RSS feed
+    #
+    $template = loadTemplate( "index.xml.template",
+                              die_on_bad_params => 0 );
+    $template->param( entries => $entries ) if ( $entries );
+    open( OUTPUT, ">", "$CONFIG{'output'}/index.rss" );
+    print OUTPUT $template->output();
+    close( OUTPUT );
+}
+
+
+
+=begin doc
+
+  Write out a /tags/$foo/index.html containing each blog entry which has the
+ tag '$foo'.
+
+=end doc
+
+=cut
+
+sub outputTagPage
+{
+    my ( $tagName ) = ( @_ );
+
+    #
+    #  Make the tag directory.
+    #
+    my $dir = "$CONFIG{'output'}/tags/";
+    mkpath( $dir, 0, 0755 ) if ( ! -d $dir );
+
+    #
+    #  Now the specific one.
+    #
+    $dir = "$CONFIG{'output'}/tags/$tagName";
+    mkdir $dir, 0755 if ( ! -d $dir );
+
+    my %allTags;
+    my %tagEntries;
+    foreach my $f ( keys %data )
+    {
+        my $h = $data{$f};
+        my $tags = $h->{'tags'} || undef;
+        foreach my $t ( @$tags )
+        {
+            $allTags{$t}+=1;
+            my $a = $tagEntries{$t};
+            push @$a, $f ;
+            $tagEntries{$t}= $a;
+        }
+    }
+
+    my $matching =  $tagEntries{$tagName};
+
+    my $entries;
+
+    #
+    #  Now read the matching entries.
+    #
+    foreach my $f ( sort @$matching )
+    {
+        my $blog = readBlogEntry( $f );
+        if (keys( %$blog ) )
+        {
+            $CONFIG{'verbose'} && print "\tAdded: $f\n";
+            push( @$entries, $blog );
+        }
+    }
+
+    #
+    #  Now write the output as a HTML page.
+    #
+    my $template = loadTemplate( "tags.template" );
+    $template->param( entries => $entries )   if ( $entries );
+    $template->param( tagname => $tagName );
+    $template->param( tagcloud => $CLOUD{'tag'} ) if ( $CLOUD{'tag'} );
+    $template->param( datecloud => $CLOUD{'archive'} ) if ( $CLOUD{'archive'} );
+
+    #
+    #  Page to use
+    #
+    my $index = $CONFIG{'filename'} || "index.html";
+
+    open( OUTPUT, ">", "$dir/$index" );
+    print OUTPUT $template->output();
+    close( OUTPUT );
+
+    #
+    #  Now output the .xml file
+    #
+    $template = loadTemplate( "tags.xml.template", die_on_bad_params => 0 );
+    $template->param( entries => $entries ) if ( $entries );
+    $template->param( tagname => $tagName ) if ( $tagName );
+    open( OUTPUT, ">", "$dir/$tagName.rss" );
+    print OUTPUT $template->output();
+    close( OUTPUT );
+
+}
+
+
+
+=begin doc
+
+  Output the archive page for the given Month + Year.
+
+  This function is a *mess* and iterates over the data structure much
+ more often than it needs to.
+
+  TODO:  FIXME
+
+=end doc
+
+=cut
+
+sub outputArchivePage
+{
+    my( $date ) = ( @_ );
+
+    #
+    #  Should we abort?
+    #
+    if ( $CONFIG{'no-archive'} )
+    {
+        $CONFIG{'verbose'} && print "Ignoring archive page, as instructed.\n";
+        return;
+    }
+
+
+    my $year = '';
+    my $month = '';
+    if ( $date =~ /(.*) ([0-9]+)/ )
+    {
+        $year  = $2;
+        $month = $1;
+    }
+
+    #
+    #  Make the directory
+    #
+    my $dir = "$CONFIG{'output'}/archive/$year";
+    mkpath( $dir, 0, 0755 ) if ( ! -d $dir );
+
+    $dir .= "/$month";
+    mkdir $dir, 0755 if ( ! -d $dir );
+
+    my $entries;
+
+
+    my %allDates;
+    my %dateEntries;
+    foreach my $f ( keys %data )
+    {
+        my $h    = $data{$f};
+        my $date = $h->{'date'} || undef;
+        $allDates{$date}+=1;
+
+        #
+        #  Strip to month
+        #
+        my ($ss,$mm,$hh,$day,$month,$year,$zone) = strptime($date);
+        my @abbr = qw( January February March April May June July August September October November December );
+        $month = $abbr[$month];
+        $year += 1900;
+        $date = $month . " " . $year;
+        my $a = $dateEntries{$date};
+        push @$a, $f ;
+        $dateEntries{$date}= $a;
+    }
+
+
+    my $matching = $dateEntries{$date};
+    foreach my $f ( reverse @$matching )
+    {
+        $CONFIG{'verbose'} && print "\tAdded: $f\n";
+
+        my $blog = readBlogEntry( $f );
+        if (keys( %$blog ) )
+        {
+            push( @$entries, $blog );
+        }
+    }
+
+    #
+    #  Now write the output as a HTML page.
+    #
+    my $template = loadTemplate( "month.template" );
+    $template->param( entries => $entries )   if ( $entries );
+    $template->param( year => $year, month => $month );
+    $template->param( tagcloud => $CLOUD{'tag'} ) if ( $CLOUD{'tag'} );
+    $template->param( datecloud => $CLOUD{'archive'} ) if ( $CLOUD{'archive'} );
+
+    #
+    #  Page to use
+    #
+    my $index = $CONFIG{'filename'} || "index.html";
+    open( OUTPUT, ">", "$dir/$index" );
+    print OUTPUT $template->output();
+    close( OUTPUT );
+
+    #
+    #  Now the RSS page.
+    #
+    $template = loadTemplate( "month.xml.template", die_on_bad_params => 0 );
+    $template->param( entries => $entries ) if ( $entries );
+    $template->param( month => $month, year => $year );
+    open( OUTPUT, ">", "$dir/$month.rss" );
+    print OUTPUT $template->output();
+    close( OUTPUT );
+}
+
+
+
+
+=begin doc
+
+  Output static page.
+
+=end doc
+
+=cut
+
+sub outputStaticPage
+{
+    my ( $filename ) = ( @_ );
+
+    #
+    #  Load the template
+    #
+    my $template = loadTemplate( "entry.template" );
+
+
+    #
+    #  Just the name of the file.
+    #
+    my $basename = $filename;
+    if ( $basename =~ /(.*)\/(.*)/ )
+    {
+        $basename=$2;
+    }
+    #
+    #  Read the entry
+    #
+    my $static = readBlogEntry( $filename );
+
+    my $title = $static->{'title'} || $basename;
+    my $tags  = $static->{'tags'};
+    my $body  = $static->{'body'};
+    my $date  = $static->{'date'} || "";
+
+    $CONFIG{'verbose'} && print "\t$filename\n";
+
+    #
+    #  Convert to suitable filename.
+    #
+    my $file = fileToTitle($title);
+    $file    = $CONFIG{'output'} . "/" . $file;
+
+    $template->param( title => $title );
+    $template->param( tags => $tags ) if ( $tags );
+    $template->param( date => $date ) if ( $date );
+    $template->param( body => $body );
+    $template->param( tagcloud => $CLOUD{'tag'} ) if ( $CLOUD{'tag'} );
+    $template->param( datecloud => $CLOUD{'archive'} ) if ( $CLOUD{'archive'} );
+    open( OUTPUT, ">", $file );
+    print OUTPUT $template->output();
+    close( OUTPUT );
+
+}
+
+
+
+=begin doc
+
+  Return a hash of interesting data from our blog file.
+
+=end doc
+
+=cut
+
+sub readBlogEntry
+{
+    my ( $filename ) = ( @_);
+
+    my %entry;
+
+
+    my $title = "";
+    my $tags  = "";
+    my $body  = "";
+    my $date  = "";
+
+    open( ENTRY, "<", $filename ) or die "Failed to read $filename $!";
+    while( my $line = <ENTRY> )
+    {
+        #
+        #  Append any tags.
+        #
+        if ( $line =~ /^tags: (.*)/i )
+        {
+            $tags .= $1;
+        }
+        elsif (( $line =~ /^title: (.*)/i ) && !length($title) )
+        {
+            $title = $1;
+        }
+        elsif (( $line =~ /^date: (.*)/i ) && !length($date) )
+        {
+            $date = $1;
+        }
+        else
+        {
+            $body .= $line;
+        }
+    }
+    close( ENTRY );
+
+    #
+    #  If we have title then we can store it
+    #
+    my $entryTags;
+
+    foreach my $tag ( split( /,/, $tags ) )
+    {
+        # strip leading and trailing space.
+        $tag =~ s/^\s+//;
+        $tag =~ s/\s+$//;
+
+        # skip empty tags.
+        next if ( !length($tag) );
+        $tag = lc($tag);
+        push ( @$entryTags, { tag => $tag } );
+    }
+
+    #
+    #  Get the link
+    #
+    my $link = fileToTitle( $title );
+
+    #
+    #  If the date isn't set then use todays.
+    #
+    if ( ! defined($date) ||( !length( $date ) ) )
+    {
+        my @abbr = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov De
+c );
+        my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
+          localtime(time);
+
+        $year += 1900;
+
+        $date = "$mday $abbr[$mon] $year";
+    }
+
+    #
+    #  Store the entry.
+    #
+    $entry{'title'} = $title;
+    $entry{'body'}  = $body if ( $body );
+    $entry{'link'}  = $link;
+    $entry{'date'}  = $date;
+    $entry{'tags'}  = $entryTags if ( $entryTags );
+    return \%entry;
+}
+
+
+
+=begin doc
+
+  Create a filename for an URL which does not contain unsafe
+ characters.
+
+=end doc
+
+=cut
+
+sub fileToTitle
+{
+    my( $file ) = ( @_ );
+
+    if ( $file =~ /(.*)\.(.*)/ )
+    {
+        $file = $1;
+    }
+    $file =~ s/ /_/g;
+    $file =~ s/\///g;
+    $file =~ s/\\//g;
+
+    my $suffix = $CONFIG{'suffix'} ||".html";
+    $file .= $suffix;
+    return( $file );
+
+}
+
+
+
+=begin doc
+
+  Load a template file.
+
+=end doc
+
+=cut
+
+sub loadTemplate
+{
+    my( $file, %params ) = (@_);
+
+    #
+    #  Make sure the file exists.
+    #
+    if ( ! -e $CONFIG{'template'} . "/" . $file )
+    {
+        print <<EOF;
+
+  The template file $file was not found in our template directory
+ of $CONFIG{'template'}.
+
+  Aborting.
+EOF
+        exit;
+    }
+
+    my $t = HTML::Template->new( filename          => $file,
+                                 path              => $CONFIG{'template'},
+                                 loop_context_vars => 1,
+                                 global_vars       => 1,
+                                 %params );
+
+    #
+    #  Global setting.
+    #
+    if ( $CONFIG{'url_prefix'} )
+    {
+        $t->param( url_prefix => $CONFIG{'url_prefix'} );
+    }
+
+    return( $t );
+}
+
+
+
+=begin doc
+
+  Read the configuration file ".chroniclerc" if it exists.
+
+=end doc
+
+=cut
+
+sub readConfigurationFile
+{
+    my $file = $ENV{'HOME'} . "/.chroniclerc";
+    return if ( ! -e $file );
+
+    my $line = "";
+
+    open( FILE, "<", $file ) or die "Cannot read file '$file' - $!";
+
+    while (defined($line = <FILE>) )
+    {
+        chomp $line;
+        if ($line =~ s/\\$//)
+        {
+            $line .= <FILE>;
+            redo unless eof(FILE);
+        }
+
+        # Skip lines beginning with comments
+        next if ( $line =~ /^([ \t]*)\#/ );
+
+        # Skip blank lines
+        next if ( length( $line ) < 1 );
+
+        # Strip trailing comments.
+        if ( $line =~ /(.*)\#(.*)/ )
+        {
+            $line = $1;
+        }
+
+        # Find variable settings
+        if ( $line =~ /([^=]+)=([^\n]+)/ )
+        {
+            my $key = $1;
+            my $val = $2;
+
+            # Strip leading and trailing whitespace.
+            $key =~ s/^\s+//;
+            $key =~ s/\s+$//;
+            $val =~ s/^\s+//;
+            $val =~ s/\s+$//;
+
+            # command expansion?
+            if ( $val =~ /(.*)`([^`]+)`(.*)/ )
+            {
+                # store
+                my $pre  = $1;
+                my $cmd  = $2;
+                my $post = $3;
+
+                # get output
+                my $output = `$cmd`;
+                chomp( $output );
+
+                # build up replacement.
+                $val = $pre . $output . $post;
+            }
+
+            # Store value.
+            $CONFIG{ $key } = $val;
+        }
+    }
+
+    close( FILE );
+
+}
+
+
+
+=begin doc
+
+  Copy the stylesheet into place unless it already exists.
+
+=end doc
+
+=cut
+
+sub copyStyleSheet
+{
+    my $input  = $CONFIG{'template'} . "/style.css";
+    my $output = $CONFIG{'output'} . "/style.css";
+
+    copy( $input, $output ) unless( -e $output );
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/blog/.cvsignore	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,1 @@
+*.txt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/chroniclerc	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,85 @@
+##
+#  Configuration file for the chronicle blog compiler.
+##
+
+
+##
+#
+#  NOTE:
+#
+#    For this file to be used it should be renamed ~/.chroniclerc
+#
+####
+
+
+
+
+#
+#  Input directory
+# 
+input = /home/skx/cvs/chronicle/data
+
+
+#
+#  The pattern of files to include
+#
+# pattern = *.txt
+#
+
+
+#
+#  Output directory to write the blog to
+#
+output = /home/skx/cvs/chronicle/output
+
+
+#
+#  Directory containing the templates
+#
+template = /home/skx/cvs/chronicle/etc
+
+
+#
+#  The number of entries to include on the index.
+#
+# entry-count = 10
+#
+
+
+#
+#  We can disable the sidebar if we want
+#
+#  no-tags = 1
+#
+#  no-archive = 1
+#
+
+
+#  The filename to use for tag lings
+#
+# filename = index.html
+#
+
+
+#
+#  Suffix to use for single entries.
+#
+# suffix = .html
+
+
+#
+#  URL prefix, if any.
+#
+url_prefix = http://www.steve.org.uk/Software/chronicle/demo/
+
+
+#
+#  A command to run pre-build
+#
+# pre-build = cvs update -A -d
+
+
+#
+#  A command to run post-build.
+#
+# post-build = scp -r output/* steve@www.steve.org.uk:/home/www/www.steve.org.uk/htdocs/Software/chronicle/demo
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/etc/README	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,33 @@
+
+etc
+===
+
+  This directory contains the template files which are used by
+ chronicle.
+
+  In some cases there are two version of each template, a normal
+ one and one with '.xml' in its name.  The latter is used for the
+ corresponding RSS feed.
+
+
+Contents
+--------
+
+  entry.template
+   Used for a single blog entry.
+
+  index.template
+   Used for the most recent N entries, the "front-page" of your blog.
+
+  month.template
+   Used for the archive view of your blog.
+   
+  style.css
+   The stylesheet applied to viewers of your blog.
+
+  tags.template
+   Used to display all entries with a given tag.
+
+
+Steve
+-- 
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/etc/entry.template	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,47 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+ <head>
+  <title>Blog: <!-- tmpl_var name='title' --></title>
+  <link rel="stylesheet" type="text/css" media="screen" href="<!-- tmpl_var name='url_prefix' -->style.css">
+ </head>
+ <body>
+
+
+<h1><a href="index.html">Blog</a> &gt; <!-- tmpl_var name='title' --></h1>
+ <div class="entry">
+  <div class="title"><!-- tmpl_var name='title' --></div>
+  <div class="date"><!-- tmpl_var name='date' --></div>
+  <div class="body"><!-- tmpl_var name='body' --></div>
+  <!-- tmpl_if name='tags' -->
+  <div class="tags">Tags: <!-- tmpl_loop name='tags' --><a href="<!-- tmpl_var name='url_prefix' -->tags/<!-- tmpl_var name='tag' -->"><!-- tmpl_var name='tag' --></a><!-- tmpl_if name="__last__" -->.<!-- tmpl_else -->, <!-- /tmpl_if --><!-- /tmpl_loop --></div>
+   <!-- /tmpl_if -->
+  </div>
+
+<div id="sidebar">
+<!-- tmpl_if name='tagcloud' -->
+<!-- tmpl_if name='datecloud' -->
+<h2>Archive</h2>
+<ul>
+<!-- tmpl_loop name='datecloud' -->
+<li><!-- tmpl_var name='year' -->
+<ul>
+<!-- tmpl_loop name='months' -->
+<li><a href="<!-- tmpl_var name='url_prefix' -->archive/<!-- tmpl_var name='year' -->/<!-- tmpl_var name='month' -->/"><!-- tmpl_var name='month' --> (<!-- tmpl_var name='count' -->)</a></li>
+<!-- /tmpl_loop -->
+</ul></li>
+<!-- /tmpl_loop -->
+</ul>
+<!-- /tmpl_if name='datecloud' -->
+
+<h2>Tags</h2>
+<!-- Tag Cloud -->
+<ul>
+<!-- tmpl_loop name='tagcloud' -->
+ <li><a href="<!-- tmpl_var name='url_prefix' -->tags/<!-- tmpl_var name='tag' -->"><!-- tmpl_var name='tag' --> (<!-- tmpl_var name='count' -->)</a></li>
+<!-- /tmpl_loop -->
+</ul>
+<!-- /tmpl_if -->
+</div>
+ </body>
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/etc/index.template	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,58 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+ <head>
+  <title>Blog: Index</title>
+  <link rel="stylesheet" type="text/css" media="screen" href="<!-- tmpl_var name='url_prefix' -->style.css">
+  <link rel="alternate" title="RSS Feed" href="<!-- tmpl_var name='url_prefix' -->index.rss" type="application/rss+xml">
+
+ </head>
+ <body>
+
+ <h1>Most Recent Entries</h1>
+
+ <!-- tmpl_loop name='entries' -->
+ <div class="entry">
+  <div class="title"><a href="<!-- tmpl_var name='url_prefix' --><!-- tmpl_var name='link' -->"><!-- tmpl_var name='title' --></a></div>
+  <div class="date"><!-- tmpl_var name='date' --></div>
+  <div class="body"><!-- tmpl_var name='body' --></div>
+  <div class="tags">
+   <!-- tmpl_if name='tags' -->
+   Tags: <!-- tmpl_loop name='tags' --><a href="<!-- tmpl_var name='url_prefix' -->tags/<!-- tmpl_var name='tag' -->"><!-- tmpl_var name='tag' --></a><!-- tmpl_if name="__last__" -->.<!-- tmpl_else -->, <!-- /tmpl_if --><!-- /tmpl_loop --> 
+  <!-- tmpl_else -->
+    No tags
+  <!-- /tmpl_if -->
+  </div>
+ </div>
+ <div class="padding"></div>
+ <!-- /tmpl_loop -->
+
+
+<div id="sidebar">
+
+<!-- tmpl_if name='datecloud' -->
+<h2>Archive</h2>
+<ul>
+<!-- tmpl_loop name='datecloud' -->
+<li><!-- tmpl_var name='year' -->
+<ul>
+<!-- tmpl_loop name='months' -->
+<li><a href="<!-- tmpl_var name='url_prefix' -->archive/<!-- tmpl_var name='year' -->/<!-- tmpl_var name='month' -->/"><!-- tmpl_var name='month' --> (<!-- tmpl_var name='count' -->)</a></li>
+<!-- /tmpl_loop -->
+</ul></li>
+<!-- /tmpl_loop -->
+</ul>
+<!-- /tmpl_if name='datecloud' -->
+
+<!-- tmpl_if name='tagcloud' -->
+<h2>Tags</h2>
+<!-- Tag Cloud -->
+<ul>
+<!-- tmpl_loop name='tagcloud' -->
+ <li><a href="<!-- tmpl_var name='url_prefix' -->tags/<!-- tmpl_var name='tag' -->"><!-- tmpl_var name='tag' --> (<!-- tmpl_var name='count' -->)</a></li>
+<!-- /tmpl_loop -->
+</ul>
+<!-- /tmpl_if -->
+</div>
+</body>
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/etc/index.xml.template	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<rss version='2.0' xmlns:lj='http://www.livejournal.org/rss/lj/1.0/'>
+	<channel>
+		<title>Blog Entries</title>
+		<description>Blog Entries</description>
+
+	<!-- tmpl_loop name="entries" -->
+	<item>
+		<title><!-- tmpl_var name="title" escape='html' --></title>
+		<link><!-- tmpl_var name='url_prefix' --><!-- tmpl_var name='link' --></link>        
+		<guid isPermaLink="true"><!-- tmpl_var name='url_prefix' --><!-- tmpl_var name='link' --></guid>
+		<description><!-- tmpl_var name="body" escape='html' --></description>
+	</item>
+	<!-- /tmpl_loop -->
+        </channel>
+</rss>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/etc/month.template	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,67 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+ <head>
+  <title>Blog : Entries from <!-- tmpl_var name='month' --> <!-- tmpl_var name='year' --></title>
+  <link rel="stylesheet" type="text/css" media="screen" href="<!-- tmpl_if name='url_prefix' --><!-- tmpl_var name='url_prefix' --><!-- tmpl_else -->../../<!-- /tmpl_if -->style.css">
+  <link rel="alternate" title="RSS Feed" href="<!-- tmpl_var name='month' escape='html' -->.rss" type="application/rss+xml">
+ </head>
+ <body>
+<!-- XML FEED -->
+
+<h1><a href="<!-- tmpl_if name='url_prefix' --><!-- tmpl_var name='url_prefix' --><!-- tmpl_else -->../../<!-- /tmpl_if -->">Blog</a> &gt; Entries from <!-- tmpl_var name='month' --> <!-- tmpl_var name='year' --></h1>
+<p>If you wish you may subscribe to an <a href="<!-- tmpl_var name='month' escape='html' -->.rss">RSS feed</a> of matching entries.</p>
+
+<!-- tmpl_loop name='entries' -->
+<div class="entry">
+ <div class="title">
+ <a href="<!-- tmpl_if name='url_prefix' --><!-- tmpl_var name='url_prefix' --><!-- tmpl_else -->../../<!-- /tmpl_if --><!-- tmpl_var name='link' -->"><!-- tmpl_var name='title' --></a>
+ </div>
+ <div class="date">
+  <!-- tmpl_var name='date' -->
+ </div>
+
+ <div class="body">
+  <!-- tmpl_var name='body' -->
+ </div>
+ <div class="tags">
+ <!-- tmpl_if name='tags' -->
+  Tags: <!-- tmpl_loop name='tags' --><a href="<!-- tmpl_if name='url_prefix' --><!-- tmpl_var name='url_prefix' --><!-- tmpl_else -->../../<!-- /tmpl_if -->tags/<!-- tmpl_var name='tag' -->"><!-- tmpl_var name='tag' --></a><!-- tmpl_if name="__last__" -->.<!-- tmpl_else -->, <!-- /tmpl_if --><!-- /tmpl_loop -->
+ <!-- tmpl_else -->
+  No tags
+ <!-- /tmpl_if -->
+ </div>
+</div>
+ <div class="padding"></div>
+<!-- /tmpl_loop -->
+
+<!-- tmpl_if name='tagcloud' -->
+
+
+
+<div id="sidebar">
+<!-- tmpl_if name='datecloud' -->
+<h2>Archive</h2>
+<ul>
+<!-- tmpl_loop name='datecloud' -->
+<li><!-- tmpl_var name='year' -->
+<ul>
+<!-- tmpl_loop name='months' -->
+<li><a href="<!-- tmpl_if name='url_prefix' --><!-- tmpl_var name='url_prefix' --><!-- tmpl_else -->../../<!-- /tmpl_if -->archive/<!-- tmpl_var name='year' -->/<!-- tmpl_var name='month' -->/"><!-- tmpl_var name='month' --> (<!-- tmpl_var name='count' -->)</a></li>
+<!-- /tmpl_loop -->
+</ul></li>
+<!-- /tmpl_loop -->
+</ul>
+<!-- /tmpl_if name='datecloud' -->
+
+<h2>Tags</h2>
+<!-- Tag Cloud -->
+<ul>
+<!-- tmpl_loop name='tagcloud' -->
+ <li><a href="<!-- tmpl_if name='url_prefix' --><!-- tmpl_var name='url_prefix' --><!-- tmpl_else -->../../<!-- /tmpl_if -->tags/<!-- tmpl_var name='tag' -->"><!-- tmpl_var name='tag' --> (<!-- tmpl_var name='count' -->)</a></li>
+<!-- /tmpl_loop -->
+</ul>
+<!-- /tmpl_if -->
+</div>
+</body>
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/etc/month.xml.template	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<rss version='2.0' xmlns:lj='http://www.livejournal.org/rss/lj/1.0/'>
+	<channel>
+		<title>Entries from <!-- tmpl_var name='month' --> <!-- tmpl_var name='year' --></title>
+		<description>Entries from <!-- tmpl_var name='month' --> <!-- tmpl_var name='year' --></description>
+
+	<!-- tmpl_loop name="entries" -->
+	<item>
+		<title><!-- tmpl_var name="title" escape='html' --></title>
+		<link><!-- tmpl_if name='url_prefix' --><!-- tmpl_var name='url_prefix' --><!-- tmpl_else -->../../<!-- /tmpl_if --><!-- tmpl_var name='link' --></link>        
+		<guid isPermaLink="true"><!-- tmpl_if name='url_prefix' --><!-- tmpl_var name='url_prefix' --><!-- tmpl_else -->../../<!-- /tmpl_if --><!-- tmpl_var name='link' --></guid>
+		<description><!-- tmpl_var name="body" escape='html' --></description>
+	</item>
+	<!-- /tmpl_loop -->
+        </channel>
+</rss>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/etc/style.css	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,78 @@
+
+body {
+	padding: 0 10px;
+	padding-top: 1px;
+	padding-bottom: 1em;
+	border-right: 1px solid rgb(128, 128, 128);
+	background-color: white;
+	color: black;
+	margin: 0;
+	margin-right: 185px;
+}
+
+
+/*
+ * Special markup for weblog entries.
+ */
+div.entry {
+	border-left: 1px solid rgb(128, 128, 128);
+	border-right: 1px solid rgb(128, 128, 128);
+	border-top: 1px solid rgb(128, 128, 128);
+	border-bottom: 1px solid rgb(128, 128, 128);
+        margin: 10px 0px;
+}
+
+div.padding {
+        padding-top: 15px;
+        padding-bottom: 15px;
+}
+div.entry div.body {
+	padding: 10px 10px;
+}
+
+div.entry .title {
+	background-color: #eee;
+	border-bottom: 1px solid rgb(128, 128, 128);
+	font-weight: bold;
+	font-size: 120%;
+	padding: 0.26ex 10px;
+	
+}
+div.entry div.date {
+        text-align: right;
+}
+div.entry div.title a {
+	color: black !important;
+	text-decoration: none !important;
+}
+div.entry div.title a:hover {
+	color: black !important;
+	text-decoration: none !important;
+}
+
+div.entry div.tags {
+	border-top: 1px solid rgb(128, 128, 128);
+	font-style: italic;
+	font-family: Verdana, Georgia, Arial, sans-serif;
+	font-size: 90%;
+	text-align: right;
+}
+
+
+div#sidebar {
+	position: absolute;
+	top: 0px;
+	right: 0px;
+	width: 165px;
+
+	font-family: sans-serif;
+	font-size: 80%;
+
+	text-align: justify;
+
+	padding: 0 10px;
+
+	background-color: white;
+
+	margin: 0;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/etc/tags.template	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,66 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+ <head>
+  <title>Blog : Entries Tagged <!-- tmpl_var name='tagname' --></title> 
+  <link rel="stylesheet" type="text/css" media="screen" href="<!-- tmpl_if name='url_prefix' --><!-- tmpl_var name='url_prefix' --><!-- tmpl_else -->../../<!-- /tmpl_if -->style.css">
+  <link rel="alternate" title="RSS Feed" href="<!-- tmpl_var name='tagname' escape='html' -->.rss" type="application/rss+xml">
+ </head>
+ <body>
+
+<h1><a href="<!-- tmpl_if name='url_prefix' --><!-- tmpl_var name='url_prefix' --><!-- tmpl_else -->../../<!-- /tmpl_if -->">Blog</a> &gt; Entries Tagged &quot;<tt><!-- tmpl_var name='tagname' --></tt>&quot;</h1>
+
+<p>If you wish you may subscribe to an <a href="<!-- tmpl_var name='tagname' escape='html' -->.rss">RSS feed</a> of matching entries.</p>
+
+
+<!-- tmpl_loop name='entries' -->
+<div class="entry">
+ <div class="title">
+ <a href="<!-- tmpl_if name='url_prefix' --><!-- tmpl_var name='url_prefix' --><!-- tmpl_else -->../../<!-- /tmpl_if --><!-- tmpl_var name='link' -->"><!-- tmpl_var name='title' --></a>
+ </div>
+ <div class="date">
+  <!-- tmpl_var name='date' -->
+ </div>
+
+ <div class="body">
+  <!-- tmpl_var name='body' -->
+ </div>
+ <!-- tmpl_if name='tags' -->
+ <div class="tags">
+  Tags: <!-- tmpl_loop name='tags' --><a href="<!-- tmpl_if name='url_prefix' --><!-- tmpl_var name='url_prefix' --><!-- tmpl_else -->../../<!-- /tmpl_if -->tags/<!-- tmpl_var name='tag' -->"><!-- tmpl_var name='tag' --></a><!-- tmpl_if name="__last__" -->.<!-- tmpl_else -->, <!-- /tmpl_if --><!-- /tmpl_loop -->
+ </div>
+ <!-- /tmpl_if -->
+</div>
+<div class="padding"></div>
+<!-- /tmpl_loop -->
+
+
+
+
+<div id="sidebar">
+<!-- tmpl_if name='datecloud' -->
+<h2>Archive</h2>
+<ul>
+<!-- tmpl_loop name='datecloud' -->
+<li><!-- tmpl_var name='year' -->
+<ul>
+<!-- tmpl_loop name='months' -->
+<li><a href="<!-- tmpl_if name='url_prefix' --><!-- tmpl_var name='url_prefix' --><!-- tmpl_else -->../../<!-- /tmpl_if -->archive/<!-- tmpl_var name='year' -->/<!-- tmpl_var name='month' -->/"><!-- tmpl_var name='month' --> (<!-- tmpl_var name='count' -->)</a></li>
+<!-- /tmpl_loop -->
+</ul></li>
+<!-- /tmpl_loop -->
+</ul>
+<!-- /tmpl_if name='datecloud' -->
+
+<!-- tmpl_if name='tagcloud' -->
+<h2>Tags</h2>
+<!-- Tag Cloud -->
+<ul>
+<!-- tmpl_loop name='tagcloud' -->
+ <li><a href="<!-- tmpl_if name='url_prefix' --><!-- tmpl_var name='url_prefix' --><!-- tmpl_else -->../../<!-- /tmpl_if -->tags/<!-- tmpl_var name='tag' -->"><!-- tmpl_var name='tag' --> (<!-- tmpl_var name='count' -->)</a></li>
+<!-- /tmpl_loop -->
+</ul>
+<!-- /tmpl_if -->
+</div>
+</body>
+</html>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/etc/tags.xml.template	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<rss version='2.0' xmlns:lj='http://www.livejournal.org/rss/lj/1.0/'>
+	<channel>
+		<title>Entries tagged <!-- tmpl_var name='tagname' escape='html' --></title>
+		<description>Entries tagged <!-- tmpl_var name='tagname' escape='html' --></description>
+
+	<!-- tmpl_loop name="entries" -->
+	<item>
+		<title><!-- tmpl_var name="title" escape='html' --></title>
+		<link><!-- tmpl_if name='url_prefix' --><!-- tmpl_var name='url_prefix' --><!-- tmpl_else -->../../<!-- /tmpl_if --><!-- tmpl_var name='link' --></link>        
+		<guid isPermaLink="true"><!-- tmpl_if name='url_prefix' --><!-- tmpl_var name='url_prefix' --><!-- tmpl_else -->../../<!-- /tmpl_if --><!-- tmpl_var name='link' --></guid>
+		<description><!-- tmpl_var name="body" escape='html' --></description>
+	</item>
+	<!-- /tmpl_loop -->
+        </channel>
+</rss>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/Makefile	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,17 @@
+
+all:
+	@cd ..; prove --shuffle tests/
+
+verbose:
+	@cd ..; prove --shuffle --verbose tests/
+
+
+modules: .PHONY
+	./modules.sh > modules.t
+
+.PHONY:
+	true
+
+clean:
+
+	rm *~
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/modules.sh	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,35 @@
+#!/bin/sh
+#
+#  Automatically attempt to create a test which ensures all the modules
+# used in the code are availabe.
+#
+# Steve
+# --
+# http://www.steve.org.uk/
+#
+# $Id: modules.sh,v 1.1.1.1 2007-08-13 22:53:14 steve Exp $
+#
+
+cat <<EOF
+#!/usr/bin/perl -w -I..
+#
+#  Test that all the Perl modules we require are available.
+#
+#  This list is automatically generated by modules.sh
+#
+# Steve
+# --
+#
+
+use Test::More qw( no_plan );
+
+EOF
+
+
+for i in `rgrep '^use ' .. | awk '{print $2}' | tr -d 
\;\(\) | sort | uniq`; \
+    do \
+     echo "BEGIN{ use_ok( '$i' ); }"; \
+     echo "require_ok( '$i' );" ; \
+     echo -e "\n" ; \
+done
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/modules.t	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,52 @@
+#!/usr/bin/perl -w -I..
+#
+#  Test that all the Perl modules we require are available.
+#
+#  This list is automatically generated by modules.sh
+#
+# Steve
+# --
+#
+
+use Test::More qw( no_plan );
+
+BEGIN{ use_ok( 'Date::Parse' ); }
+require_ok( 'Date::Parse' );
+
+
+BEGIN{ use_ok( 'File::Copy' ); }
+require_ok( 'File::Copy' );
+
+
+BEGIN{ use_ok( 'File::Find' ); }
+require_ok( 'File::Find' );
+
+
+BEGIN{ use_ok( 'File::Path' ); }
+require_ok( 'File::Path' );
+
+
+BEGIN{ use_ok( 'Getopt::Long' ); }
+require_ok( 'Getopt::Long' );
+
+
+BEGIN{ use_ok( 'HTML::Template' ); }
+require_ok( 'HTML::Template' );
+
+
+BEGIN{ use_ok( 'Pod::Usage' ); }
+require_ok( 'Pod::Usage' );
+
+
+BEGIN{ use_ok( 'strict' ); }
+require_ok( 'strict' );
+
+
+BEGIN{ use_ok( 'Test::More' ); }
+require_ok( 'Test::More' );
+
+
+BEGIN{ use_ok( 'warnings' ); }
+require_ok( 'warnings' );
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/no-tabs.t	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,100 @@
+#!/usr/bin/perl -w
+#
+#  Test that none of our scripts contain any literal TAB characters.
+#
+# Steve
+# --
+# $Id: no-tabs.t,v 1.1.1.1 2007-08-13 22:53:14 steve Exp $
+
+
+use strict;
+use File::Find;
+use Test::More qw( no_plan );
+
+
+#
+#  Find all the files beneath the current directory,
+# and call 'checkFile' with the name.
+#
+find( { wanted => \&checkFile, no_chdir => 1 }, '.' );
+
+
+
+#
+#  Check a file.
+#
+#
+sub checkFile
+{
+    # The file.
+    my $file = $File::Find::name;
+
+    # We don't care about directories
+    return if ( ! -f $file );
+
+    # Nor about backup files.
+    return if ( $file =~ /~$/ );
+
+    # Nor about files which start with ./debian/
+    return if ( $file =~ /^\.\/debian\// );
+
+    # See if it is a shell/perl file.
+    my $isShell = 0;
+    my $isPerl  = 0;
+
+    # Read the file.
+    open( INPUT, "<", $file );
+    foreach my $line ( <INPUT> )
+    {
+        if ( ( $line =~ /\/bin\/sh/ ) ||
+             ( $line =~ /\/bin\/bash/ ) )
+        {
+            $isShell = 1;
+        }
+        if ( $line =~ /\/usr\/bin\/perl/ )
+        {
+            $isPerl = 1;
+        }
+    }
+    close( INPUT );
+
+    #
+    #  We don't care about files which are neither perl nor shell.
+    #
+    if ( $isShell || $isPerl )
+    {
+        #
+        #  Count TAB characters
+        #
+        my $count = countTabCharacters( $file );
+
+        is( $count, 0, "Script has no tab characters: $file" );
+    }
+}
+
+
+
+#
+#  Count and return the number of literal TAB characters contained
+# in the specified file.
+#
+sub countTabCharacters
+{
+    my ( $file ) = (@_);
+    my $count = 0;
+
+    open( FILE, "<", $file )
+      or die "Cannot open $file - $!";
+    foreach my $line ( <FILE> )
+    {
+        # We will count multiple tab characters in a single line.
+        while( $line =~ /(.*)\t(.*)/ )
+        {
+            $count += 1;
+            $line = $1 . $2;
+        }
+    }
+    close( FILE );
+
+    return( $count );
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/perl-syntax.t	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,70 @@
+#!/usr/bin/perl -w
+#
+#  Test that every perl file we have passes the syntax check.
+#
+# Steve
+# --
+# $Id: perl-syntax.t,v 1.1.1.1 2007-08-13 22:53:14 steve Exp $
+
+
+use strict;
+use File::Find;
+use Test::More qw( no_plan );
+
+
+#
+#  Find all the files beneath the current directory,
+# and call 'checkFile' with the name.
+#
+find( { wanted => \&checkFile, no_chdir => 1 }, '.' );
+
+
+
+#
+#  Check a file.
+#
+#  If this is a perl file then call "perl -c $name", otherwise
+# return
+#
+sub checkFile
+{
+    # The file.
+    my $file = $File::Find::name;
+
+    # We don't care about directories
+    return if ( ! -f $file );
+
+    # `modules.sh` is a false positive.
+    return if ( $file =~ /modules.sh$/ );
+
+    # See if it is a perl file.
+    my $isPerl = 0;
+
+    # Read the file.
+    open( INPUT, "<", $file );
+    foreach my $line ( <INPUT> )
+    {
+        if ( $line =~ /\/usr\/bin\/perl/ )
+        {
+            $isPerl = 1;
+        }
+    }
+    close( INPUT );
+
+    #
+    #  Return if it wasn't a perl file.
+    #
+    return if ( ! $isPerl );
+
+    #
+    #  Now run 'perl -c $file' to see if we pass the syntax
+    # check.  We add a couple of parameters to make sure we're
+    # really OK.
+    #
+    #        use strict "vars";
+    #        use strict "subs";
+    #
+    my $retval = system( "perl -Mstrict=subs -Mstrict=vars -c $file 2>/dev/null >/dev/null" );
+
+    is( $retval, 0, "Perl file passes our syntax check: $file" );
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/pod.t	Mon Aug 13 22:53:14 2007 +0000
@@ -0,0 +1,17 @@
+#!/usr/bin/perl -w
+
+#
+#  Test that the POD we use in our modules is valid.
+#
+
+
+use strict;
+use Test::More;
+eval "use Test::Pod 1.00";
+plan skip_all => "Test::Pod 1.00 required for testing POD" if $@;
+
+#
+#  Run the test(s).
+#
+my @poddirs = qw( bin );
+all_pod_files_ok( all_pod_files( @poddirs ) );