#!/usr/bin/env perl use Cwd qw(getcwd); use File::Basename qw(dirname basename); use File::Copy::Recursive qw(dircopy); use File::Find qw(finddepth); use File::Path qw(make_path); use File::Slurp; use POSIX qw(strftime); use Text::FrontMatter::YAML; use Text::Template; use Markdent::Simple::Fragment; use strict; use utf8; use constant { OUT_PATH => "/out", POSTS_PATH => "/posts", ASSETS_PATH => "/assets", PAGES_PATH => "/pages", TEMPLATES_PATH => "/templates", }; # globals my $cwd = getcwd; my $post_dir = $cwd . POSTS_PATH; my $pages_dir = $cwd . PAGES_PATH; my $tmpl_dir = $cwd . TEMPLATES_PATH; my $post_tmpl = Text::Template->new(SOURCE => "$tmpl_dir/post.html.tmpl"); my $assets_path = $cwd . ASSETS_PATH; my $out_path = $cwd . OUT_PATH; my $postout_path = $out_path . POSTS_PATH; my $return_link = qq{}; # used across a couple functions my $parser = Markdent::Simple::Fragment->new; sub make_fragment { my $body = shift; $parser->markdown_to_html( dialect => 'GitHub', markdown => $body, ); } sub strip_id { my $id = shift; $id =~ s/[ \t]/-/g; $id =~ s/[^A-Za-z0-9_-]//g; return $id; } sub do_footnotes { my $text = shift; my %footnotes; my @used; my $footnote_counter = 0; # First grab the definitions while ($text =~ s{ \n\[\^([^\n]+?)\]\:[ \t]* \n? (.*?)(?:\n{1,2} # end at new paragraph (?=\n[ ]{0,4}\S)|\Z) # Lookahead for non-space at line-start, or end of doc }{\n}sx) { my $id = strip_id($1); $footnotes{$1} = make_fragment(qq{$2 [$return_link](#fnref:$id)}); } # then replace the inline footnotes $text =~ s{ \[\^(.*?)\] }{ my $result = ''; my $id = strip_id($1); if (defined $footnotes{$id}) { $footnote_counter++; $result = qq{[$footnote_counter]}; push(@used, $id); } $result; }xsge; my $fn_block = qq{

    }; # finally, append the footnotes foreach my $id (@used) { my $footnote = $footnotes{$id}; $fn_block .= qq{
  1. $footnote
  2. }; } $fn_block .= qq{
}; return ($text, $fn_block); } # Converts a post file to a metadata hash. # # Takes one argument, the path to the post file. # # Returns a hash with the relevant metadata (body text is stored as # $metadata{"content"}, desired filename is stored as # $metadata{"fname"}). sub post_to_meta { defined(my $fname = shift) or warn "No filename argument!"; my $fdata = read_file($fname); my $mdfm = Text::FrontMatter::YAML->new( document_string => $fdata ); my $metadata = $mdfm->frontmatter_hashref; chomp(my $body = $mdfm->data_text); if ($body ne "") { # for very funny bits, i assure you $body =~ s{\[\^citation\sneeded\]}{ "[citation needed]" }egm; my $tag_attrs = qr{ (?: # Match one attr name/value pair \s+ # There needs to be at least some whitespace # before each attribute name. [\w.:_-]+ # Attribute name \s*=\s* (?: ".+?" # "Attribute value" | '.+?' # 'Attribute value' | [^\s]+? # AttributeValue (HTML5) ) )* # Zero or more }x; my $markdown_attr = qr{ \s* markdown \s* = \s* (['"]) (.*?) \1 }xs; my $empty_tag = qr{< \w+ $tag_attrs \s* />}oxms; use Text::Balanced qw(gen_extract_tagged); my $extract_block = gen_extract_tagged(qr{< div $tag_attrs \s* >}oxms, undef, undef, { ignore => [$empty_tag] }); $body =~ s{ ( ) }{ my $return = $1; my ($tag, $remainder, $prefix, $opening_tag, $text_in_tag, $closing_tag) = $extract_block->($return); if ($tag) { if ($opening_tag =~ s/$markdown_attr//i) { my $markdown = $2; if ($markdown =~ /^(1|on|yes)$/) { $tag = $prefix . $opening_tag . "\n" . make_fragment($text_in_tag) . "\n" . $closing_tag; } else { $tag = $prefix . $opening_tag . $text_in_tag . $closing_tag; } $return = $tag; } } $return; }egmxs; my $fn_body; ($body, $fn_body) = do_footnotes($body); $fn_body = "" unless defined $fn_body; $metadata->{content} = make_fragment($body) . $fn_body; } # HACK: Stuffing the basename in the metadata because I don't want # to deal with hashes of hashes $metadata->{fname} = basename($fname) unless exists($metadata->{"fname"}); $metadata; } # Gets an array of all posts for a directory. # # Takes one argument, the path to the posts directory. # # Returns an array of metadata hashes (see post_to_meta above for # more on what those hashes look like). sub all_posts_for_dir { defined(my $postdir = shift) or warn "No directory argument!"; my @posts; opendir(PD, $postdir) or die $!; while (my $fname = readdir(PD)) { next if ($fname =~ /^\.+$/); my $post = &post_to_meta("$postdir/$fname"); push @posts, $post; } closedir(PD); sort { $b->{created} cmp $a->{created} } @posts; } sub all_pages_for_dir { defined(my $pagedir = shift) or warn "No directory argument!"; my @pages; finddepth(sub { return if ($File::Find::name =~ /^\.+$/); my %page; $_ = $File::Find::name; if (/^$pagedir\/(.+).tmpl$/) { $page{fname} = $1; $page{tmpl} = Text::Template->new(SOURCE => $File::Find::name); push @pages, \%page; } }, $pagedir); @pages; } sub mkpath { make_path( shift, { error => \my $path_err } ); if ($path_err && @$path_err) { print "Errors occurred while making posts directory!\n"; foreach my $err (@$path_err) { my ($path, $msg) = %$err; if ($path eq '') { print " $msg\n"; } else { print " $path: $msg\n"; } } # effectively die return 0; } else { return 1; } } sub include_tmpl { my $tmpl_name = shift; # TODO: do we want to cache snippet-type templates like this? my $tmpl = Text::Template->new(SOURCE => "$tmpl_dir/$tmpl_name.tmpl"); if (defined $tmpl) { my $props = shift; $tmpl->fill_in(HASH => { props => \$props }); } else { "error: $tmpl_dir/$tmpl_name.tmpl is missing" } } ## End subroutines # subroutines used in templates my %utils = ( strftime => \&strftime, include_tmpl => \&include_tmpl, ); # make posts dir or die mkpath($postout_path) or die "Unable to create directory."; my @posts = all_posts_for_dir $post_dir; print "Generating blog posts...\n"; foreach my $post (@posts) { if (!exists $post->{content}) { print " $post->{fname} has no content, not making a page...\n"; next; } print " Processing $post->{fname}...\n"; my %post_hash = %utils; $post_hash{post} = \$post; my $post_content = $post_tmpl->fill_in(HASH => \%post_hash); if (defined($post_content)) { open(POSTOUT, '>', "$postout_path/$post->{fname}.html") or die("Unable to write $post->{fname}.html: $!"); print POSTOUT $post_content; close(POSTOUT); } else { die "Failed to process $post->{fname}: $Text::Template::ERROR"; } } print "Generating pages...\n"; my @pages = all_pages_for_dir $pages_dir; foreach my $pg (@pages) { mkpath(dirname("$out_path/" . $pg->{fname})) or die "Unable to create directory."; print " Processing $pg->{fname}...\n"; my %page_hash = %utils; $page_hash{posts} = \@posts; my $page_content = $pg->{tmpl}->fill_in(HASH => \%page_hash); if (defined($page_content)) { open(PGOUT, '>', "$out_path/$pg->{fname}") or die("Unable to write $pg->{fname}: $!"); print PGOUT $page_content; close(PGOUT); } else { die "Failed to process $pg->{fname}: $Text::Template::ERROR"; } } print "Copying static assets...\n"; local $File::Copy::Recursive::RMTrgDir = 2; dircopy($assets_path, "$out_path/" . ASSETS_PATH) or die "Unable to copy assets: $!"; print "Done! Artifacts have been stored in $out_path.\n";