website/lib/DB.rakumod

216 lines
7.2 KiB
Raku
Raw Normal View History

2025-01-21 03:24:01 -05:00
use v6.e.PREVIEW;
2025-01-21 01:31:33 -05:00
#| Post database
unit module DB;
use Pandoc;
2025-01-21 03:24:01 -05:00
use JSON::Class:auth<zef:vrurg>;
2025-02-05 04:37:28 -05:00
use XML;
2025-02-06 18:39:20 -05:00
use XQ;
2025-01-21 01:31:33 -05:00
2025-01-22 20:13:45 -05:00
use DB::Post;
2025-02-09 01:50:37 -05:00
use DB::Series;
2025-01-22 20:13:45 -05:00
use DB::BlogMeta;
2025-01-22 20:49:27 -05:00
use DB::MarkdownPost;
2025-01-22 20:53:52 -05:00
use DB::IdrisPost;
use DB::PlaceholderPost;
2025-02-09 05:28:59 -05:00
use Render::Series;
2025-02-05 04:37:28 -05:00
use Atom;
2025-02-03 21:36:07 -05:00
use Config;
2025-01-22 05:00:58 -05:00
subset PostTypes where MarkdownPost:D | IdrisPost:D | PlaceholderPost:D;
2025-01-21 03:24:01 -05:00
#| The top level posts database
class PostDB {
#| The metadata for the blog
2025-01-21 22:08:10 -05:00
has BlogMeta:D $.meta is required;
#| A mapping from post ids to posts
# has %.posts is Posts;
has %.posts{Int} of PostTypes = %();
2025-02-09 01:50:37 -05:00
#| A mapping from series ids to series
has %.series{Int} of Series = %();
#| The post id to use for placeholder posts
has Int $.placeholder-id = 0;
2025-01-22 05:00:58 -05:00
2025-01-22 04:29:44 -05:00
#| Get the next unused post ID
method next-post-id(--> Int) {
2025-01-22 05:00:58 -05:00
if %!posts.elems > 0 {
2025-01-22 04:29:44 -05:00
%!posts.keys.max + 1
} else {
0
}
}
2025-02-09 02:12:14 -05:00
#| Get the next unused series ID
method next-series-id(--> Int) {
if %!series.elems > 0 {
%!series.keys.max + 1
} else {
0
}
}
2025-01-22 04:29:44 -05:00
#| Insert a new post to the DB, returning its id
2025-01-22 05:00:58 -05:00
method insert-post(PostTypes $post --> Int) {
2025-01-22 04:29:44 -05:00
my $id = self.next-post-id;
%!posts{$id} = $post;
$id
}
2025-02-09 02:12:14 -05:00
#| Insert a new series to the DB, returning its id
method insert-series(Series:D $series --> Int) {
my $id = self.next-series-id;
%!series{$id} = $series;
$id
}
#| Initialize a new database
method init(BlogMeta:D $meta --> PostDB:D) {
my %posts{Int} of PostTypes = %();
%posts{0} = PlaceholderPost.empty;
PostDB.new(
meta => $meta,
posts => %posts,
)
}
#| Write a database to a directory
method write(IO::Path:D $dir) {
my $posts-dir = $dir.add('posts/');
2025-02-09 01:50:37 -05:00
my $series-dir = $dir.add('series/');
# Make sure directory structrue exists
mkdir $dir unless $dir.e;
mkdir $posts-dir unless $posts-dir.e;
2025-02-09 01:50:37 -05:00
mkdir $series-dir unless $series-dir.e;
# Write out metadata
# TODO: Track changes and only write changed files
2025-02-07 02:02:40 -05:00
$dir.add('meta.json').spurt: $!meta.to-json(:sorted-keys);
# Write out posts (ids are the filename)
for %!posts.kv -> $key, $value {
2025-02-07 02:02:40 -05:00
$posts-dir.add("$key.json").spurt: $value.to-json(:sorted-keys);
}
2025-02-09 01:50:37 -05:00
# Write out the series
for %!series.kv -> $key, $value {
$series-dir.add("$key.json").spurt: $value.to-json(:sorted-keys);
}
}
2025-02-03 21:36:07 -05:00
#| Render the site to the provided output directory
method render(IO::Path:D $out-dir, Config:D :$config = Config.new) {
2025-02-06 20:32:38 -05:00
## Consistency checks
# Check to make sure all the slugs are unique
my @all-the-slugs = %!posts.values.map(*.all-slugs).flat;
die "Duplicate slug detected"
unless @all-the-slugs.unique.elems == @all-the-slugs.elems;
## Rendering
2025-02-03 21:36:07 -05:00
my $posts = $out-dir.add('posts/');
my $by-id = $posts.add('by-id/');
my $by-slug = $posts.add('by-slug/');
# Make sure the directory structure exists
mkdir $out-dir unless $out-dir.e;
mkdir $posts unless $posts.e;
mkdir $by-id unless $by-id.e;
mkdir $by-slug unless $by-slug.e;
# Render all the posts and make symlinks
for %!posts.kv -> $id, $post {
2025-02-09 02:43:22 -05:00
my $html = $config.generate-post: $id, $post, self;
2025-02-03 21:36:07 -05:00
my $id-path = $by-id.add: "$id.html";
$id-path.spurt: $html;
for $post.all-slugs -> $slug {
2025-02-04 01:20:57 -05:00
# remove the symlink if it already exists
2025-02-05 03:26:39 -05:00
my $slug-path = $by-slug.add: "$slug.html";
2025-02-04 01:20:57 -05:00
$slug-path.unlink if $slug-path.l;
$id-path.symlink: $slug-path;
2025-02-03 21:36:07 -05:00
}
}
# Render the index
2025-02-05 03:26:39 -05:00
$out-dir.add('index.html').spurt: $config.generate-index(self);
2025-02-05 03:28:49 -05:00
# Render the archive
$out-dir.add('archive.html').spurt: $config.generate-archive(self);
2025-02-05 04:37:28 -05:00
# Symlink the about article
2025-02-05 03:31:26 -05:00
my $about-path = $out-dir.add('about.html');
$about-path.unlink if $about-path.l;
$by-id.add("{$!meta.about-id}.html").symlink: $about-path;
2025-02-07 01:43:52 -05:00
# Generate the tags pages
my @tags = %!posts.values.map(*.tags).flat.unique.sort;
2025-02-07 01:43:52 -05:00
$out-dir.add('tags.html').spurt: $config.generate-tags-page(self, @tags);
2025-02-07 01:58:44 -05:00
my $tags-dir = $out-dir.add('tags/');
mkdir $tags-dir unless $tags-dir.e;
for @tags -> $tag {
$tags-dir.add("$tag.html").spurt:
$config.generate-tag-page(self, $tag);
}
2025-02-09 05:51:52 -05:00
# Generate the series pages
2025-02-09 05:28:59 -05:00
my $series-dir = $out-dir.add('series/');
mkdir $series-dir unless $series-dir.e;
for %!series.kv -> $key, $value {
$series-dir.add("$key.html").spurt:
series-page($key, self);
}
2025-02-09 05:51:52 -05:00
# Generate the main series page
$out-dir.add('series.html').spurt:
series-list-page self;
2025-02-05 04:37:28 -05:00
# Render the rss/atom feed
my $atom-path = $out-dir.add('atom.xml');
my $atom = posts-to-atom self;
2025-02-06 18:39:20 -05:00
$atom-path.spurt: format-xml(~$atom);
2025-02-06 18:45:15 -05:00
# Create the resources folder and copy over our style sheets
my $res-dir = $out-dir.add('resources/');
mkdir $res-dir unless $res-dir.e;
2025-02-10 18:30:33 -05:00
# symlink the resources directory to make "interactive" styling eaiser
# TODO: Directories support
%?RESOURCES<colors.css>.IO.symlink: $res-dir.add('colors.css');
%?RESOURCES<main.css>.IO.symlink: $res-dir.add('main.css');
%?RESOURCES<code.css>.IO.symlink: $res-dir.add('code.css');
%?RESOURCES<admonitions.css>.IO.symlink: $res-dir.add('admonitions.css');
2025-02-03 21:36:07 -05:00
}
2025-02-05 03:26:39 -05:00
#| Get a list of posts sorted by date
method sorted-posts() {
%!posts.sort(*.value.posted-at).reverse
}
}
2025-02-03 21:36:07 -05:00
#| Read the database out of a directory
sub read-db(IO::Path:D $dir --> PostDB:D) is export {
my $posts-dir = $dir.add('posts/');
2025-02-09 01:50:37 -05:00
my $series-dir = $dir.add('series/');
die "DB directory does not exist" unless $dir.e;
die "posts directory does not exist" unless $posts-dir.e;
# Read metadata
my $meta = BlogMeta.from-json: $dir.add('meta.json').slurp;
# Read posts
my %posts{Int} of PostTypes = %();
for dir $posts-dir -> $post {
my $id = $post.extension("").basename.Int;
# TODO: Dejankify this, maybe see if we can work with parsed, but
# unmarshalled json
given $post.slurp {
when /'"placeholder": true'/ {
%posts{$id} = PlaceholderPost.from-json: $_;
}
when /'"markdown": true'/ {
%posts{$id} = MarkdownPost.from-json: $_;
}
when /'"idris": true'/ {
%posts{$id} = IdrisPost.from-json: $_;
}
default {
die "Unsupported post type: $post";
}
}
}
2025-02-09 01:50:37 -05:00
# Read series
2025-02-09 02:12:14 -05:00
my %series{Int} of Series:D = %();
2025-02-09 01:50:37 -05:00
# For backwards compatability, the series directory is optional. It will be
# created on the next db operation, but we don't need it to read the site
if $series-dir.e {
for dir $series-dir -> $series {
my $id = $series.extension("").basename.Int;
%series{$id} = Series.from-json: $series.slurp;
}
}
# Build db structure
2025-02-09 01:50:37 -05:00
PostDB.new: meta => $meta, posts => %posts, series => %series
2025-01-21 03:24:01 -05:00
}