Compare commits
18 commits
b095fe356b
...
1d91c51084
Author | SHA1 | Date | |
---|---|---|---|
1d91c51084 | |||
bdceab7652 | |||
109165b80e | |||
baf8d6556b | |||
7cf4827d0c | |||
471603ae02 | |||
35bdb1c86c | |||
8ce16daa58 | |||
5c569780dc | |||
4793d660e2 | |||
d170a2fccf | |||
0571a16dab | |||
49502df416 | |||
73de17fc2c | |||
7a5ef790f3 | |||
e05269b9a3 | |||
b55a4f2b41 | |||
bbf7b4f6c8 |
19 changed files with 732 additions and 58 deletions
234
blog
234
blog
|
@ -3,9 +3,12 @@ use v6.e.PREVIEW;
|
|||
|
||||
use DB;
|
||||
use DB::BlogMeta;
|
||||
use DB::Series;
|
||||
use DB::MarkdownPost;
|
||||
use DB::IdrisPost;
|
||||
|
||||
use Pretty::Table;
|
||||
|
||||
my %*SUB-MAIN-OPTS =
|
||||
:named-anywhere,
|
||||
:bundling,
|
||||
|
@ -70,6 +73,7 @@ multi MAIN(
|
|||
|
||||
#| Create a new markdown post
|
||||
multi MAIN(
|
||||
"post",
|
||||
"new",
|
||||
"markdown",
|
||||
#| The path to the markdown file
|
||||
|
@ -96,6 +100,7 @@ multi MAIN(
|
|||
|
||||
#| Create a new idris post
|
||||
multi MAIN(
|
||||
"post",
|
||||
"new",
|
||||
"idris",
|
||||
#| The path to the idris file
|
||||
|
@ -125,7 +130,8 @@ multi MAIN(
|
|||
|
||||
#| Update the last editied time on a post
|
||||
multi MAIN(
|
||||
"touch",
|
||||
"post",
|
||||
"edit",
|
||||
#| The post id to touch
|
||||
Int:D $id,
|
||||
#| The path of the database file
|
||||
|
@ -150,3 +156,229 @@ multi MAIN(
|
|||
my $db = read-db $db-dir;
|
||||
$db.render: $site-dir;
|
||||
}
|
||||
|
||||
#| Provide a table of posts, in newest to oldest order
|
||||
multi MAIN(
|
||||
"post",
|
||||
"list",
|
||||
#| The path of the database directory
|
||||
IO::Path(Str) :$db-dir = $default-db-dir,
|
||||
#| The number of posts to show on a single page
|
||||
Int :$per-page = 10;
|
||||
#| The page number to show
|
||||
Int :$page = 1;
|
||||
) {
|
||||
my $db = read-db $db-dir;
|
||||
my @pages =
|
||||
$db.sorted-posts.rotor($per-page, :partial);
|
||||
my @page = @pages[$page - 1].flat;
|
||||
my $table = Pretty::Table.new:
|
||||
title => "Posts (page $page/{@pages.elems})",
|
||||
field-names => ["ID", "Title", "Type"];
|
||||
for @page -> $pair {
|
||||
my $id = $pair.key;
|
||||
my $post = $pair.value;
|
||||
# TODO: Terminal aware truncation
|
||||
my $title = do if $post.title.chars > 50 {
|
||||
"{$post.title.substr(0, 50)}..."
|
||||
} else {
|
||||
$post.title
|
||||
};
|
||||
my $type = do given $post {
|
||||
when MarkdownPost {
|
||||
"md"
|
||||
}
|
||||
when IdrisPost {
|
||||
"idr"
|
||||
}
|
||||
default {
|
||||
""
|
||||
}
|
||||
}
|
||||
$table.add-row: [$id, $title, $type];
|
||||
}
|
||||
say $table;
|
||||
}
|
||||
|
||||
#| Display information about a post
|
||||
multi MAIN(
|
||||
"post",
|
||||
"info",
|
||||
#| The id of the post to display information for
|
||||
Int $id,
|
||||
#| The path of the database directory
|
||||
IO::Path(Str) :$db-dir = $default-db-dir,
|
||||
#| Display all of the information and not just the primaries
|
||||
Bool :$full,
|
||||
) {
|
||||
my $db = read-db $db-dir;
|
||||
my $post = $db.posts{$id.Int};
|
||||
given $post {
|
||||
say 'Title: ', .title;
|
||||
say 'Type: ', .WHAT.^name;
|
||||
say 'Source: ', .source.relative;
|
||||
if .hidden {
|
||||
say "Hidden";
|
||||
}
|
||||
if .all-slugs {
|
||||
if $full {
|
||||
say 'Slugs: ';
|
||||
for .all-slugs -> $slug {
|
||||
say ' * ', $slug;
|
||||
}
|
||||
} else {
|
||||
say 'Primary Slug: ', .all-slugs[*-1];
|
||||
}
|
||||
} else {
|
||||
say 'Slugs: []';
|
||||
}
|
||||
if .tags {
|
||||
say 'Tags:';
|
||||
for .tags -> $tag {
|
||||
say ' * ', $tag;
|
||||
}
|
||||
}
|
||||
given .posted-at {
|
||||
say 'Posted At: ', .Date.Str, ' ', .hh-mm-ss;
|
||||
}
|
||||
if .edited-at {
|
||||
if $full {
|
||||
say 'Edits: ';
|
||||
for .edited-at.sort.reverse {
|
||||
say ' * ', .Date.Str, ' ', .hh-mm-ss;
|
||||
}
|
||||
} else {
|
||||
given .edited-at.sort[*-1] {
|
||||
say 'Last Edited At: ', .Date.Str, ' ', .hh-mm-ss;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#| Add or remove a tag to a post
|
||||
multi MAIN(
|
||||
"post",
|
||||
"tag",
|
||||
#| The id of the post to display information for
|
||||
Int $id,
|
||||
#| The tag to add/remove
|
||||
Str $tag,
|
||||
#| remove the tag instead of adding it
|
||||
Bool :$remove,
|
||||
#| The path of the database directory
|
||||
IO::Path(Str) :$db-dir = $default-db-dir,
|
||||
) {
|
||||
my $db = read-db $db-dir;
|
||||
my $post = $db.posts{$id.Int};
|
||||
if $remove {
|
||||
die "Post did not have requested tag"
|
||||
unless $tag ~~ any($post.tags);
|
||||
my $index = $post.tags.first: $tag;
|
||||
$post.tags.=grep(* ne $tag);
|
||||
} else {
|
||||
die "Post already had requested tag"
|
||||
if $tag ~~ any($post.tags);
|
||||
$post.tags.push: $tag;
|
||||
}
|
||||
$db.write: $db-dir;
|
||||
}
|
||||
|
||||
#| Create a new series
|
||||
multi MAIN(
|
||||
"series",
|
||||
"new",
|
||||
#| The path of the database file
|
||||
IO::Path(Str) :$db-dir = $default-db-dir,
|
||||
) {
|
||||
my $db = read-db $db-dir;
|
||||
say 'Series Title: ';
|
||||
my $title = get;
|
||||
say 'Series Description: ';
|
||||
my $desc = get;
|
||||
|
||||
my $series = Series.new:
|
||||
title => $title, desc => $desc;
|
||||
my $id = $db.insert-series: $series;
|
||||
say 'Series inserted with id ', $id;
|
||||
|
||||
$db.write: $db-dir;
|
||||
}
|
||||
|
||||
|
||||
#| Provide a table of series
|
||||
multi MAIN(
|
||||
"series",
|
||||
"list",
|
||||
#| The path of the database directory
|
||||
IO::Path(Str) :$db-dir = $default-db-dir,
|
||||
#| The number of items to show on a single page
|
||||
Int :$per-page = 10;
|
||||
#| The page number to show
|
||||
Int :$page = 1;
|
||||
) {
|
||||
my $db = read-db $db-dir;
|
||||
my @pages =
|
||||
$db.series.sort(*.key).rotor($per-page, :partial);
|
||||
my @page = @pages[$page - 1].flat;
|
||||
my $table = Pretty::Table.new:
|
||||
title => "Series (page $page/{@pages.elems})",
|
||||
field-names => ["ID", "Title", "Desc"];
|
||||
for @page -> $pair {
|
||||
my $id = $pair.key;
|
||||
my $series = $pair.value;
|
||||
# TODO: Terminal aware truncation
|
||||
my $title = do if $series.title.chars > 40 {
|
||||
"{$series.title.substr(0, 50)}..."
|
||||
} else {
|
||||
$series.title
|
||||
};
|
||||
my $desc = do if $series.desc.chars > 40 {
|
||||
"{$series.desc.substr(0, 50)}..."
|
||||
} else {
|
||||
$series.desc
|
||||
};
|
||||
$table.add-row: [$id, $title, $desc];
|
||||
}
|
||||
say $table;
|
||||
}
|
||||
|
||||
#| Display the contents of a series
|
||||
multi MAIN(
|
||||
"series",
|
||||
"info",
|
||||
#| The id of the series to display
|
||||
Int $id,
|
||||
#| The path of the database directory
|
||||
IO::Path(Str) :$db-dir = $default-db-dir,
|
||||
) {
|
||||
my $db = read-db $db-dir;
|
||||
my $series = $db.series{$id.Int};
|
||||
say 'Title: ', $series.title;
|
||||
say 'Description:';
|
||||
for $series.desc.lines -> $line {
|
||||
say ' ', $line;
|
||||
}
|
||||
say 'Posts:';
|
||||
for $series.post-ids -> $post-id {
|
||||
my $post = $db.posts{$post-id};
|
||||
say ' * ', $post-id, ': ', $post.title;
|
||||
}
|
||||
}
|
||||
|
||||
#| Add a post to a series
|
||||
multi MAIN(
|
||||
"series",
|
||||
"add",
|
||||
#| The id of the series to add to
|
||||
Int $series-id,
|
||||
#| The id of the post to add
|
||||
Int $post-id,
|
||||
#| The path of the database directory
|
||||
IO::Path(Str) :$db-dir = $default-db-dir,
|
||||
) {
|
||||
my $db = read-db $db-dir;
|
||||
my $series = $db.series{$series-id.Int};
|
||||
$series.post-ids.push: $post-id.Int;
|
||||
$db.write: $db-dir;
|
||||
}
|
||||
|
|
14
db/posts/5.json
Normal file
14
db/posts/5.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"edited-at": [
|
||||
],
|
||||
"hidden": false,
|
||||
"idris": true,
|
||||
"ipkg": "/home/nathan/Projects/Blog/projects/Idris/Idris.ipkg",
|
||||
"posted-at": "2025-02-09T06:23:37.499533-05:00",
|
||||
"slugs": [
|
||||
],
|
||||
"source": "/home/nathan/Projects/Blog/projects/Idris/src/LessMacrosMoreTypes/Printf.md",
|
||||
"tags": [
|
||||
"idris"
|
||||
]
|
||||
}
|
7
db/series/0.json
Normal file
7
db/series/0.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"desc": "Macros are annoying, but an unfortunate fact of life in many programming languages. Especially in languages with nominally \"strong\" type systems, like Rust, macros are quite frequently needed to work around the type system to avoid needless repetition when consuming an API, generate formulaic boilerplate that only exists to please the type system, or work around the lack of variadic functions for things like printf. Lets explore the ways we can use dependently typed constructs to eliminate the need for such macros.",
|
||||
"post-ids": [
|
||||
5
|
||||
],
|
||||
"title": "Less Macros, More Types"
|
||||
}
|
|
@ -39,6 +39,9 @@ sub post-to-item(BlogMeta:D $meta, Int:D $id, Post:D $post --> XML::Element) {
|
|||
$xml.append: $author;
|
||||
$xml.append:
|
||||
XML::Element.new(:name<link>, :attribs({:href($link), :rel<alternate>}));
|
||||
# TODO: Actually embed the content
|
||||
$xml.append:
|
||||
XML::Element.new(:name<content>, :attribs({:src($link), :type<html>}));
|
||||
$xml.append:
|
||||
XML::Element.new(:name<summary>, :nodes([$desc])) if $desc;
|
||||
if $post.tags {
|
||||
|
|
|
@ -10,20 +10,21 @@ use DB::Post;
|
|||
|
||||
unit class Config;
|
||||
|
||||
# TODO: Support GFM admonitions
|
||||
method generate-post(Post:D $post, BlogMeta:D $meta) {
|
||||
method generate-post(Int:D $id, Post:D $post, $db) {
|
||||
my $meta = $db.meta;
|
||||
my $content = $post.render-html;
|
||||
my $head = generate-head($meta, $post.title, $post.description);
|
||||
my $body =
|
||||
body [
|
||||
site-header $meta;
|
||||
article :class<post>, [
|
||||
post-header $post;
|
||||
post-header $id, $post, $db;
|
||||
div :class<post-body>, [
|
||||
$content;
|
||||
]
|
||||
]
|
||||
];
|
||||
# TODO: Setup Comments
|
||||
# TODO: Setup footer
|
||||
# my $footer;
|
||||
|
||||
|
@ -32,28 +33,7 @@ method generate-post(Post:D $post, BlogMeta:D $meta) {
|
|||
$body
|
||||
];
|
||||
|
||||
"<!doctype html>$html"
|
||||
}
|
||||
|
||||
method generate-blurb(Int:D $id, $db) {
|
||||
my $post = $db.posts{$id};
|
||||
my $desc = $post.description;
|
||||
my $link = post-link $id, $post;
|
||||
div :class<post-blurb>, [
|
||||
div :class<post-blurb-title>, [
|
||||
a :href($link), span [
|
||||
h2 $post.title;
|
||||
];
|
||||
];
|
||||
post-info $post;
|
||||
if $desc ~~ Str:D {
|
||||
div :class<post-blurb-description>, [
|
||||
p $post.description;
|
||||
];
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
]
|
||||
show-html $html
|
||||
}
|
||||
|
||||
method generate-index($db) {
|
||||
|
@ -62,7 +42,7 @@ method generate-index($db) {
|
|||
.head(10)
|
||||
.grep(!*.value.hidden)
|
||||
.map(-> $pair {
|
||||
self.generate-blurb: $pair.key, $db
|
||||
generate-blurb $pair.key, $db
|
||||
});
|
||||
|
||||
my $head = generate-head($db.meta);
|
||||
|
@ -79,7 +59,7 @@ method generate-index($db) {
|
|||
$body
|
||||
];
|
||||
|
||||
"<!doctype html>$html"
|
||||
show-html $html
|
||||
}
|
||||
|
||||
method generate-archive($db) {
|
||||
|
@ -87,7 +67,7 @@ method generate-archive($db) {
|
|||
$db.sorted-posts
|
||||
.grep(!*.value.hidden)
|
||||
.map(-> $pair {
|
||||
self.generate-blurb: $pair.key, $db
|
||||
generate-blurb $pair.key, $db
|
||||
});
|
||||
|
||||
my $head = generate-head($db.meta);
|
||||
|
@ -104,7 +84,7 @@ method generate-archive($db) {
|
|||
$body
|
||||
];
|
||||
|
||||
"<!doctype html>$html"
|
||||
show-html $html
|
||||
}
|
||||
|
||||
method generate-tag-blurb($db, $tag, $limit?) {
|
||||
|
@ -116,7 +96,7 @@ method generate-tag-blurb($db, $tag, $limit?) {
|
|||
a :href($link), span [
|
||||
h3 $post.title;
|
||||
];
|
||||
post-info $post;
|
||||
post-info $id, $post, $db;
|
||||
if $desc ~~ Str:D {
|
||||
div :class<tag-blurb-post-description>, [
|
||||
p $post.description;
|
||||
|
@ -162,7 +142,7 @@ method generate-tags-page($db, @tags) {
|
|||
$body
|
||||
];
|
||||
|
||||
"<!doctype html>$html"
|
||||
show-html $html
|
||||
}
|
||||
|
||||
method generate-tag-page($db, $tag) {
|
||||
|
@ -178,5 +158,5 @@ method generate-tag-page($db, $tag) {
|
|||
$body
|
||||
];
|
||||
|
||||
"<!doctype html>$html"
|
||||
show-html $html
|
||||
}
|
||||
|
|
|
@ -9,10 +9,12 @@ use XML;
|
|||
use XQ;
|
||||
|
||||
use DB::Post;
|
||||
use DB::Series;
|
||||
use DB::BlogMeta;
|
||||
use DB::MarkdownPost;
|
||||
use DB::IdrisPost;
|
||||
use DB::PlaceholderPost;
|
||||
use Render::Series;
|
||||
use Atom;
|
||||
use Config;
|
||||
|
||||
|
@ -25,6 +27,8 @@ class PostDB {
|
|||
#| A mapping from post ids to posts
|
||||
# has %.posts is Posts;
|
||||
has %.posts{Int} of PostTypes = %();
|
||||
#| 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;
|
||||
|
||||
|
@ -37,6 +41,15 @@ class PostDB {
|
|||
}
|
||||
}
|
||||
|
||||
#| Get the next unused series ID
|
||||
method next-series-id(--> Int) {
|
||||
if %!series.elems > 0 {
|
||||
%!series.keys.max + 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#| Insert a new post to the DB, returning its id
|
||||
method insert-post(PostTypes $post --> Int) {
|
||||
my $id = self.next-post-id;
|
||||
|
@ -44,6 +57,13 @@ class PostDB {
|
|||
$id
|
||||
}
|
||||
|
||||
#| 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 = %();
|
||||
|
@ -57,9 +77,11 @@ class PostDB {
|
|||
#| Write a database to a directory
|
||||
method write(IO::Path:D $dir) {
|
||||
my $posts-dir = $dir.add('posts/');
|
||||
my $series-dir = $dir.add('series/');
|
||||
# Make sure directory structrue exists
|
||||
mkdir $dir unless $dir.e;
|
||||
mkdir $posts-dir unless $posts-dir.e;
|
||||
mkdir $series-dir unless $series-dir.e;
|
||||
# Write out metadata
|
||||
# TODO: Track changes and only write changed files
|
||||
$dir.add('meta.json').spurt: $!meta.to-json(:sorted-keys);
|
||||
|
@ -67,6 +89,10 @@ class PostDB {
|
|||
for %!posts.kv -> $key, $value {
|
||||
$posts-dir.add("$key.json").spurt: $value.to-json(:sorted-keys);
|
||||
}
|
||||
# Write out the series
|
||||
for %!series.kv -> $key, $value {
|
||||
$series-dir.add("$key.json").spurt: $value.to-json(:sorted-keys);
|
||||
}
|
||||
}
|
||||
|
||||
#| Render the site to the provided output directory
|
||||
|
@ -87,7 +113,7 @@ class PostDB {
|
|||
mkdir $by-slug unless $by-slug.e;
|
||||
# Render all the posts and make symlinks
|
||||
for %!posts.kv -> $id, $post {
|
||||
my $html = $config.generate-post: $post, $!meta;
|
||||
my $html = $config.generate-post: $id, $post, self;
|
||||
my $id-path = $by-id.add: "$id.html";
|
||||
$id-path.spurt: $html;
|
||||
for $post.all-slugs -> $slug {
|
||||
|
@ -114,6 +140,16 @@ class PostDB {
|
|||
$tags-dir.add("$tag.html").spurt:
|
||||
$config.generate-tag-page(self, $tag);
|
||||
}
|
||||
# Generate the series pages
|
||||
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);
|
||||
}
|
||||
# Generate the main series page
|
||||
$out-dir.add('series.html').spurt:
|
||||
series-list-page self;
|
||||
# Render the rss/atom feed
|
||||
my $atom-path = $out-dir.add('atom.xml');
|
||||
my $atom = posts-to-atom self;
|
||||
|
@ -124,6 +160,7 @@ class PostDB {
|
|||
$res-dir.add('colors.css').spurt: %?RESOURCES<colors.css>.slurp;
|
||||
$res-dir.add('main.css').spurt: %?RESOURCES<main.css>.slurp;
|
||||
$res-dir.add('code.css').spurt: %?RESOURCES<code.css>.slurp;
|
||||
$res-dir.add('admonitions.css').spurt: %?RESOURCES<admonitions.css>.slurp;
|
||||
}
|
||||
|
||||
#| Get a list of posts sorted by date
|
||||
|
@ -135,6 +172,7 @@ class PostDB {
|
|||
#| Read the database out of a directory
|
||||
sub read-db(IO::Path:D $dir --> PostDB:D) is export {
|
||||
my $posts-dir = $dir.add('posts/');
|
||||
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
|
||||
|
@ -160,6 +198,16 @@ sub read-db(IO::Path:D $dir --> PostDB:D) is export {
|
|||
}
|
||||
}
|
||||
}
|
||||
# Read series
|
||||
my %series{Int} of Series:D = %();
|
||||
# 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
|
||||
PostDB.new: meta => $meta, posts => %posts
|
||||
PostDB.new: meta => $meta, posts => %posts, series => %series
|
||||
}
|
||||
|
|
|
@ -23,8 +23,8 @@ has Str:D $.base-url is required;
|
|||
#| Return the base url, but substitute it out if the test environment variable
|
||||
#| is set
|
||||
method get-base-url(--> Str:D) {
|
||||
if %*ENV<BLOG_TEST> {
|
||||
"http://localhost:8080"
|
||||
if %*ENV<LOCAL_RSS> {
|
||||
"http://localhost:8000"
|
||||
} else {
|
||||
$!base-url
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ DateTime:D @.edited-at
|
|||
#| An optional list of extra slugs to use for this post
|
||||
has Str:D @.slugs is json = [];
|
||||
#| An optional list of tags for this post
|
||||
has Str:D @.tags is json = [];
|
||||
has Str:D @.tags is rw is json = [];
|
||||
#| Should the post be hidden from the main list
|
||||
has Bool:D $.hidden is json is rw = False;
|
||||
|
||||
|
|
35
lib/DB/Series.rakumod
Normal file
35
lib/DB/Series.rakumod
Normal file
|
@ -0,0 +1,35 @@
|
|||
use v6.e.PREVIEW;
|
||||
|
||||
use JSON::Class:auth<zef:vrurg>;
|
||||
|
||||
#| A plain markdown post
|
||||
unit class Series is json(:pretty);
|
||||
|
||||
#| The title of a series
|
||||
has Str:D $.title is required;
|
||||
|
||||
#| The description of a series
|
||||
has Str:D $.desc is required;
|
||||
|
||||
#| The ids of the posts in the series, in series order
|
||||
has Int:D @.post-ids is rw = [];
|
||||
|
||||
#| Returns true if this series contains the given post id
|
||||
method contains-post(Int:D $post-id --> Bool:D) {
|
||||
if $post-id ~~ any @!post-ids {
|
||||
True
|
||||
} else {
|
||||
False
|
||||
}
|
||||
}
|
||||
|
||||
#| Returns the date of the lastest post
|
||||
method latest-post($db) {
|
||||
my @posts = @!post-ids.map(-> $i {$db.posts{$i}});
|
||||
if @posts {
|
||||
my $most-recent-post = @posts.max(*.posted-at);
|
||||
$most-recent-post.posted-at
|
||||
} else {
|
||||
Nil
|
||||
}
|
||||
}
|
|
@ -38,6 +38,8 @@ sub generate-head(BlogMeta:D $meta, $title?, $description?) is export {
|
|||
:href</resources/main.css>;
|
||||
link :rel<stylesheet>,
|
||||
:href</resources/code.css>;
|
||||
link :rel<stylesheet>,
|
||||
:href</resources/admonitions.css>;
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -59,8 +61,9 @@ sub site-header(BlogMeta:D $meta) is export {
|
|||
];
|
||||
div :class<header-links>, [
|
||||
header-link 'Index', '/index.html', 'home';
|
||||
header-link 'Archive', '/archive.html', 'archive';
|
||||
header-link 'All Posts', '/archive.html', 'archive';
|
||||
header-link 'Tags', '/tags.html', 'purchase-tag-alt';
|
||||
header-link 'Series', '/series.html', 'book';
|
||||
header-link 'About', '/about.html', 'info-circle';
|
||||
header-link 'Feed', '/atom.xml', 'rss';
|
||||
];
|
||||
|
|
|
@ -3,6 +3,7 @@ unit module Render::Post;
|
|||
|
||||
use Render::Util;
|
||||
use DB::Post;
|
||||
use DB::Series;
|
||||
|
||||
use HTML::Functional;
|
||||
|
||||
|
@ -17,9 +18,9 @@ sub post-date(Post:D $post) is export {
|
|||
);
|
||||
|
||||
div :class<post-time>, :title("Posted At $timestamp"), [
|
||||
icon 'time';
|
||||
icon 'calendar';
|
||||
' ';
|
||||
$timestamp
|
||||
$datetime.Date.Str
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -37,7 +38,7 @@ sub post-edit(Post:D $post) is export {
|
|||
div :class<post-edit>, :title("Last Edited At $timestamp"), [
|
||||
icon 'edit';
|
||||
' ';
|
||||
$timestamp
|
||||
$datetime.Date.Str
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -62,7 +63,7 @@ sub post-tag(Str:D $tag) is export {
|
|||
span :class<post-tag>, [
|
||||
a :href("/tags/$tag.html"), [
|
||||
icon 'hash';
|
||||
$tag;
|
||||
span $tag;
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -81,20 +82,73 @@ sub post-tags(Post:D $post) is export {
|
|||
}
|
||||
}
|
||||
|
||||
sub post-info(Post:D $post) is export {
|
||||
sub series-tag(Int:D $post-id, Int:D $series-id, Series:D $series) is export {
|
||||
span :class<post-series-tag>, [
|
||||
a :href("/series/$series-id.html"), [
|
||||
icon 'book';
|
||||
' ';
|
||||
span :class<post-series-tag-inner>, [
|
||||
$series.title;
|
||||
' ';
|
||||
'(';
|
||||
($series.post-ids.first($post-id, :k) + 1).Str;
|
||||
'/';
|
||||
$series.post-ids.elems.Str;
|
||||
')';
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
sub post-series-tags(Int:D $post-id, Post:D $post, $db) is export {
|
||||
# Find all the series this post is in
|
||||
my @series = $db.series.grep(*.value.contains-post: $post-id);
|
||||
if @series {
|
||||
div :class<post-series-tags>,
|
||||
@series.map(-> $pair {
|
||||
series-tag $post-id, $pair.key, $pair.value
|
||||
});
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
sub post-info(Int:D $id, Post:D $post, $db) is export {
|
||||
div :class<post-info>, [
|
||||
post-date $post;
|
||||
post-edit $post;
|
||||
post-read-time $post;
|
||||
post-tags $post;
|
||||
post-series-tags $id, $post, $db;
|
||||
];
|
||||
}
|
||||
|
||||
sub post-header(Post:D $post) is export {
|
||||
sub post-header(Int:D $id, Post:D $post, $db) is export {
|
||||
header :class<post-header>, [
|
||||
div :class<post-title>, [
|
||||
h1 $post.title;
|
||||
];
|
||||
post-info $post;
|
||||
post-info $id, $post, $db;
|
||||
]
|
||||
}
|
||||
|
||||
sub generate-blurb(Int:D $id, $db) is export {
|
||||
my $post = $db.posts{$id};
|
||||
my $desc = $post.description;
|
||||
my $link = post-link $id, $post;
|
||||
div :class<post-blurb>, [
|
||||
div :class<post-blurb-title>, [
|
||||
a :href($link), span [
|
||||
h2 $post.title;
|
||||
];
|
||||
];
|
||||
post-info $id, $post, $db;
|
||||
if $desc ~~ Str:D {
|
||||
div :class<post-blurb-description>, [
|
||||
p $post.description;
|
||||
];
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
144
lib/Render/Series.rakumod
Normal file
144
lib/Render/Series.rakumod
Normal file
|
@ -0,0 +1,144 @@
|
|||
use v6.e.PREVIEW;
|
||||
unit module Render::Series;
|
||||
|
||||
use Render::Util;
|
||||
use Render::Head;
|
||||
use Render::Post;
|
||||
use DB::Post;
|
||||
use DB::Series;
|
||||
|
||||
use HTML::Functional;
|
||||
|
||||
sub series-date(Series:D $series, $db) is export {
|
||||
my $datetime = $series.latest-post: $db;
|
||||
if $datetime {
|
||||
my $timestamp = sprintf(
|
||||
"%s %02d:%02d%s",
|
||||
$datetime.yyyy-mm-dd,
|
||||
($datetime.hour % 12) || 12,
|
||||
$datetime.minute,
|
||||
$datetime.hour < 12 ?? 'am' !! 'pm'
|
||||
);
|
||||
|
||||
div :class<series-time>, :title("Latest post made at $timestamp"), [
|
||||
icon 'calendar';
|
||||
' ';
|
||||
$datetime.Date.Str
|
||||
]
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
sub series-read-time(Series:D $series, $db) is export {
|
||||
my @readtimes = $series.post-ids.map(-> $i {$db.posts{$i}.readtimes});
|
||||
my ($slow, $average, $fast) = 0, 0, 0;
|
||||
for @readtimes -> ($s, $a, $f) {
|
||||
$slow += $s;
|
||||
$average += $a;
|
||||
$fast += $f;
|
||||
}
|
||||
div :class<series-read-time>, :title<Estimated read time at 140/180/220 WPM>, [
|
||||
icon 'timer';
|
||||
' ';
|
||||
mins-to-string $slow;
|
||||
' ';
|
||||
'/';
|
||||
' ';
|
||||
mins-to-string $average;
|
||||
' ';
|
||||
'/';
|
||||
' ';
|
||||
mins-to-string $fast;
|
||||
]
|
||||
}
|
||||
|
||||
sub series-count(Series:D $series, $db) is export {
|
||||
my $count = $series.post-ids.elems;
|
||||
div :class<series-article-count>, :title("Series has $count articles"), [
|
||||
icon 'add-to-queue';
|
||||
' ';
|
||||
"$count articles";
|
||||
]
|
||||
}
|
||||
|
||||
sub series-info(Series:D $series, $db) is export {
|
||||
div :class<series-info>, [
|
||||
series-date $series, $db;
|
||||
series-read-time $series, $db;
|
||||
series-count $series, $db;
|
||||
]
|
||||
}
|
||||
|
||||
sub series-header(Series:D $series, $db) is export {
|
||||
header :class<series-header>, [
|
||||
div :class<series-title>, [
|
||||
h1 $series.title;
|
||||
];
|
||||
div :class<series-desc>, [
|
||||
p $series.desc;
|
||||
];
|
||||
series-info $series, $db;
|
||||
]
|
||||
}
|
||||
|
||||
sub series-page(Int:D $series-id, $db) is export {
|
||||
my $meta = $db.meta;
|
||||
my $series = $db.series{$series-id};
|
||||
my $head = generate-head($meta, $series.title, $series.desc);
|
||||
my $body =
|
||||
body [
|
||||
site-header $meta;
|
||||
article :class<series>, [
|
||||
series-header $series, $db;
|
||||
div :class<series-blurbs>,
|
||||
$series.post-ids.map(*.&generate-blurb($db));
|
||||
]
|
||||
];
|
||||
|
||||
my $html = html :lang<en>, [
|
||||
$head;
|
||||
$body
|
||||
];
|
||||
|
||||
show-html $html;
|
||||
}
|
||||
|
||||
sub series-blurb(Int:D $id, Series:D $series, $db) {
|
||||
my $link = "/series/$id.html";
|
||||
div :class<series-list-blurb>, [
|
||||
div :class<series-list-blurb-title>, [
|
||||
a :href($link), span [
|
||||
h2 $series.title;
|
||||
];
|
||||
p $series.desc;
|
||||
];
|
||||
series-info $series, $db;
|
||||
]
|
||||
}
|
||||
|
||||
sub series-list-page($db) is export {
|
||||
my @series = $db.series.sort(*.value.latest-post: $db);
|
||||
my @series-blurbs = ();
|
||||
for @series -> $pair {
|
||||
my $id = $pair.key;
|
||||
my $series = $pair.value;
|
||||
@series-blurbs.push:
|
||||
series-blurb $id, $series, $db;
|
||||
}
|
||||
|
||||
my $head = generate-head($db.meta);
|
||||
my $body = body [
|
||||
site-header $db.meta;
|
||||
div :class<series-list>, [
|
||||
h1 "All Series"
|
||||
], @series-blurbs;
|
||||
];
|
||||
|
||||
my $html = html :lang<en>, [
|
||||
$head;
|
||||
$body;
|
||||
];
|
||||
|
||||
show-html $html;
|
||||
}
|
|
@ -5,6 +5,15 @@ use DB::Post;
|
|||
|
||||
use HTML::Functional;
|
||||
|
||||
sub show-html($html) is export {
|
||||
my $out = "<!doctype html>$html";
|
||||
# Work around HTML::Functional automatically putting newlines between tags
|
||||
$out ~~ s:g/'</i>' \v+ '<span>'/<\/i><span>/;
|
||||
$out ~~ s:g/\v+ '</a>'/<\/a>/;
|
||||
$out ~~ s:g/',' \v+ '<span'/,<span/;
|
||||
$out
|
||||
}
|
||||
|
||||
sub opt($test, $item) is export {
|
||||
if $test {
|
||||
$item
|
||||
|
|
|
@ -18,6 +18,7 @@ authors = "Nathan McCarty"
|
|||
-- modules to install
|
||||
modules = Idris
|
||||
, Posts.HelloWorld
|
||||
, LessMacrosMoreTypes.Printf
|
||||
|
||||
-- main file (i.e. file to load at REPL)
|
||||
-- main =
|
||||
|
|
45
projects/Idris/src/LessMacrosMoreTypes/Printf.md
Normal file
45
projects/Idris/src/LessMacrosMoreTypes/Printf.md
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Type Safe Variadic printf
|
||||
|
||||
```idris hide
|
||||
module LessMacrosMoreTypes.Printf
|
||||
|
||||
%default total
|
||||
```
|
||||
|
||||
While C can provide convenient string formatting by having hideously memory
|
||||
unsafe variadics, and dynamic languages, like python, can do the same while
|
||||
being memory safe by not being type safe, many type safe languages, such as
|
||||
Rust, are forced to provide such functionality through the use of a macro.
|
||||
Dependently typed languages, like Idris, can provide a printf like formatting
|
||||
interface, while maintaining both memory and type saftey, without the need for
|
||||
the macro. We will explore this by implementing a simplified version of `printf`
|
||||
in Idris from scratch.
|
||||
|
||||
This article is inspired by an exercise from chapter 6 of [Type Driven
|
||||
Development with
|
||||
Idris](https://www.manning.com/books/type-driven-development-with-idris), and is
|
||||
written as a literate Idris file.
|
||||
|
||||
## Gameplan
|
||||
|
||||
Our goal is to provide a printf function that can be called, much like it's C equivlant, like so:
|
||||
|
||||
> [!NOTE]
|
||||
> As this is a literate Idris document, and we haven't defined our `printf`
|
||||
> function yet, we have to use a `failing` block to ask the compiler to check
|
||||
> that this code parses, and syntax highlight it for us, but not attempt to
|
||||
> actually compile it.
|
||||
|
||||
```idris
|
||||
failing
|
||||
example_usage : String
|
||||
example_usage = printf "%s %d %02d" "hello" 1 2
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Parsing a Format String
|
||||
|
||||
## Calculating a Type From a Format String
|
||||
|
||||
## printf
|
|
@ -65,6 +65,10 @@ calls to make interaction between lazy and strict code Just Work™:
|
|||
had initially thought I had discovered just _one_ compiler bug, but it turns out
|
||||
I also stumbled into an unrelated issue in the termination checker![^8]
|
||||
|
||||
I also found a convoluted hole in the case generator that I had to reach out to
|
||||
the community for help minimizing (thank you
|
||||
dunhamsteve!):[idris-lang/Idris2#3466](https://github.com/idris-lang/Idris2/issues/3466)[^9]
|
||||
|
||||
I broke the [pack](https://github.com/stefan-hoeck/idris2-pack) package manager
|
||||
by, apparently, being the first person to try and upload a library to `pack-db`
|
||||
with library code in literate files, in my
|
||||
|
@ -96,3 +100,5 @@ ecosystem.
|
|||
subset of the language for which termination is a 'trivial' property, which
|
||||
includes most code I've written in the language so far, but not nearly all
|
||||
of it.
|
||||
|
||||
[^9]: It's me, I'm the user on the discord
|
||||
|
|
84
resources/admonitions.css
Normal file
84
resources/admonitions.css
Normal file
|
@ -0,0 +1,84 @@
|
|||
/* Universal configuration */
|
||||
.note,
|
||||
.tip,
|
||||
.important,
|
||||
.warning,
|
||||
.caution {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 66%;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--bg-1);
|
||||
color: var(--fg-1);
|
||||
padding: 0.5rem;
|
||||
border-radius: 1rem;
|
||||
border: solid 0.5rem;
|
||||
margin-top: var(--box-margin-vert);
|
||||
margin-bottom: var(--box-margin-vert);
|
||||
}
|
||||
.note .title,
|
||||
.tip .title,
|
||||
.important .title,
|
||||
.warning .title,
|
||||
.caution .title {
|
||||
align-self: center;
|
||||
}
|
||||
.note .title p,
|
||||
.tip .title p,
|
||||
.important .title p,
|
||||
.warning .title p,
|
||||
.caution .title p {
|
||||
font-size: 0;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
.note .title p::before,
|
||||
.tip .title p::before,
|
||||
.important .title p::before,
|
||||
.warning .title p::before,
|
||||
.caution .title p::before {
|
||||
font-family: 'boxicons' !important;
|
||||
font-size: 3rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Notes */
|
||||
.note {
|
||||
border-color: var(--blue);
|
||||
}
|
||||
.note .title p::before {
|
||||
content: "\eb21";
|
||||
color: var(--blue);
|
||||
}
|
||||
/* Tips */
|
||||
.tip {
|
||||
border-color: var(--green);
|
||||
}
|
||||
.tip .title p::before {
|
||||
content: "\eb0d";
|
||||
color: var(--green);
|
||||
}
|
||||
/* Importants */
|
||||
.important {
|
||||
border-color: var(--violet);
|
||||
}
|
||||
.important .title p::before {
|
||||
content: "\eb0d";
|
||||
color: var(--violet);
|
||||
}
|
||||
/* Warnings */
|
||||
.warning {
|
||||
border-color: var(--orange);
|
||||
}
|
||||
.warning .title p::before {
|
||||
content: "\e9a3";
|
||||
color: var(--orange);
|
||||
}
|
||||
/* Cautions */
|
||||
.caution {
|
||||
border-color: var(--red);
|
||||
}
|
||||
.caution .title p::before {
|
||||
content: "\ee87";
|
||||
color: var(--red);
|
||||
}
|
|
@ -53,10 +53,11 @@ a:visited {
|
|||
.site-tagline {
|
||||
color: var(--dim-0);
|
||||
}
|
||||
.post-body, .post-header, .post-blurbs, .tags, .tags .tag-blurb-post {
|
||||
.post-body, .post-header, .post-blurbs, .tags, .tags .tag-blurb-post,
|
||||
.series-header, .series-blurbs, .series-list {
|
||||
background-color: var(--bg-0);
|
||||
}
|
||||
.post-blurb, .tags .tag-blurb {
|
||||
.post-blurb, .tags .tag-blurb, .series-list-blurb {
|
||||
background-color: var(--bg-1);
|
||||
}
|
||||
:not(.tags) .tag-blurb {
|
||||
|
@ -65,12 +66,15 @@ a:visited {
|
|||
:not(.tags) .tag-blurb-post {
|
||||
background-color: var(--bg-1);
|
||||
}
|
||||
.post-title, .post-blurbs h1 {
|
||||
.post-title, .post-blurbs h1, .series-header h1, .series-list h1 {
|
||||
color: var(--green);
|
||||
}
|
||||
.post-body h2, .post-body h3, .post-body h4 {
|
||||
color: var(--fg-1);
|
||||
}
|
||||
.post-info > *, .series-info > * {
|
||||
background-color: var(--bg-2);
|
||||
}
|
||||
blockquote {
|
||||
background-color: var(--bg-1);
|
||||
}
|
||||
|
|
|
@ -27,13 +27,13 @@
|
|||
}
|
||||
|
||||
/* Main Body and Post Flexboxs */
|
||||
body, .post {
|
||||
body, .post, .series {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--box-gap);
|
||||
}
|
||||
.post {
|
||||
.post, .series, .series-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
@ -65,17 +65,17 @@ body, .post {
|
|||
flex-wrap: wrap;
|
||||
margin-top: var(--box-margin-vert);
|
||||
}
|
||||
.header-links > a > span {
|
||||
.header-links > a > span, .post-series-tag > a > span, .post-tag > a > span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.header-links > a {
|
||||
.header-links > a, .post-series-tag > a, .post-tag > a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
/* Style the post header, body, and blurbs */
|
||||
/* TODO: Style footnotes and get footnote hover working */
|
||||
.post-header, .post-body {
|
||||
.post-header, .post-body, .series-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
@ -90,11 +90,11 @@ body, .post {
|
|||
margin: auto var(--box-margin-horz);
|
||||
align-self: stretch;
|
||||
}
|
||||
.post-title h1 {
|
||||
.post-title h1, .series-title h1 {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.post-info {
|
||||
.post-info, .series-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
@ -108,7 +108,7 @@ body, .post {
|
|||
.post-body h2, .post-body h3, .post-body h4 {
|
||||
text-align: center;
|
||||
}
|
||||
.post-blurbs {
|
||||
.post-blurbs, .series-blurbs, .series-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
@ -116,8 +116,9 @@ body, .post {
|
|||
max-width: var(--content-width);
|
||||
padding: var(--box-padding-vert) var(--box-padding-horz);
|
||||
border-radius: var(--box-radius);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.post-blurb {
|
||||
.post-blurb, .series-list-blurb {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: var(--box-radius);
|
||||
|
@ -128,6 +129,10 @@ body, .post {
|
|||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.post-info > *, .series-info > * {
|
||||
padding: 0.25em;
|
||||
border-radius: 0.25em;
|
||||
}
|
||||
|
||||
/* TODO: Nice fancy blockquotes */
|
||||
blockquote {
|
||||
|
|
Loading…
Add table
Reference in a new issue