commit f2f978eaabff9765822ec6182279fa7b664efb6d Author: snow flurry Date: Sun Feb 6 22:47:31 2022 -0800 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..90e1950 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# dg-x + +Static site build system for [datagirl.xyz](https://datagirl.xyz). + +## Dependencies + +build.pl uses the following modules: + +* Cwd +* File::Copy::Recursive +* File::Path +* Markdent +* Text::Template + +ptouch.pl uses the following modules: + +* Tie::File +* Getopt::Std + +## How to use + +You'll need to create the following directories: + +* posts/ +* pages/ - Templated pages +* assets/ - Static assets +* templates/ - Templates you can call with `include_tmpl` + +To make a new post, create a file with the following format in the +posts/ folder: + +``` +title=Your blog title +desc=[Not yet implemented] Description for the RSS feed +--- +This is some content. + +## About this content +Markdown is supported. +``` + +There is no set schema for tags, so you can put whatever you want as +a tag. However, it may not be used by the build script. + +Once you're happy with your post, use the ptouch script to set the +created tag: + +``` +$ ./scripts/ptouch.pl -n posts/your_new_post +``` + +And then compile the site: + +``` +$ ./scripts/build.pl +``` + +The site can then be pushed with rsync, sftp, netcat, etc. diff --git a/scripts/build.pl b/scripts/build.pl new file mode 100755 index 0000000..a1c4ead --- /dev/null +++ b/scripts/build.pl @@ -0,0 +1,190 @@ +#!/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 Markdent::Simple::Fragment; +use Text::Template; + +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; + +# 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 %metadata; + + open(MDIN, '<', $fname) or die "Unable to open $fname: " . $!; + while () { + chomp; + if (/^(.+)?=(.*)$/) { + $metadata{$1} = $2; + } elsif (/^---$/) { + last; + } else { + warn basename($fname) . ":" . $. . ": malformed line; ignored" + } + } + + my $body = do { local $/; }; + close(MDIN); + + # 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"}); + if ($body ne "") { + my $parser = Markdent::Simple::Fragment->new; + + $metadata{content} = $parser->markdown_to_html( + dialects => 'GitHub', + markdown => $body + ); + } + + %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 + + +# 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_content = $post_tmpl->fill_in(HASH => { post => \$post, include_tmpl => \&include_tmpl }); + 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_content = $pg->{tmpl}->fill_in(HASH => { + posts => \@posts, + include_tmpl => \&include_tmpl, + }); + 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"; diff --git a/scripts/ptouch.pl b/scripts/ptouch.pl new file mode 100755 index 0000000..4d9236a --- /dev/null +++ b/scripts/ptouch.pl @@ -0,0 +1,47 @@ +#!/usr/bin/env perl + +use strict; +use utf8; +use Getopt::Std; +use POSIX qw(strftime); +use Tie::File; + +$::VERSION = "1.0.0"; + +getopts("ne", \my %opts); + +my $metavar; +if (defined $opts{n}) { + # "new" post + $metavar = "created"; +} elsif (defined $opts{e}) { + # "edited" post + $metavar = "modified"; +} else { + die "Neither -n[ew] nor -e[dited] are defined!"; +} + +my $pretty_date = strftime "%Y-%m-%d %H:%M", localtime; + +my $fname = shift @ARGV; +tie my @fharr, 'Tie::File', $fname or die $!; + +my $done = 0; +for (@fharr) { + if (s/^$metavar=.+/$metavar=$pretty_date/) { + print "$fname:$.: Overwriting existing \`$metavar\'\n"; + $done++; + } + if (s/^(---)$/$metavar=$pretty_date\n$1/) { + print "$fname:$.: Inserting new \`$metavar\'\n"; + $done++; + } + last if ($done); +} + +if (!$done) { + print "$fname: No content and no \`$metavar\', inserting at end\n"; + push @fharr, "$metavar=$pretty_date"; +} + +untie @fharr;