Compare commits

...

18 commits

19 changed files with 732 additions and 58 deletions

234
blog
View file

@ -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
View 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
View 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"
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
View 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
}
}

View file

@ -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';
];

View file

@ -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';
'&nbsp;';
$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';
'&nbsp;';
$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';
'&nbsp;';
span :class<post-series-tag-inner>, [
$series.title;
'&nbsp;';
'(';
($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
View 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';
'&nbsp;';
$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';
'&nbsp;';
mins-to-string $slow;
'&nbsp;';
'/';
'&nbsp;';
mins-to-string $average;
'&nbsp;';
'/';
'&nbsp;';
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';
'&nbsp;';
"$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;
}

View file

@ -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

View file

@ -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 =

View 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

View file

@ -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
View 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);
}

View file

@ -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);
}

View file

@ -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 {