Mercurial > hg > chronicle
view bin/chronicle @ 46:081d06e16a36
2007-10-10 20:09:24 by steve
Added '--force' argument.
author | steve |
---|---|
date | Wed, 10 Oct 2007 20:09:24 +0000 |
parents | a9f8a82045f8 |
children | 29464ede63dd |
line wrap: on
line source
#!/usr/bin/perl -w =head1 NAME chronicle - A blog compiler. =cut =head1 SYNOPSIS Path Options: --input Specify the input directory to use. --output Specify the directory to write output to. --templates Specify the path to the theme templates. --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-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. The header must contain a 'Title:' line. The 'Date:' line is optional, as is the 'Tags:' line. 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. Simply add 'format = [ markdown | textile | html]' to the configuration file to specify which you wish to use. (Or use the --format) command line argument. If you're missing the required Perl module to support your chosen input format you will be told this. =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 /etc/chroniclerc or the per-user ~/.chroniclerc file. These files contain lines of the form: =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 the Memcached deaemon, if installed and available upon the local machine. 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 AUTHOR Steve -- http://www.steve.org.uk/ $Id: chronicle,v 1.20 2007-10-10 20:09:24 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'; # # 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(); # # 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 each 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 archives. # 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 directory. # $CONFIG{'template'} = "./themes/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. TODO: Document these in the POD. =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{'template'}, "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.20 $'; 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 $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; # strip leading and trailing space. $title =~ s/^\s+// if ( length $title ); $title =~ s/\s+$// if ( length $title ); } elsif (( $line =~ /^date:(.*)/i ) && !length($date) ) { $date = $1; # strip leading and trailing space. $date =~ s/^\s+// if ( $date ); $date =~ s/\s+$// if ( $date ); } elsif ( $line =~ /^status:(.*)/i ) { 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 ) ) { my $count = $unique{$key}; 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 = time2str("%Y-%m", str2time($h->{'date'})) || undef; next if ( !$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; my @names = qw( January February March April May June July August September October November December ); 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 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 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" unless defined($year1); die "Couldn't find second year" 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 ); # # 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. 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]{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 = time2str("%Y-%m", str2time($h->{'date'})) || undef; 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 ); $template->param( year => $year, month => $month ); # # 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 ); 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; } # # 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'} || ""; $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"; } } # # I # 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> ) { # # Append any tags. # if ( $line =~ /^tags: (.*)/i ) { $tags .= $1; } elsif (( $line =~ /^title: (.*)/i ) && !length($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) ) { $date = $1; # strip leading and trailing space. $date =~ s/^\s+// if ( length $date ); $date =~ s/\s+$// if ( length $date ); } elsif (( $line =~ /^status:(.*)/ ) && !length ( $status ) ) { $status = $1; } else { $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) ); $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 ); # # 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 ) = (@_); # # 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 ); 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 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{'template'}; 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 ); }