--- /dev/null
+#!/usr/bin/perl
+
+# Written by Nathan Wagner <nw@hydaspes.if.org>
+# Copyright disclaimed. This file is in the public domain.
+
+use strict;
+use warnings;
+
+use Template;
+use Text::Markup;
+use Text::MultiMarkdown;
+use File::Find;
+use POSIX qw(strftime);
+use Digest::SHA qw(sha256_hex);
+use File::Path qw(make_path);
+
+use CGI;
+use CGI::Carp qw(fatalsToBrowser);
+
+# TODO allow per category configuration. Can just 'do .bluefin' if it exists
+# for the category
+
+my $blogtitle = `pwd`;
+chomp($blogtitle);
+$blogtitle =~ s|.+/||g;
+$blogtitle =~ s/\.[^\.]+$//;
+$blogtitle =~ s/_/ /g;
+$blogtitle = join(' ', map { ucfirst } split(/\s+/, $blogtitle));
+
+$blogtitle = 'Bad Data';
+
+my $postdir = './posts'; # where should I look for posts
+
+my $commentdir = './comments'; # where to put or look for comments
+my $commenttime = ''; # false is all allowed, positive numeric
+# is seconds after article post allowed. negative numeric is seconds
+# after last comment allowed
+my $commentsneeddir = 0;
+# true if comment directory exists, false will create directory, if needed
+# can disable comments by creating a regular file where the comment directory
+# would be
+my $maxcommentlength = 2048; # if non-zero, maximum length in bytes of
+# a comment
+
+my $pagelimit = 10; # number of articles maximum per 'page'
+my $allowqstringpagelimit = 0; # allow query string to specify pagelimit
+# positive number is most allowed, higher will be truncated to that limit
+
+# urls like /blog/[category]/archive/[pagenum]/ will be special cased for
+# showing older posts, pagelimit is used for the breaks
+my $archivename = 'archive';
+
+my $ignorefuture = 1; # ignore posts with timestamps in the future
+
+my $sortposts = sub {
+ my $cmp;
+ $cmp = $b->{'timestamp'} <=> $a->{timestamp};
+ if (!$cmp) {
+ $cmp = $a->{'name'} cmp $b->{name};
+ }
+ return $cmp;
+};
+
+my $dateformat = '%B %d, %Y';
+my $timeformat = '%H:%m:%s';
+my $posttimeformat = '%c';
+
+# get any local config
+-f 'bluefin.cfg' && do 'bluefin.cfg';
+
+my $q = new CGI;
+
+my $get = $q->path_info;
+
+my $tt = Template->new({
+ INCLUDE_PATH => 'templates'
+ }
+);
+
+my $target = "$postdir/$get";
+my $pagenumber = 0;
+my @files;
+
+if ($q->request_method() eq 'POST') {
+ # handle a comment
+ # need form fields: article, comment
+ # remaining form fields put in metatags.
+ if (-f $target) {
+ my $text = $q->param('comment');
+ my $path = $q->path_info();
+ my $author = $q->param('name');
+ if ($maxcommentlength && length($text) > $maxcommentlength) {
+ print $q->header(-status => 413);
+ # TODO if -f 413.tmpl
+ exit 0;
+ }
+ if (!-f $target) {
+ print $q->header(-status => 403);
+ exit 0;
+ }
+
+ my $article = $target;
+ my $cd = "$commentdir/$path";
+ make_path($cd);
+ my $comment;
+ $comment .= "Article: $path\n";
+ $comment .= "Name: $author\n";
+ $comment .= "\n";
+ $comment .= $q->param('comment');
+ $comment .= "\n";
+ my $hash = sha256_hex($comment);
+ open(my $fh, '>', "$cd/.$hash");
+ print $fh $comment;
+ close($fh);
+ rename("$cd/.$hash", "$cd/$hash");
+ print $q->redirect($q->url(-path_info => 1));
+ } elsif (-d $target) {
+ # trying to post to a directory
+ print $q->header(-status => 403);
+ exit 0;
+ } else {
+ # trying to post to a non-existent file
+ print $q->header(-status => 404);
+ exit 0;
+
+ }
+}
+
+#my $findposts = makefinder($target);
+
+if (-d $target or -f $target) {
+ print $q->header;
+ # a category. read in the files
+ my $page = {}; # template info
+ my @posts = (); # processed posts
+ @files = (); # raw files
+ find(\&findposts, $target); # find the post files
+ @files = sort $sortposts @files; # sort them by criteria
+ #@files = map { $_->{path} =~ s|^$target|| } @files;
+ if ($ignorefuture) {
+ my $ts = time;
+ @files = grep { $_->{timestamp} <= $ts } @files;
+ }
+
+ # process the posts we're actually going to pass to the template
+ my $start = $pagenumber * $pagelimit;
+ my $end = $start + $pagelimit;
+ my $postcount = @files;
+ if ($end > @files) {
+ $end = @files;
+ }
+ if (@files >= $start) {
+ my $url = $q->url(-relative => 1, -path_info => 1);
+ $url = $q->url();
+ @files = splice(@files, $start, $end - $start);
+ @posts = map { readpost($_->{path}, $_, $url, 1) } @files;
+ }
+
+ $page->{url} = $q->url(-rewrite => 0, -path => 0);
+ $page->{totalposts} = $postcount;
+ $page->{postpages} = int($postcount/$pagelimit) + $postcount % $pagelimit == 0 ? 0 : 1;
+ $page->{blogtitle} = $blogtitle;
+ $page->{posts} = \@posts;
+ $tt->process('category.tmpl', $page) or die $tt->error;
+} elsif (-f $target) {
+ my $page = {};
+ my $post = readpost($target);
+ $page->{blogtitle} = $blogtitle;
+ $page->{posts} = [ $post ];
+
+ # a single article
+ $tt->process('article.tmpl', $page);
+}
+exit 0;
+
+sub readfile {
+ my ($path) = @_;
+ if (!-f $path) {
+ return ();
+ }
+ my $page = {};
+ $page->{'timestamp'} = (stat($path))[9];
+ my @timeinfo = localtime($page->{'timestamp'});
+ $page->{'timeinfo'} = \@timeinfo;
+ $page->{'postdate'} = strftime($dateformat, @timeinfo);
+ $page->{'posttime'} = strftime($timeformat, @timeinfo);
+ $page->{'date'} = strftime($posttimeformat, @timeinfo);
+
+ my ($fh);
+ open $fh, $path;
+ local $/ = "";
+ my @lines = <$fh>;
+ if ($lines[0] and $lines[0] =~ m/^.+:\s*.+/m) {
+ my %hdrinfo = ($lines[0] =~ m/^(.+):\s*(.+)/mg);
+ $page->{'meta'} = \%hdrinfo;
+ $page->{'title'} = $page->{'meta'}{'Title'};
+
+ shift @lines;
+ }
+
+ my $lines = join('', @lines);
+ my $md = Text::MultiMarkdown->new();
+ my $html = $md->markdown($lines);
+ close $fh;
+ $page->{'content'} = $html;
+
+ return $page;
+}
+
+sub find_files {
+ my @paths = @_;
+ my @files = ();
+ my $asof = time;
+
+ my $find = sub { push @files, $File::Find::name if ( (-f $_) && (!m/^\./) )};
+ my $get = sub { wantarray ? @files : [ @files ] };
+ find($find, @paths);
+ return $get->();
+}
+
+sub readpost {
+ my ($path, $xinfo, $url, $comments) = @_;
+ my %page;
+
+ my $page = readfile($path);
+ return () unless $page;
+
+ $page->{'url'} = $url . '/' . $xinfo->{lpath};
+
+ if (!$page->{'title'}) {
+ my $t = $xinfo->{name};
+ $t =~ s/\.[^\.]+$//;
+ $t =~ s/_/ /g;
+ $t = join(' ', map { ucfirst } split(/\s+/, $t));
+ $page->{'title'} = $t;
+ }
+
+ my $cd = "$commentdir/" . $xinfo->{lpath};
+ if ($comments) {
+ if (-d $cd) {
+ my @comments = map { readfile($_) } find_files($cd);
+ @comments = sort {
+ my $cmp;
+ $cmp = $a->{'timestamp'} <=> $b->{timestamp};
+ if (!$cmp) {
+ $cmp = $a->{'name'} <=> $b->{name};
+ }
+ return $cmp;
+ } @comments;
+
+ $page->{comments} = \@comments;
+ } else {
+ $page->{comments} = [];
+ }
+ }
+
+ $page->{'xinfo'} = $xinfo;
+
+ my @catpath = split(m|/|, $xinfo->{lpath});
+ pop @catpath;
+ $page->{'catpath'} = \@catpath;
+ my $caturl = $q->url(-rewrite => 0) . '/../';
+ my @caturls = ();
+ foreach my $cat (@catpath) {
+ $caturl .= "$cat/";
+ push @caturls, { name => $cat, url => $caturl };
+ }
+
+ $page->{'categories'} = \@caturls;
+
+ return $page;
+}
+
+sub findposts {
+ my %fileinfo;
+ return unless -f;
+ return if m/^\./; # skip dotfiles
+ $fileinfo{'stat'} = [ stat $_ ];
+ $fileinfo{'timestamp'} = (stat _)[9];
+ $fileinfo{'name'} = $_;
+ $fileinfo{'dir'} = $File::Find::dir;
+ $fileinfo{'path'} = $File::Find::name;
+ $fileinfo{'lpath'} = $File::Find::name;
+ $fileinfo{'lpath'} =~ s|^$postdir/*||;
+ push @files, \%fileinfo;
+}
+
+__END__
+my $parser = Text::Markup->new(
+ default_format => 'multimarkdown',
+ default_encoding => 'UTF-8',
+);
+
+#my $html = $parser->parse(file => $file);
+
+print $html;