Mercurial > hg > chronicle
view bin/chronicle @ 72:a6d3f5bbbcda
Removed.
author | Steve Kemp <steve@steve.org.uk> |
---|---|
date | Sat, 08 Dec 2007 16:51:59 +0000 |
parents | e1bef4e4fb5e |
children | c64a37a823d1 |
line wrap: on
line source
#!/usr/bin/perl -w =head1 NAME chronicle - A blog compiler. =cut =head1 SYNOPSIS Path Options: --config Specify a configuration file to read. --input Specify the input directory to use. --output Specify the directory to write output to. --theme-dir Specify the path to the theme templates. --theme Specify the theme to use. --pattern Specify the pattern of files to work with. --url-prefix Specify the prefix to the live blog. Pre & Post-Build Commands: --pre-build Specify a command to execute prior to building the blog. --post-build Specify a command to execute once the blog has been built. Blog Entry Options: --format Specify the format of your entries, HTML/textile/markdown. Optional Features: --force Force the copying of static files from the blog theme. --no-archive Don't create an archive page. --no-cache Don't use the optional memcached features, even if available. --no-calander Don't use the optional calendar upon the index. --no-tags Don't produce any tag pages. --lower-case Lower-case all filenames which are output. 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 within a single directory, into a blog consisting of static HTML files. 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 BLOG FORMAT The format of the text files we process is critical to the output pages. Each entry should look something like this: =for example begin Title: This is the title of the blog post Date: 2nd March 2007 Tags: one, two, three, long tag The text of your entry goes here. =for example end In this example we can see that the entry itself has been prefaced with a small header. An entry header is contains three optional lines, if these are not present then there are sensible defaults as described below: =over 8 =item Title: Describes the title of the post. If not present the filename of the entry is used instead. =item Date: The date the post was written. If not present the creation time of the file is used instead. =item Tags: Any tags which should be associated with the entry, separated by commas. =back The text of the entry itself is assumed to be HTML, however if you have the optional modules installed you may write it in Markdown or Textile formats - if they are not present you will receive a message informing you of the names of the required modules. You may specify the format of your entries either in the configuration file, or via the command line flag B<--format>. =cut =head1 CONFIGURATION The configuration of the software is minimal, and generally performed via the command line arguments. However it is possible to save settings either in the file global /etc/chroniclerc or the per-user ~/.chroniclerc file. If you wish you may pass the name of another configuration file to the script with the B<--config> flag. This will be read after the previous two files, and may override any settings which are present. The configuration file contains lines like these: =for example begin input = /home/me/blog output = /var/www/blog format = markdown =for example end Keys which are unknown are ignored. =cut =head1 OPTIONAL CACHING To speed the rebuilding of a large blog the compiler may use a local Memcached deaemon, if installed and available. To install this, under a Debian GNU/Linux system please run: =for example begin apt-get update apt-get install memcached libcache-memcached-perl =for example end You may disable this caching behaviour with --no-cache, and see the effect with --verbose. =cut =head1 OPTIONAL CALENDAR If the 'HTML::CalendarMonthSimple' module is available each blog will contain a simple month-view of the current month upon the index. To disable this invoke the program with '--no-calendar'. =cut =head1 AUTHOR Steve -- http://www.steve.org.uk/ $Id: chronicle,v 1.28 2007-11-10 00:13:21 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 Date::Format; use File::Copy; use File::Path; use Getopt::Long; use HTML::Template; use Pod::Usage; use Time::Local; # # Release number # # NOTE: Set by 'make release'. # my $RELEASE = 'UNRELEASED'; # # The names of the months. Posted early to allow i18n users. # my @names = qw( January February March April May June July August September October November December ); # # Setup default options. # my %CONFIG = setupDefaultOptions(); # # Read the global and per-user configuration files, if they exist. # readConfigurationFile( "/etc/chroniclerc" ); readConfigurationFile( $ENV{'HOME'} . "/.chroniclerc" ); # # Parse the command line arguments. # parseCommandLineArguments(); # # Another configuration file? # readConfigurationFile( $CONFIG{'config'} ) if ( defined $CONFIG{'config'} ); # # Make sure we have arguments which are sane. # # Specifically we want to cope with the "new" 'theme-dir', and 'theme' # arguments. # # sanityCheckArguments(); # # Listing themes? # if ( $CONFIG{'list-themes'} ) { listThemes( $CONFIG{'theme-dir'} ); exit; } # Should we run something before we start? # if ( $CONFIG{'pre-build'} ) { $CONFIG{'verbose'} && print "Running command: $CONFIG{'pre-build'}\n"; system($CONFIG{'pre-build'}); } # # 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 used within our entries. # my %all_tags; %all_tags = findAllTags() unless( $CONFIG{'no-tags'} ); # # Find each unique month + year we've used. # my %all_dates; %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'} );; # # Create the output directories. # mkpath( $CONFIG{'output'}, 0, 0755 ) if ( ! -d $CONFIG{'output'} ); foreach my $tag ( keys %all_tags ) { mkpath( "$CONFIG{'output'}/tags/$tag", 0, 0755 ); } foreach my $date ( keys %all_dates ) { next unless ( $date =~ /^([0-9]{4})-([0-9]{2})/ ); mkpath( "$CONFIG{'output'}/archive/$1/$2", 0, 0755 ); } # # Output each static page. # $CONFIG{'verbose'} && print "Creating static pages:\n"; foreach my $file ( keys %data ) { outputStaticPage( $file ); } # # Build an output page for every tag which has ever been used. # foreach my $tagName ( sort keys %all_tags ) { $CONFIG{'verbose'} && print "Creating tag page: $tagName\n"; outputTagPage( $tagName ); } # # Now build the archive pages. # foreach my $date ( keys( %all_dates ) ) { $CONFIG{'verbose'} && print "Creating archive page: $date\n"; outputArchivePage( $date ); } # # Finally out the most recent entries for the front-page. # outputIndexPage(); # # Copy any static files into place. # copyStaticFiles(); # # 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 { my %CONFIG; # # Text directory. # $CONFIG{'input'} = "./blog"; # # Output directory. # $CONFIG{'output'} = "./output"; # # Theme setup # $CONFIG{'theme-dir'} = "./themes/"; $CONFIG{'theme'} = "default"; # # prefix for all links. # $CONFIG{'url-prefix'} = ""; # # Default input format # $CONFIG{'format'} = 'html'; # # Entries per-page for the index. # $CONFIG{'entry-count'} = 10; # # Don't overwrite files by default # $CONFIG{'force'} = 0; return( %CONFIG ); } =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, "list-themes", \$CONFIG{'list-themes'}, # paths "config=s", \$CONFIG{'config'}, "input=s", \$CONFIG{'input'}, "output=s", \$CONFIG{'output'}, "theme-dir=s", \$CONFIG{'theme-dir'}, "theme=s", \$CONFIG{'theme'}, "pattern=s", \$CONFIG{'pattern'}, # optional "force", \$CONFIG{'force'}, "no-tags", \$CONFIG{'no-tags'}, "no-cache", \$CONFIG{'no-cache'}, "no-archive", \$CONFIG{'no-archive'}, "lower-case", \$CONFIG{'lower-case'}, # input format. "format=s", \$CONFIG{'format'}, # prefix "url-prefix=s", \$CONFIG{'url_prefix'}, # 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.28 $'; if ( $REVISION =~ /1.([0-9.]+) / ) { $REVISION = $1; } print( "chronicle release $RELEASE\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 ) ) ) { # # Ignore directories. # next if ( -d $file ); my $tags = ''; my $title = ''; my $date = ''; my $private = 0; my @tags; open( INPUT, "<", $file ) or die "Failed to open blog file $file - $!"; while( my $line = <INPUT> ) { # # Get the tags # if ( ( $line =~ /^tags:(.*)/i ) && !length($tags) ) { $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 and store the tags. $t = lc($t); push ( @tags, $t ); } } elsif (( $line =~ /^title:(.*)/i ) && !length($title) ) { # # Get the title. # $title = $1; # strip leading and trailing space. $title =~ s/^\s+// if ( length $title ); $title =~ s/\s+$// if ( length $title ); } elsif (( $line =~ /^date:(.*)/i ) && !length($date) ) { # # Get the date. # $date = $1; # strip leading and trailing space. $date =~ s/^\s+// if ( $date ); $date =~ s/\s+$// if ( $date ); } elsif ( $line =~ /^status:(.*)/i ) { # # The security level. # my $level = $1; # strip leading and trailing space. $level =~ s/^\s+// if ( $level ); $level =~ s/\s+$// if ( $level ); $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 ) ) { # count. my $count = $unique{$key}; # size for the HTML. my $size = 10 + ( $count * 5 ); $size = 40 if ( $size >= 40 ); push( @$results, { tag => $key, count => $count, size => $size } ); } 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'}; # # Not a date? Use the ctime of the file. # if ( !defined( $date ) || !length($date) ) { # # Test the file for creation time. # my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, $atime,$mtime,$ctime,$blksize,$blocks) = stat($f); $date = localtime( $ctime ); } $date = time2str("%Y-%m", str2time($date)); $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. =end doc =cut sub createDateCloud { my( %entry_dates ) = ( @_ ); my $results; my $year; my $months; foreach my $date ( sort keys %entry_dates ) { next unless ( $date =~ /^([0-9]{4})-([0-9]{2})/ ); if ( $year and $1 ne $year ) { push( @$results, { year => $year, months => $months } ); undef $months; } $year = $1; push( @$months, { month => $2, month_name => $names[$2-1], count => $entry_dates{$date} } ); } push( @$results, { year => $year, months => $months } ); return $results; } =begin doc Sort by date. =end doc =cut sub bywhen { # # Parse and return the date # my ($ss1,$mm1,$hh1,$day1,$month1,$year1,$zone1) = strptime($a->{'date'}); my ($ss2,$mm2,$hh2,$day2,$month2,$year2,$zone2) = strptime($b->{'date'}); # # Abort if we didn't work. # die "Couldn't find first year: $a->{'date'}" unless defined($year1); die "Couldn't find second year: $b->{'date'}" unless defined($year2); # # Convert to compare # my $c = timelocal(0,0,0,$day1,$month1,$year1 + 1900); my $d = timelocal(0,0,0,$day2,$month2,$year2 + 1900); return $d <=> $c; } =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'}; foreach my $f ( @tmp2 ) { push( @$entries, $f ) if ( $max > 0 ); $max -= 1; } # # Open the index template. # my $template = loadTemplate( "index.template", die_on_bad_params => 0 ); # # create the calendar if we can. # my $calendar = createCalendar(); if ( defined( $calendar ) ) { my $text = $calendar->as_HTML(); $text =~ s/<\/?b>//g; $text =~ s/<\/?p>//g; $template->param( calendar => 1, calendar_month => $text ); } # # The entries. # $template->param( entries => $entries ) if ( $entries ); # # The clouds # $template->param( tagcloud => $CLOUD{'tag'} ) if ( $CLOUD{'tag'} ); $template->param( datecloud => $CLOUD{'archive'} ) if ( $CLOUD{'archive'} ); # # Blog title and subtitle, if present. # $template->param( blog_title => $CONFIG{'blog_title'} ) if ( $CONFIG{'blog_title'} ); $template->param( blog_subtitle => $CONFIG{'blog_subtitle'} ) if ( $CONFIG{'blog_subtitle'} ); $template->param( release => $RELEASE ); # # Page to use # my $index = $CONFIG{'filename'} || "index.html"; outputTemplate( $template, $index ); # # Output the RSS feed # $template = loadTemplate( "index.xml.template", die_on_bad_params => 0 ); $template->param( entries => $entries ) if ( $entries ); outputTemplate( $template, "index.rss" ); } =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 ) = ( @_ ); my $dir = "tags/$tagName"; 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", die_on_bad_params => 0 ); # # The entries. # $template->param( entries => $entries ) if ( $entries ); $template->param( tagname => $tagName ); # # The clouds # $template->param( tagcloud => $CLOUD{'tag'} ) if ( $CLOUD{'tag'} ); $template->param( datecloud => $CLOUD{'archive'} ) if ( $CLOUD{'archive'} ); # # Blog title and subtitle, if present. # $template->param( blog_title => $CONFIG{'blog_title'} ) if ( $CONFIG{'blog_title'} ); $template->param( blog_subtitle => $CONFIG{'blog_subtitle'} ) if ( $CONFIG{'blog_subtitle'} ); # # Page to use # my $index = $CONFIG{'filename'} || "index.html"; outputTemplate( $template, "$dir/$index" ); # # 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 ); outputTemplate( $template, "$dir/$tagName.rss" ); } =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. =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]{4})-([0-9]{2})/ ) { $year = $1; $month = $2; } # # Make the directory # my $dir = "archive/$year/$month"; my $entries; my %dateEntries; foreach my $f ( keys %data ) { my $h = $data{$f}; my $date = $h->{'date'}; # # Not a date? Use the file. # if ( !defined( $date ) || !length($date) ) { # # Test the file for creation time. # my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, $atime,$mtime,$ctime,$blksize,$blocks) = stat($f); $date = localtime( $ctime ); } $date = time2str("%Y-%m", str2time($date)); push @{$dateEntries{$date}}, $f ; } 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", die_on_bad_params => 0 ); # # The entries # $template->param( entries => $entries ) if ( $entries ); # # Output the month + year. # $template->param( year => $year, month => $month ); $template->param( month_name => $names[$month - 1 ] ); # # The clouds # $template->param( tagcloud => $CLOUD{'tag'} ) if ( $CLOUD{'tag'} ); $template->param( datecloud => $CLOUD{'archive'} ) if ( $CLOUD{'archive'} ); # # Blog title and subtitle, if present. # $template->param( blog_title => $CONFIG{'blog_title'} ) if ( $CONFIG{'blog_title'} ); $template->param( blog_subtitle => $CONFIG{'blog_subtitle'} ) if ( $CONFIG{'blog_subtitle'} ); # # Page to use # my $index = $CONFIG{'filename'} || "index.html"; outputTemplate( $template, "$dir/$index" ); # # 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 ); $template->param( month_name => $names[$month - 1 ] ); outputTemplate( $template, "$dir/$month.rss" ); } =begin doc Output static page. =end doc =cut sub outputStaticPage { my ( $filename ) = ( @_ ); # # Load the template # my $template = loadTemplate( "entry.template", die_on_bad_params => 0 ); # # Just the name of the file. # my $basename = $filename; if ( $basename =~ /(.*)\/(.*)/ ) { $basename=$2; } if ( $basename =~ /^(.*)\.(.*)$/ ) { $basename = $1; } # # Read the entry # my $static = readBlogEntry( $filename ); # # Get the pieces of information. # my $title = $static->{'title'} || $basename; my $tags = $static->{'tags'}; my $body = $static->{'body'}; my $date = $static->{'date'}; if ( !defined($date) ) { my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, $atime,$mtime,$ctime,$blksize,$blocks) = stat($filename); $date = localtime( $ctime ); } $CONFIG{'verbose'} && print "\t$filename\n"; # # Convert to suitable filename. # my $file = fileToTitle($title); # # The entry. # $template->param( title => $title ); $template->param( tags => $tags ) if ( $tags ); $template->param( date => $date ) if ( $date ); $template->param( body => $body ); # # Our clouds # $template->param( tagcloud => $CLOUD{'tag'} ) if ( $CLOUD{'tag'} ); $template->param( datecloud => $CLOUD{'archive'} ) if ( $CLOUD{'archive'} ); # # Blog title and subtitle, if present. # $template->param( blog_title => $CONFIG{'blog_title'} ) if ( $CONFIG{'blog_title'} ); $template->param( blog_subtitle => $CONFIG{'blog_subtitle'} ) if ( $CONFIG{'blog_subtitle'} ); outputTemplate( $template, $file ); } =begin doc Return a hash of interesting data from our blog file. =end doc =cut sub readBlogEntry { my ( $filename ) = ( @_); my %entry; # # Do we have the memcache module available? # my $cache = undef; my $test = "use Cache::Memcached;"; eval( $test ); if ( ( ! $@ ) && ( ! $CONFIG{'no-cache'} ) ) { # create the cache object $cache = new Cache::Memcached {'servers' => ["localhost:11211"] }; # fetch from the cache if it is present. my $cached = $cache->get( "file_$filename" ); if ( defined( $cached ) ) { $CONFIG{'verbose'} && print "memcache-get: $filename\n"; return( \%$cached ) } else { $CONFIG{'verbose'} && print "memcache-fail: $filename\n"; } } my $title = ""; # entry title. my $tags = ""; # entry tags. my $body = ""; # entry body. my $date = ""; # entry date my $status = ""; # entry privacy/security. open( ENTRY, "<", $filename ) or die "Failed to read $filename $!"; while( my $line = <ENTRY> ) { # # Get the tags. # if (( $line =~ /^tags: (.*)/i ) && !length( $tags ) ) { $tags = $1; } elsif (( $line =~ /^title: (.*)/i ) && !length($title) ) { # # Get the title # $title = $1; # strip leading and trailing space. $title =~ s/^\s+// if ( length $title ); $title =~ s/\s+$// if ( length $title ); } elsif (( $line =~ /^date: (.*)/i ) && !length($date) ) { # # Get the date. # $date = $1; # strip leading and trailing space. $date =~ s/^\s+// if ( length $date ); $date =~ s/\s+$// if ( length $date ); } elsif (( $line =~ /^status:(.*)/ ) && !length ( $status ) ) { # # Security level? # $status = $1; } else { # # Just a piece of body text. # $body .= $line; } } close( ENTRY ); # # Determine the input format to use. # my $format = lc($CONFIG{'format'}); # # Now process accordingly. # if ( $format eq 'html' ) { # nop } elsif( $format eq 'markdown' ) { $body = markdown2HTML( $body ); } elsif( $format eq 'textile' ) { $body = textile2HTML( $body ); } else { print "Unkown blog entry format ($CONFIG{'format'}).\n"; print "Treating as HTML.\n"; } # # # If we have tags then we should use them. # 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) ); # tags are lowercase. $tag = lc($tag); push ( @$entryTags, { tag => $tag } ); } # # 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 Dec ); 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{'date'} = $date; $entry{'tags'} = $entryTags if ( $entryTags ); # # No title? # if ( !defined($entry{'title'}) || !length($entry{'title'}) ) { my $basename = $filename; if ( $basename =~ /(.*)\/(.*)/ ) { $basename=$2; } if ( $basename =~ /^(.*)\.(.*)$/ ) { $basename = $1; } $entry{'title'} = $basename; } # # Get the link - after ensuring we have a title. # my $link = fileToTitle( $entry{'title'} ); $entry{'link'} = $link; # # Store the read file in the cache if we're using it. # if ( defined( $cache ) ) { $CONFIG{'verbose'} && print "memcache-set: $filename\n"; $cache->set( "file_$filename", \%entry ); } 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; # # Lower case? # $file = lc($file) if ( $CONFIG{'lower-case'} ); return( $file ); } =begin doc Load a template file. =end doc =cut sub loadTemplate { my( $file, %params ) = (@_); # # Get the directory. # my $dir = $CONFIG{'theme-dir'}; # # XML files go in theme-dir/xml/ # if ( $file =~ /\.xml\./i ) { $dir .= "/xml/"; } else { $dir .= "/" . $CONFIG{'theme'} . "/"; } # # Make sure the file exists. # if ( ! -e $dir . $file ) { print <<EOF; The template file $file was not found in the theme directory. Theme : $CONFIG{'theme'} Theme Directory: $CONFIG{'theme-dir'} We expected to find $dir$file; Aborting. EOF exit; } my $t = HTML::Template->new( filename => $file, path => $dir, loop_context_vars => 1, global_vars => 1, %params ); return( $t ); } =begin doc Set URL for top directory and output a template. =end doc =cut sub outputTemplate { my( $template, $path ) = ( @_ ); # # Select relative/absolute URL prefix. # my $top; if ( $CONFIG{'url_prefix'} ) { $top = $CONFIG{'url_prefix'}; } else { $top = $path; $top =~ s'[^/]+/'../'g; $top =~ s'[^/]*$''; } $template->param( top => $top ); open( OUTPUT, ">", "$CONFIG{'output'}/$path" ); print OUTPUT $template->output(); close( OUTPUT ); } =begin doc Read the specified configuration file if it exists. =end doc =cut sub readConfigurationFile { my( $file ) = ( @_ ); # # If it doesn't exist ignore it. # 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 Sanity check our arguments: 1. Make sure we have a theme-directory 2. Make sure we have a theme. =end doc =cut sub sanityCheckArguments { if ( !$CONFIG{'theme-dir'} ) { print <<EOF; Error - You don't have a theme directory setup. Please specify --theme-dir=/some/path, or add this to your configuration file: theme-dir = /path/to/use/ EOF exit; } if ( ! -d $CONFIG{'theme-dir'} ) { print "The theme directory you specified doesn't exist:\n"; print "\t" . $CONFIG{'theme-dir'} . "\n"; exit; } if ( !$CONFIG{'theme'} ) { print <<EOF; You've not specified a theme. Please specify --theme=xx Or add this to your configuration file: theme = xx [You may list themes with --list-themes] EOF exit; } if ( ! -d $CONFIG{'theme-dir'} . "/" . $CONFIG{'theme'} ) { print "The theme directory you specified doesn't exist in the theme directory:\n"; print "\tTheme :" . $CONFIG{'theme'} . "\n"; print "\tTheme dir:" . $CONFIG{'theme-dir'} . "\n"; print "\tExpected :" . $CONFIG{'theme-dir'} . "/" . $CONFIG{'theme'} . "\n"; exit; } } =begin doc Copy any static files from the theme directory into the "live" location in the output. This only works for a top-level target directory. Unless --force is specified we skip copying files which already exist. =end doc =cut sub copyStaticFiles { # # Soure and destination for the copy # my $input = $CONFIG{'theme-dir'} . "/" . $CONFIG{'theme'}; my $output = $CONFIG{'output'}; foreach my $pattern ( qw! *.css *.jpg *.gif *.png *.js *.ico ! ) { foreach my $file ( glob( $input . "/" . $pattern ) ) { # # Get the name of the file. # if ( $file =~ /(.*)\/(.*)/ ) { $file = $2; } if ( $CONFIG{'force'} || ( ! -e "$output/$file" ) ) { $CONFIG{'verbose'} && print "Copying static file: $file\n"; copy( "$input/$file", "$output/$file" ); } } } } =begin doc Convert from markdown to HTML. =end doc =cut sub markdown2HTML { my( $text ) = (@_); # # Make sure we have the module installed. Use eval to # avoid making this mandatory. # my $test = "use Text::Markdown;"; # # Test loading the module. # eval( $test ); if ( $@ ) { print <<EOF; You have chosen to format your input text via Markdown, but the Perl module Text::Markdown is not installed. Aborting. EOF exit; } # # Convert. # $text = Text::Markdown::Markdown( $text ); return( $text ); } =begin doc Convert from textile to HTML. =end doc =cut sub textile2HTML { my( $text ) = (@_); # # Make sure we have the module installed. Use eval to # avoid making this mandatory. # my $test = "use Text::Textile;"; # # Test loading the module. # eval( $test ); if ( $@ ) { print <<EOF; You have chosen to format your input text via Textile, but the Perl module Text::Textile is not installed. Aborting. EOF exit; } # # Convert. # $text = Text::Textile::textile( $text ); return( $text ); } sub listThemes { my( $dir ) = ( @_ ); $CONFIG{'verbose'} && print "Listhing themes beneath : $dir\n"; foreach my $name ( sort( glob( $dir . "/*" ) ) ) { next unless( -d $name ); next if ( $name =~ /\/xml$/ ); if ( $name =~ /^(.*)\/([^\/\\]*)$/ ) { print $2 . "\n"; } } } =begin doc Create and configure a calendar for the index, if and only iff the HTML::CalendarMonthSimple module is installed. =end doc =cut sub createCalendar { # # Attempt to load the module. # my $test = "use HTML::CalendarMonthSimple;"; eval( $test ); # # If there was an error, or the calander is disabled then # return undef. # if ( ( $@ ) || ( $CONFIG{'no-calendar'} ) ) { print "Calander not available: $@"; return undef; } # # Continue # my $cal = new HTML::CalendarMonthSimple(); # configuration of the calendar $cal->border(0); $cal->weekstartsonmonday(1); $cal->showweekdayheaders(1); $cal->sunday('Sun'); $cal->saturday('Sat'); $cal->weekdays('Mo','Tue','We','Thu','Fr'); # get 4th element from localtime aka month in form of (0..11) my $curmonth = (localtime)[4] + 1; foreach my $f (%data) { my $h = $data{$f}; next if ( !$h ); my $entrydate = $h->{'date'}; if ( !$entrydate ) { my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, $atime,$mtime,$ctime,$blksize,$blocks) = stat($f); $entrydate = localtime( $ctime ); } my $date = time2str("%Y-%m-%d", str2time($entrydate)); my ($year,$month,$day) = split(/-/,$date); if ($month eq $curmonth) { $cal->setdatehref($day,fileToTitle($data{$f}->{'title'})); } } return ($cal); }