# HG changeset patch # User steve # Date 1187045594 0 # Node ID bc8961a81af66b8bba592f6ceeec202dbf32ec51 # Parent 30c6796bded8920a687c6134940033a5de9fcfe4 2007-08-13 22:53:14 by steve Initial revision diff -r 30c6796bded8 -r bc8961a81af6 .cvsignore --- /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 diff -r 30c6796bded8 -r bc8961a81af6 Makefile --- /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 diff -r 30c6796bded8 -r bc8961a81af6 README --- /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 diff -r 30c6796bded8 -r bc8961a81af6 bin/chronicle --- /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 < ) + { + 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 <{'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 ( ) + { + 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 ( ) + { + 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 = ) + { + # + # 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 <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 = ) ) + { + chomp $line; + if ($line =~ s/\\$//) + { + $line .= ; + 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 ); +} diff -r 30c6796bded8 -r bc8961a81af6 blog/.cvsignore --- /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 diff -r 30c6796bded8 -r bc8961a81af6 chroniclerc --- /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 diff -r 30c6796bded8 -r bc8961a81af6 etc/README --- /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 diff -r 30c6796bded8 -r bc8961a81af6 etc/entry.template --- /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 @@ + + + + Blog: <!-- tmpl_var name='title' --> + + + + + +

Blog >

+
+
+
+
+ +
Tags: .,
+ +
+ + + + \ No newline at end of file diff -r 30c6796bded8 -r bc8961a81af6 etc/index.template --- /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 @@ + + + + Blog: Index + + + + + + +

Most Recent Entries

+ + +
+
+
+
+
+ + Tags: ., + + No tags + +
+
+
+ + + + + + \ No newline at end of file diff -r 30c6796bded8 -r bc8961a81af6 etc/index.xml.template --- /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 @@ + + + + Blog Entries + Blog Entries + + + + <!-- tmpl_var name="title" escape='html' --> + + + + + + + diff -r 30c6796bded8 -r bc8961a81af6 etc/month.template --- /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 @@ + + + + Blog : Entries from <!-- tmpl_var name='month' --> <!-- tmpl_var name='year' --> + + + + + + +

Blog > Entries from

+

If you wish you may subscribe to an RSS feed of matching entries.

+ + +
+
+ +
+
+ +
+ +
+ +
+
+ + Tags: ., + + No tags + +
+
+
+ + + + + + + + + \ No newline at end of file diff -r 30c6796bded8 -r bc8961a81af6 etc/month.xml.template --- /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 @@ + + + + Entries from <!-- tmpl_var name='month' --> <!-- tmpl_var name='year' --> + Entries from + + + + <!-- tmpl_var name="title" escape='html' --> + ../../ + ../../ + + + + + diff -r 30c6796bded8 -r bc8961a81af6 etc/style.css --- /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 diff -r 30c6796bded8 -r bc8961a81af6 etc/tags.template --- /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 @@ + + + + Blog : Entries Tagged <!-- tmpl_var name='tagname' --> + + + + + +

Blog > Entries Tagged ""

+ +

If you wish you may subscribe to an RSS feed of matching entries.

+ + + +
+
+ +
+
+ +
+ +
+ +
+ +
+ Tags: ., +
+ +
+
+ + + + + + + + \ No newline at end of file diff -r 30c6796bded8 -r bc8961a81af6 etc/tags.xml.template --- /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 @@ + + + + Entries tagged <!-- tmpl_var name='tagname' escape='html' --> + Entries tagged + + + + <!-- tmpl_var name="title" escape='html' --> + ../../ + ../../ + + + + + diff -r 30c6796bded8 -r bc8961a81af6 tests/Makefile --- /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 *~ diff -r 30c6796bded8 -r bc8961a81af6 tests/modules.sh --- /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 < \&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 ( ) + { + 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 ( ) + { + # We will count multiple tab characters in a single line. + while( $line =~ /(.*)\t(.*)/ ) + { + $count += 1; + $line = $1 . $2; + } + } + close( FILE ); + + return( $count ); +} diff -r 30c6796bded8 -r bc8961a81af6 tests/perl-syntax.t --- /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 ( ) + { + 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" ); +} diff -r 30c6796bded8 -r bc8961a81af6 tests/pod.t --- /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 ) );