Compare commits
21 commits
b307eb391b
...
8fc5a0fbda
Author | SHA1 | Date | |
---|---|---|---|
8fc5a0fbda | |||
d79c4b6f46 | |||
a85bfdcd2a | |||
a5f9c659ff | |||
a2a811aa4a | |||
51e50cd0d6 | |||
5ced2f6298 | |||
3d2d1f897e | |||
c0780047bf | |||
0e87e752d2 | |||
79ee0d4c63 | |||
e01342c980 | |||
4c6839c698 | |||
4aefda36fd | |||
88bb800ec4 | |||
75b174f0a7 | |||
d6b17a4be0 | |||
3069e480a4 | |||
63e581ac03 | |||
82485924be | |||
4de44c7e8c |
29 changed files with 801 additions and 33 deletions
45
blog
45
blog
|
@ -3,6 +3,7 @@ use v6.e.PREVIEW;
|
|||
|
||||
use DB;
|
||||
use DB::BlogMeta;
|
||||
use DB::HeaderLink;
|
||||
use DB::Series;
|
||||
use DB::MarkdownPost;
|
||||
use DB::IdrisPost;
|
||||
|
@ -417,3 +418,47 @@ multi MAIN(
|
|||
$series.post-ids.push: $post-id.Int;
|
||||
$db.write: $db-dir;
|
||||
}
|
||||
|
||||
#| Add a header link
|
||||
multi MAIN(
|
||||
"header-link",
|
||||
"add",
|
||||
#| The path of the database directory
|
||||
IO::Path(Str) :$db-dir = $default-db-dir,
|
||||
) {
|
||||
my $db = read-db $db-dir;
|
||||
print "Link Text: ";
|
||||
my $text = get;
|
||||
print "Link Location: ";
|
||||
my $link = get;
|
||||
print "Icon: ";
|
||||
my $icon = get;
|
||||
|
||||
my $header-link =
|
||||
HeaderLink.new:
|
||||
text => $text, link => $link, icon => $icon;
|
||||
|
||||
$db.meta.header-links.push: $header-link;
|
||||
|
||||
$db.write: $db-dir;
|
||||
}
|
||||
|
||||
#| Do a clean build and upload the blog to the remote
|
||||
multi MAIN(
|
||||
"upload",
|
||||
#| The path of the database directory
|
||||
IO::Path(Str) :$db-dir = $default-db-dir,
|
||||
#| The path of the output directory
|
||||
IO::Path(Str) :$site-dir = $default-site-dir,
|
||||
) {
|
||||
my $db = read-db $db-dir;
|
||||
my $site = "{$site-dir.absolute}/";
|
||||
# Clean out the site dir
|
||||
my $proc = run <rm -rf>, $site;
|
||||
die "clean failed" unless $proc;
|
||||
# Render the site
|
||||
$db.render: $site-dir;
|
||||
# Upload it
|
||||
$proc = run <rsync -avzhL>, $site, <static.stranger.systems:/var/www/www.stranger.systems>;
|
||||
die "rsync failed" unless $proc;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
{
|
||||
"about-id": 2,
|
||||
"base-url": "https://www.stranger.systems",
|
||||
"header-links": [
|
||||
{
|
||||
"icon": "receipt",
|
||||
"link": "/posts/by-slug/hire-me.html",
|
||||
"text": "Hire Me"
|
||||
}
|
||||
],
|
||||
"placeholder-id": 0,
|
||||
"tagline": "Making software better by making it weird",
|
||||
"title": "Stranger Systems"
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
"slugs": [
|
||||
],
|
||||
"source": "/dev/null",
|
||||
"special": false,
|
||||
"tags": [
|
||||
]
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
"slugs": [
|
||||
],
|
||||
"source": "/home/nathan/Projects/Blog/projects/Markdown/MyNewBlog.md",
|
||||
"special": false,
|
||||
"tags": [
|
||||
"meta",
|
||||
"raku"
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
"slugs": [
|
||||
],
|
||||
"source": "/home/nathan/Projects/Blog/projects/Markdown/About.md",
|
||||
"special": true,
|
||||
"tags": [
|
||||
]
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
"slugs": [
|
||||
],
|
||||
"source": "/home/nathan/Projects/Blog/projects/Markdown/CryptoSuite.md",
|
||||
"special": false,
|
||||
"tags": [
|
||||
"cryptography"
|
||||
]
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
"slugs": [
|
||||
],
|
||||
"source": "/home/nathan/Projects/Blog/projects/Markdown/2025/01-Jan/AdventOfBugs.md",
|
||||
"special": false,
|
||||
"tags": [
|
||||
"idris",
|
||||
"advent-of-code"
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"edited-at": [
|
||||
"2025-02-14T12:00:50.492225-05:00",
|
||||
"2025-02-14T23:42:15.625034-05:00"
|
||||
],
|
||||
"hidden": false,
|
||||
"idris": true,
|
||||
|
@ -9,6 +11,7 @@
|
|||
],
|
||||
"source": "/home/nathan/Projects/Blog/projects/Idris/src/LessMacrosMoreTypes/Printf.md",
|
||||
"source-code": "https://git.stranger.systems/thatonelutenist/website/src/branch/trunk/projects/Idris/src/LessMacrosMoreTypes/Printf.md",
|
||||
"special": false,
|
||||
"tags": [
|
||||
"idris"
|
||||
]
|
||||
|
|
13
db/posts/6.json
Normal file
13
db/posts/6.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"edited-at": [
|
||||
],
|
||||
"hidden": true,
|
||||
"markdown": true,
|
||||
"posted-at": "2025-03-15T14:36:38.402495-04:00",
|
||||
"slugs": [
|
||||
],
|
||||
"source": "/home/nathan/Projects/Blog/projects/Markdown/HireMe.md",
|
||||
"special": true,
|
||||
"tags": [
|
||||
]
|
||||
}
|
16
db/posts/7.json
Normal file
16
db/posts/7.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"edited-at": [
|
||||
],
|
||||
"hidden": false,
|
||||
"idris": true,
|
||||
"ipkg": "/home/nathan/Projects/Blog/projects/Idris/Idris.ipkg",
|
||||
"posted-at": "2025-03-15T18:07:00.649227-04:00",
|
||||
"slugs": [
|
||||
],
|
||||
"source": "/home/nathan/Projects/Blog/projects/Idris/src/DependentNuggets/FreshLists.md",
|
||||
"source-code": "https://git.stranger.systems/thatonelutenist/website/src/branch/trunk/projects/Idris/src/DependentNuggets/FreshLists.md",
|
||||
"special": false,
|
||||
"tags": [
|
||||
"idris"
|
||||
]
|
||||
}
|
|
@ -1,7 +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.",
|
||||
"desc": "Macros are annoying, but an unfortunate fact of life in many programming languages.</p><p>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"
|
||||
}
|
||||
}
|
||||
|
|
7
db/series/1.json
Normal file
7
db/series/1.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"desc": "Introductions to the building blocks of dependently typed programs.",
|
||||
"post-ids": [
|
||||
7
|
||||
],
|
||||
"title": "Dependent Nuggets"
|
||||
}
|
|
@ -5,6 +5,7 @@ use HTML::Functional;
|
|||
use Render::Util;
|
||||
use Render::Head;
|
||||
use Render::Post;
|
||||
use Render::Foot;
|
||||
use DB::BlogMeta;
|
||||
use DB::Post;
|
||||
|
||||
|
@ -13,9 +14,20 @@ unit class Config;
|
|||
method generate-post(Int:D $id, Post:D $post, $db) {
|
||||
my $meta = $db.meta;
|
||||
my $content = $post.render-html;
|
||||
my $cactus-script = qq「」
|
||||
document.addEventListener('DOMContentLoaded', () => \{
|
||||
initComments(\{
|
||||
node: document.getElementById("comment-section"),
|
||||
defaultHomeserverUrl: "https://matrix.cactus.chat",
|
||||
serverName: "cactus.chat",
|
||||
siteName: "stranger.systems",
|
||||
commentSectionId: "post-$id"
|
||||
\})\})」;
|
||||
my $head =
|
||||
head [
|
||||
# Generate the universal header components
|
||||
generate-head($meta, $post.title, $post.description);
|
||||
# Open graph tags for embedding
|
||||
meta :property<og:title>, :content($post.title);
|
||||
meta :property<og:url>, :content(post-link-abs $db.meta, $id, $post);
|
||||
meta :property<og:site_name>, :content($db.meta.title);
|
||||
|
@ -26,20 +38,38 @@ method generate-post(Int:D $id, Post:D $post, $db) {
|
|||
$post.tags.map(-> $tag {
|
||||
meta :property<article:tag>, :content($tag)
|
||||
});
|
||||
|
||||
# Cactus comments support
|
||||
link :rel<stylesheet>,
|
||||
:href</resources/cactus.css>,
|
||||
:type<text/css>;
|
||||
script :src<https://gateway.pinata.cloud/ipfs/QmSiWN27KZZ1XE32jKwifBnS3nWTUcFGNArKzur2nmDgoL/v0.13.0/cactus.js>;
|
||||
# Only actually load the script if the post isn't hidden
|
||||
optl !$post.hidden, -> {script $cactus-script};
|
||||
];
|
||||
my $body =
|
||||
body [
|
||||
site-header $meta;
|
||||
article :class<post>, [
|
||||
post-header $id, $post, $db;
|
||||
div :class<post-body>, [
|
||||
$content;
|
||||
]
|
||||
]
|
||||
# Only generate the post header if the post isn't special
|
||||
optl !$post.special, -> {post-header $id, $post, $db};
|
||||
# If the post is special, wrap it in a special div
|
||||
do if $post.special {
|
||||
div :class<special-post>, [
|
||||
div :class<post-body>, [
|
||||
$content;
|
||||
];
|
||||
];
|
||||
} else {
|
||||
div :class<post-body>, [
|
||||
$content;
|
||||
];
|
||||
}
|
||||
];
|
||||
# Only actually have the comment section if the post isn't hidden
|
||||
optl !$post.hidden, -> {div :id<comment-section>, :class<comments>};
|
||||
generate-footer;
|
||||
];
|
||||
# TODO: Setup Comments
|
||||
# TODO: Setup footer
|
||||
# my $footer;
|
||||
|
||||
my $html = html :lang<en>, [
|
||||
$head,
|
||||
|
@ -64,6 +94,7 @@ method generate-index($db) {
|
|||
div :class<post-blurbs>, [
|
||||
h1 "Recent Posts"
|
||||
], @most-recent;
|
||||
generate-footer;
|
||||
];
|
||||
|
||||
my $html =
|
||||
|
@ -89,6 +120,7 @@ method generate-archive($db) {
|
|||
div :class<post-blurbs>, [
|
||||
h1 "All Posts"
|
||||
], @most-recent;
|
||||
generate-footer;
|
||||
];
|
||||
|
||||
my $html =
|
||||
|
@ -147,6 +179,7 @@ method generate-tags-page($db, @tags) {
|
|||
div :class<tags>, [
|
||||
h1 "Tags";
|
||||
], @tags.map(-> $tag {self.generate-tag-blurb($db, $tag, 4)});
|
||||
generate-footer;
|
||||
];
|
||||
|
||||
my $html =
|
||||
|
@ -163,6 +196,7 @@ method generate-tag-page($db, $tag) {
|
|||
my $body = body [
|
||||
site-header $db.meta;
|
||||
self.generate-tag-blurb($db, $tag, 4);
|
||||
generate-footer;
|
||||
];
|
||||
|
||||
my $html =
|
||||
|
|
|
@ -159,10 +159,11 @@ class PostDB {
|
|||
mkdir $res-dir unless $res-dir.e;
|
||||
# symlink the resources directory to make "interactive" styling eaiser
|
||||
# TODO: Directories support
|
||||
%?RESOURCES<colors.css>.IO.symlink: $res-dir.add('colors.css') unless %?RESOURCES<colors.css>.IO.e;
|
||||
%?RESOURCES<main.css>.IO.symlink: $res-dir.add('main.css') unless %?RESOURCES<main.css>.IO.e;
|
||||
%?RESOURCES<code.css>.IO.symlink: $res-dir.add('code.css') unless %?RESOURCES<code.css>.IO.e;
|
||||
%?RESOURCES<admonitions.css>.IO.symlink: $res-dir.add('admonitions.css') unless %?RESOURCES<admonitions.css>.IO.e;
|
||||
%?RESOURCES<colors.css>.IO.symlink: $res-dir.add('colors.css') unless $res-dir.add('colors.css').e;
|
||||
%?RESOURCES<main.css>.IO.symlink: $res-dir.add('main.css') unless $res-dir.add('main.css').e;
|
||||
%?RESOURCES<code.css>.IO.symlink: $res-dir.add('code.css') unless $res-dir.add('code.css').e;
|
||||
%?RESOURCES<admonitions.css>.IO.symlink: $res-dir.add('admonitions.css') unless $res-dir.add('admonitions.css').e;
|
||||
%?RESOURCES<cactus.css>.IO.symlink: $res-dir.add('cactus.css') unless $res-dir.add('cactus.css').e;
|
||||
}
|
||||
|
||||
#| Get a list of posts sorted by date
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use v6.e.PREVIEW;
|
||||
|
||||
use JSON::Class:auth<zef:vrurg>;
|
||||
use DB::HeaderLink;
|
||||
|
||||
# Top level metadata for the blog
|
||||
unit class BlogMeta is json(:pretty);
|
||||
|
@ -17,6 +18,9 @@ has Int:D $.placeholder-id is rw = 0;
|
|||
#| The id of the about post
|
||||
has Int:D $.about-id is rw = 0;
|
||||
|
||||
#| Optional list of extra header links
|
||||
has HeaderLink:D @.header-links is rw = [];
|
||||
|
||||
#| The base url of this post
|
||||
has Str:D $.base-url is required;
|
||||
|
||||
|
|
12
lib/DB/HeaderLink.rakumod
Normal file
12
lib/DB/HeaderLink.rakumod
Normal file
|
@ -0,0 +1,12 @@
|
|||
use v6.e.PREVIEW;
|
||||
|
||||
use JSON::Class:auth<zef:vrurg>;
|
||||
|
||||
# Additional header links
|
||||
unit class HeaderLink is json(:pretty);
|
||||
|
||||
has Str:D $.link is required is rw;
|
||||
|
||||
has Str:D $.text is required is rw;
|
||||
|
||||
has Str $.icon is rw;
|
|
@ -43,6 +43,8 @@ has Str:D @.tags is rw is json = [];
|
|||
has Bool:D $.hidden is json is rw = False;
|
||||
#| An optional link to the source code for the post
|
||||
has Str $.source-code is rw is json;
|
||||
#| Special posts follow different rendering rules
|
||||
has Bool:D $.special is json is rw = False;
|
||||
|
||||
#| Get the title for this post, intended to be extracted from whatever
|
||||
#| document produced it
|
||||
|
@ -60,9 +62,9 @@ method updated(--> DateTime:D) {
|
|||
#| Get the list of slugs for this post, including ones auto generated from
|
||||
#| the title, as well as any additional slugs
|
||||
method all-slugs(--> Array[Str:D]) {
|
||||
my $title-words = self.title.lc.trim.words;
|
||||
my $long-title-slug = $title-words.join('-');
|
||||
my $six-word-slug = self.title.lc.words.head(6).join('-');
|
||||
my @title-words = self.title.lc.trim.words.map(*.subst(/<-alnum>/, :g));
|
||||
my $long-title-slug = @title-words.join('-');
|
||||
my $six-word-slug = @title-words.head(6).join('-');
|
||||
my Str:D @slugs = @!slugs.clone;
|
||||
@slugs.push($long-title-slug);
|
||||
@slugs.push($six-word-slug);
|
||||
|
|
69
lib/Render/Foot.rakumod
Normal file
69
lib/Render/Foot.rakumod
Normal file
|
@ -0,0 +1,69 @@
|
|||
use v6.e.PREVIEW;
|
||||
unit module Render::Foot;
|
||||
|
||||
use HTML::Functional;
|
||||
|
||||
use Render::Util;
|
||||
use DB::BlogMeta;
|
||||
|
||||
sub footer-link($name, $title, $path, $icon) {
|
||||
div :class<footer-link>, [
|
||||
$icon;
|
||||
a :href($path), :title($title), [
|
||||
$name;
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
sub generate-footer() is export {
|
||||
footer [
|
||||
div :class<contact>, [
|
||||
div :class<footer-title>, span "Contact Me";
|
||||
div :class<footer-links>, [
|
||||
footer-link (^ "Discord"),
|
||||
"Discord",
|
||||
"https://discordapp.com/users/thatonelutenist",
|
||||
simple-icon "discord";
|
||||
footer-link (^ "Matrix"),
|
||||
"Matrix",
|
||||
"https://matrix.to/#/@thatonelutenist:stranger.systems",
|
||||
simple-icon "matrix";
|
||||
footer-link (^ "Public Email Inbox"),
|
||||
"Email",
|
||||
"mailto:~thatonelutenist/public-inbox@lists.sr.ht",
|
||||
icon-solid "envelope";
|
||||
];
|
||||
];
|
||||
div :class<code>, [
|
||||
div :class<footer-title>, span "Find My Code";
|
||||
div :class<footer-links>, [
|
||||
footer-link "git.stranger.systems",
|
||||
"Stranger Systems Forgejo",
|
||||
"https://git.stranger.systems/explore/repos",
|
||||
simple-icon "forgejo";
|
||||
footer-link (^ "sr.ht"),
|
||||
"sr.ht",
|
||||
"https://sr.ht/~thatonelutenist/",
|
||||
simple-icon "sourcehut";
|
||||
footer-link (^ "Github"),
|
||||
"Github",
|
||||
"https://github.com/nmccarty",
|
||||
simple-icon "github";
|
||||
footer-link (^ "Gitlab"),
|
||||
"Gitlab",
|
||||
"https://gitlab.com/thatonelutenist",
|
||||
simple-icon "gitlab";
|
||||
];
|
||||
];
|
||||
div :class<socials>, [
|
||||
div :class<footer-title>, span "Social Media";
|
||||
div :class<footer-links>, [
|
||||
footer-link "Mastodon",
|
||||
"Mastodon",
|
||||
"https://hachyderm.io/@thatonelutenist",
|
||||
simple-icon "mastodon";
|
||||
];
|
||||
]
|
||||
]
|
||||
}
|
||||
|
|
@ -41,11 +41,15 @@ sub generate-head(BlogMeta:D $meta, $title?, $description?) is export {
|
|||
:href</resources/code.css>;
|
||||
link :rel<stylesheet>,
|
||||
:href</resources/admonitions.css>;
|
||||
# Verify mastodon
|
||||
link :rel<me>, :href<https://hachyderm.io/@thatonelutenist>;
|
||||
# Atribute on mastodon
|
||||
meta :name<fediverse:creator>, :content<@thatonelutenist@hachyderm.io>;
|
||||
# Tell dark reader that we'll behave
|
||||
meta :name<color-scheme>, :content<light dark>;
|
||||
# Tell feed readers about our feed
|
||||
link :rel<alternate>, :type<application/atom+xml>, :title($meta.title),
|
||||
:href</feed.xml>;
|
||||
:href</atom.xml>;
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -71,6 +75,6 @@ sub site-header(BlogMeta:D $meta) is export {
|
|||
header-link 'Series', '/series.html', 'book';
|
||||
header-link 'About', '/about.html', 'info-circle';
|
||||
header-link 'Feed', '/atom.xml', 'rss';
|
||||
];
|
||||
], $meta.header-links.map(-> $link {header-link $link.text, $link.link, $link.icon});
|
||||
]
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ unit module Render::Series;
|
|||
|
||||
use Render::Util;
|
||||
use Render::Head;
|
||||
use Render::Foot;
|
||||
use Render::Post;
|
||||
use DB::Post;
|
||||
use DB::Series;
|
||||
|
@ -93,7 +94,8 @@ sub series-page(Int:D $series-id, $db) is export {
|
|||
series-header $series, $db;
|
||||
div :class<series-blurbs>,
|
||||
$series.post-ids.map(*.&generate-blurb($db));
|
||||
]
|
||||
];
|
||||
generate-footer;
|
||||
];
|
||||
|
||||
my $html = html :lang<en>, [
|
||||
|
@ -133,6 +135,7 @@ sub series-list-page($db) is export {
|
|||
div :class<series-list>, [
|
||||
h1 "All Series"
|
||||
], @series-blurbs;
|
||||
generate-footer;
|
||||
];
|
||||
|
||||
my $html = html :lang<en>, [
|
||||
|
|
|
@ -49,10 +49,18 @@ sub icon($icon) is export {
|
|||
i(:class("bx bx-$icon"))
|
||||
}
|
||||
|
||||
sub icon-solid($icon) is export {
|
||||
i(:class("bx bxs-$icon"))
|
||||
}
|
||||
|
||||
sub logo($logo) is export {
|
||||
i(:class("bx bxl-$logo"))
|
||||
}
|
||||
|
||||
sub simple-icon($icon) is export {
|
||||
img :src("https://cdn.simpleicons.org/$icon/474747/b9b9b9")
|
||||
}
|
||||
|
||||
sub mins-to-string($mins) is export {
|
||||
if $mins < 60 {
|
||||
$mins.Str ~ "m"
|
||||
|
|
|
@ -19,6 +19,7 @@ authors = "Nathan McCarty"
|
|||
modules = Idris
|
||||
, Posts.HelloWorld
|
||||
, LessMacrosMoreTypes.Printf
|
||||
, DependentNuggets.FreshLists
|
||||
|
||||
-- main file (i.e. file to load at REPL)
|
||||
-- main =
|
||||
|
|
8
projects/Idris/src/DependentNuggets/FreshLists.md
Normal file
8
projects/Idris/src/DependentNuggets/FreshLists.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# FreshLists: Containers That Only Accept "Fresh" Elements
|
||||
|
||||
```idris hide
|
||||
module DependentNuggets.FreshLists
|
||||
```
|
||||
|
||||
When programming, we quite frequently encounter the for a data structure that can contain at most one of each given element. Typically, we would use a `Set`, which satisfies this constraint, but does so as a runtime invariant that must be taken on trust, and results in ergonomic concerns when used as a component of API design.
|
||||
|
|
@ -5,13 +5,16 @@ module LessMacrosMoreTypes.Printf
|
|||
|
||||
import Data.List
|
||||
import System
|
||||
|
||||
-- Only needed for addenda
|
||||
import Data.Vect
|
||||
```
|
||||
|
||||
While C can provide "convenient" string formatting by having hideously unsafe variadics, and dynamic languages, like python, can do the same, 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 safety, without the need for macros. 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, with the source available [here](https://git.stranger.systems/thatonelutenist/website/src/branch/trunk/projects/Idris/src/LessMacrosMoreTypes/Printf.md).
|
||||
|
||||
## Gameplan
|
||||
## Game plan
|
||||
|
||||
Our goal is to provide a `printf` function that can be called much like it's C equivalent:
|
||||
|
||||
|
@ -29,7 +32,7 @@ Idris lacks a dedicated facility for variadics, but we can call functions in typ
|
|||
Idris allows us to manipulate types as first class values, and we can use the runtime values of previous arguments to the function we are declaring as arguments to later type-level functions. These type-level functions can, furthermore, recurse into themselves to produce type signatures of varying length and content.
|
||||
|
||||
> [!TIP]
|
||||
> Idris requires that these type level functions be [_total_](https://idris2.readthedocs.io/en/latest/tutorial/theorems.html#totality-checking) for them to be expanded at compile time, meaning they are known to the compiler to return a value in constant time for all possible inputs.
|
||||
> Idris requires that these type level functions be [_total_](https://idris2.readthedocs.io/en/latest/tutorial/theorems.html#totality-checking) for them to be expanded at compile time, meaning they are known to the compiler to return a value in finite time for all possible inputs.
|
||||
|
||||
This allows us to produce variadic functions, so long as the number of arguments and their types can be determined from one of the arguments of the function before the variadic portion.
|
||||
|
||||
|
@ -43,7 +46,7 @@ First, we need a data structure to describe our format string. We define the `Fo
|
|||
data Format : Type where
|
||||
||| A slot that should be filled in with a number
|
||||
Number : (next : Format) -> Format
|
||||
||| A slot that should be filled in with a number, padded to a certian number
|
||||
||| A slot that should be filled in with a number, padded to a certain number
|
||||
||| of digits
|
||||
PaddedNumber : (digits : Nat) -> (next : Format) -> Format
|
||||
||| A slot that should be filled in with a string
|
||||
|
@ -188,7 +191,7 @@ printfFmt (Just x) acc = printfFmt' x acc
|
|||
|
||||
### With a Format String
|
||||
|
||||
`printf` is easily defined a simple wrapper, converting the provided string to a `Format` and passing it to `printfFmt`. After accepting the format string argument, we parse a `Maybe Format` from it, providing the parsed value to our `PrintfType` function to calculate the rest of our type signature.
|
||||
`printf` is easily defined as a simple wrapper, converting the provided string to a `Format` and passing it to `printfFmt`. After accepting the format string argument, we parse a `Maybe Format` from it, providing the parsed value to our `PrintfType` function to calculate the rest of our type signature.
|
||||
|
||||
> [!TIP]
|
||||
> The `_` syntax that appears in this function introduces an anonymous hole. When the value of a hole is forced by type checking (resolved), the compiler fills in its value for you. Holes defined with `_` will result in an "unsolved holes" compile error if unresolved.
|
||||
|
@ -260,7 +263,7 @@ failing
|
|||
runtimeFormatString = printf (blackbox "%s %s") "Hello" "World"
|
||||
```
|
||||
|
||||
We can construct a variant of `printf` that will work with runtime format strings by defining a custom, heterogenous list-like structure, and making a fallible version of our `printf` function based on this data structure:
|
||||
We can construct a variant of `printf` that will work with runtime format strings by defining a custom, heterogeneous list-like structure, and making a fallible version of our `printf` function based on this data structure:
|
||||
|
||||
```idris
|
||||
data PrintfInput : Type where
|
||||
|
@ -328,6 +331,9 @@ We've made a fun little function that works much like C's `printf` function or R
|
|||
|
||||
Avoiding using a macro also gives us a lot of neat bonuses, like `printf` automatically playing nice with partial application and currying, with no extra effort on our part.
|
||||
|
||||
> [!NOTE]
|
||||
> The `the (List _) x` construction here in explicit type annotation, needed because of an ambiguity in list-type literals introduced by importing `Data.Vect` for the addenda
|
||||
|
||||
```idris hide
|
||||
-- @@test partial application
|
||||
partialApplication : IO Bool
|
||||
|
@ -336,7 +342,7 @@ partialApplication = do
|
|||
```
|
||||
<div class="unit-test">
|
||||
```idris
|
||||
map (printf "Hello %s") ["Alice", "Bob"] == ["Hello Alice", "Hello Bob"]
|
||||
map (printf "Hello %s") (the (List _) ["Alice", "Bob"]) == ["Hello Alice", "Hello Bob"]
|
||||
```
|
||||
</div>
|
||||
|
||||
|
@ -393,5 +399,47 @@ The need for a bespoke data structure for `PrintfInput` also isn't ideal.
|
|||
|
||||
We will delve in ways to rectify all of these complaints in future entries in this series.
|
||||
|
||||
## Addenda
|
||||
|
||||
### Selecting from a list of format strings with matching type signatures
|
||||
|
||||
As long as the format strings are known at compile time, `PrintfType` will fully expand to a function type signature that contains no references to the string. For example, `PrintfType (parseFormat (unpack "%s"))` expands to `String -> String`.
|
||||
|
||||
We can take advantage of this by indexing a container type by the `PrintfType` of a "template" format string, and then our container can hold any formatting function of compatible type signature, like so:
|
||||
|
||||
```idris
|
||||
formatChoices : Vect 3 (PrintfType (parseFormat (unpack "%s")))
|
||||
formatChoices = [printf "Hello %s!", printf "Nice to meet you %s", printf "\"%s\""]
|
||||
```
|
||||
|
||||
We can then index out a single format function from this container (it doesn't have to be a `Vect`, it could also be a map indexed by the format string itself, for instance):
|
||||
|
||||
```idris hide
|
||||
-- @@test formatChoicesOne
|
||||
formatChoicesOne : IO Bool
|
||||
formatChoicesOne = do
|
||||
pure $
|
||||
```
|
||||
<div class="unit-test">
|
||||
```idris
|
||||
let f = index 1 formatChoices in f "Alice" == "Nice to meet you Alice"
|
||||
```
|
||||
</div>
|
||||
|
||||
Or apply all of the contained formatting functions to the same string:
|
||||
|
||||
```idris hide
|
||||
-- @@test formatChoicesAll
|
||||
formatChoicesAll : IO Bool
|
||||
formatChoicesAll = do
|
||||
pure $
|
||||
```
|
||||
<div class="unit-test">
|
||||
```idris
|
||||
(formatChoices <*> replicate _ "World")
|
||||
== ["Hello World!", "Nice to meet you World", "\"World\""]
|
||||
```
|
||||
</div>
|
||||
|
||||
|
||||
[^1]: Ignoring for a second the special handling for format strings in modern compilers
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# About Me
|
||||
|
||||
## About Me
|
||||
|
||||
My name is Nathan, I'm an engineering psychologist by training, and a systems
|
||||
engineer by trade.
|
||||
|
||||
|
|
11
projects/Markdown/HireMe.md
Normal file
11
projects/Markdown/HireMe.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Hire Me
|
||||
|
||||
## Hire Me!
|
||||
|
||||
I am available for consulting gigs, if your company is new to using Rust, or interested in introducing Rust is into your tech stack, I can train your team on Rust, provide code review, and help with introducing Rust to your existing code base.
|
||||
|
||||
I am also currently looking for a full time role. Professionally, I am most comfortable working in Rust in backend or systems programming roles, I have historically specialized in tooling and consensus protocols, but also have experience working in embedded and with ETL pipelines. I'm also comfortable working with Go, C#, and most functional programming languages like F# or Haskell.
|
||||
|
||||
A PDF copy of my CV can be found [here](https://static.stranger.systems/cv-no-phone.pdf), more information about me can be found on my [About](/about.html) page.
|
||||
|
||||
If you are interested in hiring me for a full time position, or retaining me for a consulting role, please reach out to me at <inquiries@stranger.systems>. My consulting rate varies depending on the scope of the work and the nature of your company, if you are worried about affordability, please do still reach out to me, I'm sure we can work something out.
|
360
resources/cactus.css
Normal file
360
resources/cactus.css
Normal file
|
@ -0,0 +1,360 @@
|
|||
:root{
|
||||
--cactus-text-color--soft: var(--dim-0);
|
||||
--cactus-background-color--strong: var(--bg-1);
|
||||
--cactus-border-color: var(--dim-0);
|
||||
--cactus-box-shadow-color: transparent;
|
||||
--cactus-button-text-color:inherit;
|
||||
--cactus-button-color: var(--bg-2);
|
||||
--cactus-button-color--strong: var(--bg-2);
|
||||
--cactus-button-color--stronger: var(--bg-2);
|
||||
--cactus-login-form-text-color:inherit;
|
||||
--cactus-border-width:1px;
|
||||
--cactus-border-radius:0.4em;
|
||||
--cactus-text-color:inherit;
|
||||
--cactus-background-color: var(--bg-0);
|
||||
--cactus-error-color: var(--red)
|
||||
}
|
||||
.cactus-comment-header a {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.cactus-container{
|
||||
display:flex;
|
||||
width: 100%;
|
||||
max-width: var(--content-width);
|
||||
padding: var(--box-padding-vert) var(--box-padding-horz);
|
||||
border-radius: var(--box-radius);
|
||||
flex-direction:column;
|
||||
gap:1em;
|
||||
color:var(--cactus-text-color);
|
||||
background-color:var(--cactus-background-color)
|
||||
}
|
||||
.cactus-error{
|
||||
padding:.5em;
|
||||
padding-inline-end:1.5em;
|
||||
border:var(--cactus-border-width) solid var(--cactus-error-color);
|
||||
border-radius:var(--cactus-border-radius);
|
||||
position:relative
|
||||
}
|
||||
.cactus-error-close{
|
||||
position:absolute;
|
||||
right:.2em;
|
||||
top:0;
|
||||
color:var(--cactus-button-color);
|
||||
background-color:transparent;
|
||||
border:none
|
||||
}
|
||||
.cactus-error-close:hover:not([disabled]){
|
||||
color:var(--cactus-button-color--strong);
|
||||
cursor:pointer
|
||||
}
|
||||
.cactus-error-close:active:not([disabled]){
|
||||
color:var(--cactus-button-color--stronger);
|
||||
cursor:pointer
|
||||
}
|
||||
.cactus-error-close-icon{
|
||||
inline-size:20px;
|
||||
block-size:20px
|
||||
}
|
||||
.cactus-error-text{
|
||||
color:var(--cactus-error-color);
|
||||
font-weight:700;
|
||||
margin:0
|
||||
}
|
||||
.cactus-editor{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:.5em
|
||||
}
|
||||
.cactus-editor>span{
|
||||
display:flex
|
||||
}
|
||||
.cactus-editor-textarea{
|
||||
display:flex;
|
||||
flex:1;
|
||||
height:9rem;
|
||||
border-radius:var(--cactus-border-radius);
|
||||
border:solid var(--cactus-border-width) var(--green);
|
||||
padding:.5em;
|
||||
box-sizing:content-box;
|
||||
background-color:transparent;
|
||||
color:inherit;
|
||||
font:inherit
|
||||
}
|
||||
.cactus-editor-textarea::placeholder{
|
||||
text-align:center;
|
||||
line-height:8rem;
|
||||
font-size:1.5rem;
|
||||
color:var(--cactus-text-color--soft)
|
||||
}
|
||||
.cactus-editor-below{
|
||||
display:flex;
|
||||
flex-wrap:wrap;
|
||||
gap:.5em;
|
||||
justify-content:flex-end
|
||||
}
|
||||
.cactus-editor-name{
|
||||
display:flex;
|
||||
flex:1
|
||||
}
|
||||
.cactus-editor-name>span{
|
||||
display:flex;
|
||||
flex:1;
|
||||
min-inline-size:20ch;
|
||||
max-inline-size:40ch
|
||||
}
|
||||
.cactus-editor-name>span>input{
|
||||
inline-size:100%;
|
||||
border-radius:var(--cactus-border-radius);
|
||||
border:solid var(--cactus-border-width) var(--green);
|
||||
padding:.5em;
|
||||
background-color:transparent;
|
||||
color:inherit;
|
||||
font-size:inherit
|
||||
}
|
||||
.cactus-editor-name>span>input::placeholder{
|
||||
color:var(--cactus-text-color--soft)
|
||||
}
|
||||
.cactus-editor-buttons{
|
||||
display:flex;
|
||||
gap:.5em
|
||||
}
|
||||
.cactus-matrixdotto-only{
|
||||
align-self:center;
|
||||
text-decoration:none
|
||||
}
|
||||
.cactus-login-form-wrapper{
|
||||
position:fixed;
|
||||
top:0;
|
||||
bottom:0;
|
||||
left:0;
|
||||
right:0;
|
||||
z-index:1;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center
|
||||
}
|
||||
.cactus-login-form{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:2em;
|
||||
padding:2rem;
|
||||
border-radius:var(--cactus-border-radius);
|
||||
background-color:var(--cactus-background-color--strong);
|
||||
color:var(--cactus-login-form-text-color);
|
||||
box-shadow:0 .5em 1em .5em var(--cactus-box-shadow-color);
|
||||
box-sizing:border-box;
|
||||
inline-size:100%;
|
||||
max-inline-size:300px
|
||||
}
|
||||
.cactus-login-close{
|
||||
align-self:flex-end;
|
||||
position:relative;
|
||||
margin:-2em;
|
||||
padding:0;
|
||||
color:var(--cactus-button-color);
|
||||
scale:2;
|
||||
background-color:transparent;
|
||||
border:none
|
||||
}
|
||||
.cactus-login-close:hover:not([disabled]){
|
||||
color:var(--cactus-button-color--strong);
|
||||
cursor:pointer
|
||||
}
|
||||
.cactus-login-close:active:not([disabled]){
|
||||
color:var(--cactus-button-color--stronger);
|
||||
cursor:pointer
|
||||
}
|
||||
.cactus-login-close-icon{
|
||||
inline-size:20px;
|
||||
block-size:20px
|
||||
}
|
||||
.cactus-login-title{
|
||||
align-self:center;
|
||||
font-size:1.17em;
|
||||
font-weight:700;
|
||||
margin:0
|
||||
}
|
||||
.cactus-login-client{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:1em
|
||||
}
|
||||
.cactus-login-client-title{
|
||||
font-size:1em;
|
||||
font-weight:700;
|
||||
margin:0
|
||||
}
|
||||
.cactus-matrixdotto-button{
|
||||
justify-content:center;
|
||||
text-decoration:none
|
||||
}
|
||||
.cactus-login-credentials{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:1em
|
||||
}
|
||||
.cactus-login-credentials-title{
|
||||
font-size:1em;
|
||||
font-weight:700;
|
||||
margin:0
|
||||
}
|
||||
.cactus-login-field{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:.25em
|
||||
}
|
||||
.cactus-login-label{
|
||||
font-size:1em;
|
||||
padding-bottom:.25em;
|
||||
color:var(--cactus-text-color--soft)
|
||||
}
|
||||
.cactus-login-error{
|
||||
margin:0;
|
||||
font-size:.8em;
|
||||
color:var(--cactus-text-color--soft)
|
||||
}
|
||||
.cactus-login-field>input{
|
||||
border-radius:var(--cactus-border-radius);
|
||||
border:solid var(--cactus-border-width) var(--cactus-border-color);
|
||||
padding:.5em;
|
||||
background-color:transparent;
|
||||
color:inherit;
|
||||
font-size:inherit
|
||||
}
|
||||
.cactus-login-field>input::placeholder{
|
||||
color:var(--cactus-text-color--soft)
|
||||
}
|
||||
.cactus-login-credentials-button{
|
||||
justify-content:center
|
||||
}
|
||||
.cactus-comments-container{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:1em
|
||||
}
|
||||
.cactus-comments-list{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:.5em
|
||||
}
|
||||
.cactus-comment{
|
||||
display:flex;
|
||||
flex-direction:row;
|
||||
gap:1em;
|
||||
padding-block-end:.5em;
|
||||
border-block-end: 2px dotted var(--dim-0);
|
||||
}
|
||||
.cactus-comment-avatar{
|
||||
display:flex
|
||||
}
|
||||
.cactus-comment-avatar>*{
|
||||
width:2.5rem;
|
||||
height:2.5rem;
|
||||
border-radius:50%;
|
||||
margin:0
|
||||
}
|
||||
.cactus-comment-avatar-placeholder{
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
background-color:var(--cactus-border-color);
|
||||
color:var(--bg-2);
|
||||
}
|
||||
.cactus-comment-avatar-placeholder:before{
|
||||
content:"?"
|
||||
}
|
||||
.cactus-comment-content{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:.5em
|
||||
}
|
||||
.cactus-comment-header{
|
||||
display:flex;
|
||||
gap:.5em;
|
||||
flex-wrap:wrap
|
||||
}
|
||||
.cactus-comment-displayname{
|
||||
font-weight:700;
|
||||
cursor:pointer;
|
||||
text-decoration:none;
|
||||
color:inherit
|
||||
}
|
||||
.cactus-comment-time{
|
||||
color:var(--cactus-text-color--soft)
|
||||
}
|
||||
.cactus-message-text>:first-child{
|
||||
margin-block-start:0
|
||||
}
|
||||
.cactus-message-text>:last-child{
|
||||
margin-block-end:0
|
||||
}
|
||||
.cactus-message-emote{
|
||||
padding-top:.5em;
|
||||
color:var(--cactus-text-color--soft)
|
||||
}
|
||||
.cactus-message-image{
|
||||
max-width:100%;
|
||||
height:auto
|
||||
}
|
||||
.cactus-message-file{
|
||||
line-height:3em;
|
||||
margin-left:1em
|
||||
}
|
||||
.cactus-message-video{
|
||||
max-width:100%
|
||||
}
|
||||
.cactus-button{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
padding-block:.6em;
|
||||
padding-inline:1em;
|
||||
background-color:var(--cactus-button-color);
|
||||
font-weight:700;
|
||||
border-radius:var(--cactus-border-radius);
|
||||
color:var(--cactus-button-text-color);
|
||||
font-size:inherit;
|
||||
border:none
|
||||
}
|
||||
.cactus-button:hover:not([disabled]){
|
||||
background-color:var(--cactus-button-color--strong);
|
||||
cursor:pointer
|
||||
}
|
||||
.cactus-button:active:not([disabled]){
|
||||
background-color:var(--cactus-button-color--stronger);
|
||||
cursor:pointer
|
||||
}
|
||||
.cactus-view-more{
|
||||
display:flex;
|
||||
justify-content:center
|
||||
}
|
||||
.spinner{
|
||||
align-self:center;
|
||||
width:2em;
|
||||
height:2em;
|
||||
display:flex;
|
||||
gap:.3em
|
||||
}
|
||||
.spinner>div{
|
||||
flex:1;
|
||||
background-color:var(--cactus-border-color);
|
||||
animation:sk-stretchdelay 2.4s ease-in-out infinite
|
||||
}
|
||||
.spinner .rect2{
|
||||
animation-delay:-2.2s
|
||||
}
|
||||
.spinner .rect3{
|
||||
animation-delay:-2s
|
||||
}
|
||||
.spinner .rect4{
|
||||
animation-delay:-1.8s
|
||||
}
|
||||
@keyframes sk-stretchdelay{
|
||||
0%,40%,to{
|
||||
transform:scaleY(.4)
|
||||
}
|
||||
20%{
|
||||
transform:scaleY(1)
|
||||
}
|
||||
}
|
||||
/*# sourceMappingURL=/style.css.map */
|
|
@ -69,9 +69,20 @@ a:visited {
|
|||
.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-body h2 {
|
||||
color: var(--red);
|
||||
background-color: var(--bg-1);
|
||||
}
|
||||
|
||||
.post-body h3 {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.post-body h4 {
|
||||
color: var(--violet);
|
||||
}
|
||||
|
||||
.post-info > *, .series-info > *, .header-links > * {
|
||||
background-color: var(--bg-2);
|
||||
}
|
||||
|
@ -91,6 +102,23 @@ blockquote {
|
|||
.unit-test .bx-info-circle {
|
||||
color: var(--yellow);
|
||||
}
|
||||
footer {
|
||||
background-color: var(--bg-0);
|
||||
}
|
||||
footer > div {
|
||||
background-color: var(--bg-1);
|
||||
}
|
||||
.footer-link {
|
||||
background-color: var(--bg-2);
|
||||
}
|
||||
|
||||
/* Formatting for special pages */
|
||||
.special-post h2 {
|
||||
color: var(--green);
|
||||
}
|
||||
.special-post h3 {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
/* Colorization for idris code blocks */
|
||||
code {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
--box-padding-horz: 1rem;
|
||||
--box-margin-vert: 0.5rem;
|
||||
--box-margin-horz: 0.5rem;
|
||||
--footer-padding: 0.25rem;
|
||||
--box-gap: 0.5rem;
|
||||
--box-radius: 1rem;
|
||||
}
|
||||
|
@ -78,11 +79,18 @@ body, .post, .series {
|
|||
font-size: 1.1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: var(--box-margin-vert);
|
||||
justify-content: center;
|
||||
}
|
||||
.header-links > a > span, .post-series-tag > a > span, .post-tag > a > span {
|
||||
.header-links > a > span,
|
||||
.post-series-tag > a > span,
|
||||
.post-tag > a > span,
|
||||
.footer-link > a > div {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.header-links > a, .post-series-tag > a, .post-tag > a {
|
||||
.header-links > a,
|
||||
.post-series-tag > a,
|
||||
.post-tag > a,
|
||||
.footer-link > a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
@ -121,6 +129,13 @@ body, .post, .series {
|
|||
.post-body h2, .post-body h3, .post-body h4 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.post-body h2 {
|
||||
padding: var(--box-margin-vert) var(--box-margin-horz);
|
||||
border-radius: var(--box-radius);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.post-blurbs, .series-blurbs, .series-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -130,7 +145,7 @@ body, .post, .series {
|
|||
padding: var(--box-padding-vert) var(--box-padding-horz);
|
||||
border-radius: var(--box-radius);
|
||||
}
|
||||
.post-blurb, .series-list-blurb {
|
||||
.post-blurb, .series-list-blurb, .series-list-blurb-title {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: var(--box-radius);
|
||||
|
@ -174,7 +189,6 @@ blockquote {
|
|||
.tag-blurb-links {
|
||||
display: block;
|
||||
border-radius: var(--box-radius);
|
||||
border-radius: var(--box-radius);
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
gap: var(--box-gap);
|
||||
|
@ -197,6 +211,69 @@ blockquote {
|
|||
.tag-blurb-title {
|
||||
margin-top: var(--box-margin-vert);
|
||||
margin-bottom: 0;
|
||||
font-size: 1.5em;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Style the footer */
|
||||
footer {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: stretch;
|
||||
gap: var(--box-gap);
|
||||
max-width: var(--content-width);
|
||||
width: 100%;
|
||||
border-radius: var(--box-radius);
|
||||
padding: var(--box-padding-vert) var(--box-padding-horz);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
footer > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
flex: 1;
|
||||
gap: var(--box-gap);
|
||||
border-radius: var(--box-radius);
|
||||
padding: var(--footer-padding);
|
||||
}
|
||||
.footer-title {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
footer i {
|
||||
font-size: 1rem;
|
||||
line-height: 0.8rem;
|
||||
}
|
||||
.footer-link > a {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
.footer-link > img {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
margin: 0.1rem;
|
||||
}
|
||||
.footer-links {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--box-gap);
|
||||
border-radius: var(--box-radius);
|
||||
padding: var(--footer-padding);
|
||||
}
|
||||
.footer-link {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-content: center;
|
||||
border-radius: 0.25rem;
|
||||
padding: var(--footer-padding);
|
||||
}
|
||||
|
||||
/* Formatting for special pages */
|
||||
.special-post h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
.special-post h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue