.Str;
+ my $out = pygment $code, $lang;
+ ## Mangle the html to meet our needs
+ # Delete the existing div and construct a
inside the
+ $out ~~ s:g/'<' \/? 'div' <-[>]>* '>'//;
+ $out ~~ s:g/''//;
+ $out ~~ s:g/'
'/<\/code><\/pre>/;
+ # Rename the classes to match our needs
+ $out ~~ s:g/'span class="' (\w+) '"'/span class="hl-{$/[0].lc}"/;
+ $text = $match.replace-with: $out;
+ }
+ $text
+}
diff --git a/lib/Render/Head.rakumod b/lib/Render/Head.rakumod
new file mode 100644
index 0000000..e7eb435
--- /dev/null
+++ b/lib/Render/Head.rakumod
@@ -0,0 +1,68 @@
+use v6.e.PREVIEW;
+unit module Render::Head;
+
+use HTML::Functional;
+
+use Render::Util;
+use DB::BlogMeta;
+
+sub generate-head(BlogMeta:D $meta, $title?, $description?) is export {
+ head [
+ meta :charset;
+ meta :name, :content;
+ meta :author :content;
+ do if $title ~~ Str:D {
+ title "$title — {$meta.title}";
+ } else {
+ title $meta.title;
+ }
+ # Add description, if one exists
+ optl $description ~~ Str:D, -> {meta :description :content($description)};
+ # Preconnect to all our resource sources
+ link :rel :href;
+ link :rel :href;
+ link :rel :href :crossorigin;
+ link :rel :href;
+ # Load fonts, Iosevka for code, Open Sans for content, and boxicons for
+ # icons
+ link :rel,
+ :href;
+ link :rel,
+ :href;
+ link :rel,
+ :href;
+ # Link our style sheets
+ link :rel,
+ :href;
+ link :rel,
+ :href;
+ link :rel,
+ :href;
+ ]
+}
+
+sub site-header(BlogMeta:D $meta) is export {
+ sub header-link($name, $path, $icon) {
+ a :href("$path"), [
+ icon $icon;
+ ' ';
+ span $name;
+ ]
+ }
+ header :class, [
+ div :class, [
+ # TODO: Use a real image here
+ $meta.title
+ ];
+ div :class, [
+ $meta.tagline
+ ];
+ div :class, [
+ header-link 'Index', '/index.html', 'home';
+ header-link 'Archive', '/archive.html', 'archive';
+ header-link 'Tags', '/tags.html', 'purchase-tag-alt';
+ header-link 'About', '/about.html', 'info-circle';
+ header-link 'Feed', '/atom.xml', 'rss';
+ ];
+ ]
+}
diff --git a/lib/Render/Post.rakumod b/lib/Render/Post.rakumod
new file mode 100644
index 0000000..826582e
--- /dev/null
+++ b/lib/Render/Post.rakumod
@@ -0,0 +1,100 @@
+use v6.e.PREVIEW;
+unit module Render::Post;
+
+use Render::Util;
+use DB::Post;
+
+use HTML::Functional;
+
+sub post-date(Post:D $post) is export {
+ my $datetime = $post.posted-at;
+ my $timestamp = sprintf(
+ "%s %02d:%02d%s",
+ $datetime.yyyy-mm-dd,
+ ($datetime.hour % 12) || 12,
+ $datetime.minute,
+ $datetime.hour < 12 ?? 'am' !! 'pm'
+ );
+
+ div :class, :title("Posted At $timestamp"), [
+ icon 'time';
+ ' ';
+ $timestamp
+ ]
+}
+
+sub post-edit(Post:D $post) is export {
+ return [] unless $post.edited-at.elems;
+ my $datetime = $post.edited-at.max;
+ my $timestamp = sprintf(
+ "%s %02d:%02d%s",
+ $datetime.yyyy-mm-dd,
+ ($datetime.hour % 12) || 12,
+ $datetime.minute,
+ $datetime.hour < 12 ?? 'am' !! 'pm'
+ );
+
+ div :class, :title("Last Edited At $timestamp"), [
+ icon 'edit';
+ ' ';
+ $timestamp
+ ]
+}
+
+sub post-read-time(Post:D $post) is export {
+ my ($slow, $average, $fast) = $post.readtimes;
+ div :class, :title, [
+ icon 'timer';
+ ' ';
+ mins-to-string $slow;
+ ' ';
+ '/';
+ ' ';
+ mins-to-string $average;
+ ' ';
+ '/';
+ ' ';
+ mins-to-string $fast;
+ ]
+}
+
+sub post-tag(Str:D $tag) is export {
+ span :class, [
+ a :href("/tags/$tag.html"), [
+ icon 'hash';
+ $tag;
+ ]
+ ]
+}
+
+sub post-tags(Post:D $post) is export {
+ my @tags = $post.tags.sort;
+ if @tags {
+ @tags.=map(*.&post-tag);
+ div :class, [
+ icon 'purchase-tag-alt';
+ ' ';
+ intersperse(', ', @tags);
+ ]
+ } else {
+ []
+ }
+}
+
+sub post-info(Post:D $post) is export {
+ div :class, [
+ post-date $post;
+ post-edit $post;
+ post-read-time $post;
+ post-tags $post;
+ ];
+}
+
+sub post-header(Post:D $post) is export {
+ header :class, [
+ div :class, [
+ h1 $post.title;
+ ];
+ post-info $post;
+ ]
+}
diff --git a/lib/Render/Util.rakumod b/lib/Render/Util.rakumod
new file mode 100644
index 0000000..05630a7
--- /dev/null
+++ b/lib/Render/Util.rakumod
@@ -0,0 +1,54 @@
+use v6.e.PREVIEW;
+unit module Render::Util;
+
+use DB::Post;
+
+use HTML::Functional;
+
+sub opt($test, $item) is export {
+ if $test {
+ $item
+ } else {
+ []
+ }
+}
+
+sub optl($test, &item) is export {
+ if $test {
+ item
+ } else {
+ []
+ }
+}
+
+#| Link to the post by the primary slug, if there is one, linking to it by id
+#| otherwise
+sub post-link(Int:D $id, Post:D $post --> Str:D) is export {
+ my @slugs = $post.all-slugs;
+ if @slugs {
+ "/posts/by-slug/{@slugs[*-1]}.html"
+ } else {
+ "/posts/by-id/$id.html"
+ }
+}
+
+sub icon($icon) is export {
+ i(:class("bx bx-$icon"))
+}
+
+sub mins-to-string($mins) is export {
+ if $mins < 60 {
+ $mins.Str ~ "m"
+ } else {
+ my $h = $mins div 60;
+ my $m = $mins mod 60;
+ $h.Str ~ "h" ~ $m.Str ~ "m"
+ }
+}
+
+sub intersperse (\element, +list) is export {
+ gather for list {
+ FIRST .take, next;
+ take slip element, $_;
+ }
+}
diff --git a/projects/Markdown/2025/01-Jan/AdventOfBugs.md b/projects/Markdown/2025/01-Jan/AdventOfBugs.md
new file mode 100644
index 0000000..3242738
--- /dev/null
+++ b/projects/Markdown/2025/01-Jan/AdventOfBugs.md
@@ -0,0 +1,98 @@
+# Advent of bugs
+
+Near the start of january, after bumping into some ecosystem issues and lack of
+a personal support library while working on the 2024
+[Advent of Code](https://adventofcode.com/) in Idris, I started working on a
+project to solve all of the Advent of Code problems from all the years in a
+single massive entirely literate Idris project and publish it as an mdbook. I'm
+calling it
+[Idris 2 by Extremely Contrived](https://static.stranger.systems/idris-by-contrived-example/)
+example.
+
+## The Good
+
+This has been an amazingly fun project so far. AoC problems are nice and bite
+sized, giving really good material to work with for incrementally introducing
+more and more complex concepts. I have been following a sort of "weirdness
+budget" for each day's solutions, letting myself build on the already
+established weirdness from previous days. So far I'm 13 days in, and I've
+already had excuses to introduce all sorts of fun concepts, effects, dependent
+pattern matching, indexed type families, and refinement types, just to name a
+few.
+
+Functional programming languages are a lot of fun to model puzzle problems in to
+start with, but the new design space afforded by dependent typing offers a new,
+wonderfully fun challenge of figuring out just what to show off while I'm
+solving the puzzle. There have already been a few problems that I've wound up
+spending a few days on just tweaking and refining until I was pleased with the
+balance between weirdness expenditure and showing off what I want to show off in
+a reasonably approachable way.
+
+A couple of personal favorites of mine so far have been the
+[JSON parser](https://static.stranger.systems/idris-by-contrived-example/Parser/JSON.html),
+and the
+[`Digits` views](https://static.stranger.systems/idris-by-contrived-example/Util/Eff.html).
+The JSON parser is written in a bespoke mini-library for doing effectively
+parsing that I created just for this project, and refining the library to
+optimize for readability of the parsers written in it was an absolute joy. The
+digits views allowing pattern matching on normal integers as if they were lists
+of digits was less fun to write[^1], they are amazingly fun to use.
+
+## The Bad
+
+I've had to write a lot of support code already. I can't really fault the
+language, its a pretty new language in its pre-1.0 stage, in a pretty niche
+area, dependent types are still quite scary to the majority of programmers, and
+ecosystem improvement was a major goal of the project going in. I have already
+had to write several "basic" data structures, and there's no sign the need to do
+so will let up anytime soon[^6].
+
+Honestly the most painful part has been the lack of support for Idris in
+external tooling. Essentially every syntax highlighting library has _completely
+ass_ support for Idris. I managed to eventually sneak proper semantic
+highlighting into mdbook, it even plays nice with mdbook themes, but I had to
+commit horrible, _horrible_ crimes to get this working[^2].
+
+## The Ugly
+
+This isn't exactly a complaint, after all, ecosystem improvement was a goal
+after all, but I've already run into 2 or 3 compiler bugs, a bug in the `pack`
+package manager, and a weird behavior in katla that's a _maybe_ bug.
+
+I busted the part of the compiler that automatically inserts `force` and `delay`
+calls to make interaction between lazy and strict code Just Work™:
+[idris-lang/Idris2#3461](https://github.com/idris-lang/Idris2/issues/3461). I
+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 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
+[Structures](https://git.sr.ht/~thatonelutenist/Structures) package, checking in
+a new data structure motivated by my advent project, completely breaking the
+automated docs generation:
+[stefan-hoeck/idris2-pack!319](https://github.com/stefan-hoeck/idris2-pack/pull/319).
+This one was especially wild to me, with how popular literate programs are in
+the community, I would have never expected to be the first person to try this.
+
+## Looking Forward
+
+Despite the challenges, this has been a lovely experience so far. I greatly look
+forward to pressing through to completion, whatever it may bring, and have a
+trophy case full of bugs identified/fixed and new libraries to bring to the
+ecosystem.
+
+[^1]: While perfectly understandable, it's not reasonable to expect the compiler
+ to be able to reason about primitive "machine" integers like this on its
+ own, needing to resort to so much `believe_me` on this one did make me a bit
+ sad
+
+[^6]: Well well well, if it isn't the consequences of my actions
+
+[^2]: Do not look at the `build-book` script if you value your sanity
+
+[^8]: Before anyone gets upset here, despite the name, the termination checker
+ doesn't actually solve the halting problem. It only automatically accepts a
+ 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.
diff --git a/projects/Markdown/RustPosting.md b/projects/Markdown/RustPosting.md
new file mode 100644
index 0000000..632d2f1
--- /dev/null
+++ b/projects/Markdown/RustPosting.md
@@ -0,0 +1,205 @@
+# Rustposting
+
+Some example code with some potential problem characters:
+
+```rust
+let newline_string = "hello \n world";
+let thing = *newline_string;
+```
+
+Here is some example rust code:
+
+```rust
+fn main() {
+ // Statements here are executed when the compiled binary is called.
+
+ // Print text to the console
+ println!("Hello World!");
+}
+```
+
+And a slightly less trivial example:
+
+```rust
+fn main() {
+ // Variables can be type annotated.
+ let logical: bool = true;
+
+ let a_float: f64 = 1.0; // Regular annotation
+ let an_integer = 5i32; // Suffix annotation
+
+ // Or a default will be used.
+ let default_float = 3.0; //
+ let default_integer = 7; //
+
+ // A type can also be inferred from context.
+ let mut inferred_type = 12; // Type i64 is inferred from another line.
+ inferred_type = 4294967296i64;
+
+ // A mutable variable's value can be changed.
+ let mut mutable = 12; // Mutable
+ mutable = 21;
+
+ // Error! The type of a variable can't be changed.
+ mutable = true;
+
+ // Variables can be overwritten with shadowing.
+ let mutable = true;
+
+ /* Compound types - Array and Tuple */
+
+ // Array signature consists of Type T and length as [T; length].
+ let my_array: [i32; 5] = [1, 2, 3, 4, 5];
+
+ // Tuple is a collection of values of different types
+ // and is constructed using parentheses ().
+ let my_tuple = (5u32, 1u8, true, -5.04f32);
+}
+```
+
+Toss in some type definitions to
+
+```rust
+#[derive(Debug)]
+struct Person {
+ name: String,
+ age: u8,
+}
+
+// A unit struct
+struct Unit;
+
+// A tuple struct
+struct Pair(i32, f32);
+
+enum WebEvent {
+ // An variant may either be
+ PageLoad,
+ PageUnload,
+ // like tuple structs,
+ KeyPress(char),
+ Paste(String),
+ // or c-like structures.
+ Click { x: i64, y: i64 },
+}
+
+struct Point {
+ x: f64,
+ y: f64,
+}
+
+// Implementation block, all associated functions & methods go in here
+impl Point {
+ // This is an "associated function" because this function is associated with
+ // a particular type, that is, Point.
+ //
+ // Associated functions don't need to be called with an instance.
+ // These functions are generally used like constructors.
+ fn origin() -> Point {
+ Point { x: 0.0, y: 0.0 }
+ }
+
+ // Another associated function, taking two arguments:
+ fn new(x: f64, y: f64) -> Point {
+ Point { x: x, y: y }
+ }
+}
+
+```
+
+Modules and imports
+
+```rust
+#![allow(unused_variables)]
+
+use deeply::nested::function as other_function;
+use std::fs::File;
+
+fn function() {
+ println!("called function()");
+}
+
+mod deeply {
+ pub mod nested {
+ pub fn function() {
+ println!("called deeply::nested::function()");
+ }
+ }
+}
+
+struct Val {
+ val: f64,
+}
+
+struct GenVal {
+ gen_val: T,
+}
+
+// impl of Val
+impl Val {
+ fn value(&self) -> &f64 {
+ &self.val
+ }
+}
+
+impl GenVal {
+ fn value(&self) -> &T {
+ &self.gen_val
+ }
+}
+
+let a = Box::new(5i32);
+
+macro_rules! say_hello {
+ () => {
+ // The macro will expand into the contents of this block.
+ println!("Hello!")
+ };
+}
+
+macro_rules! calculate {
+ (eval $e:expr) => {
+ {
+ let val: usize = $e; // Force types to be unsigned integers
+ println!("{} = {}", stringify!{$e}, val);
+ }
+ };
+}
+
+fn give_adult(drink: Option<&str>) {
+ // Specify a course of action for each case.
+ match drink {
+ Some("lemonade") => println!("Yuck! Too sugary."),
+ Some(inner) => println!("{}? How nice.", inner),
+ None => println!("No drink? Oh well."),
+ }
+}
+
+impl Person {
+
+ // Gets the area code of the phone number of the person's job, if it exists.
+ fn work_phone_area_code(&self) -> Option {
+ // It would take a lot more code - try writing it yourself and see which
+ // is easier.
+ self.job?.phone_number?.area_code
+ }
+}
+
+#[cfg(target_family = "unix")]
+#[link(name = "m")]
+extern {
+ // this is a foreign function
+ // that computes the square root of a single precision complex number
+ fn csqrtf(z: Complex) -> Complex;
+
+ fn ccosf(z: Complex) -> Complex;
+}
+
+fn main() {
+ let raw_p: *const u32 = &10;
+
+ unsafe {
+ assert!(*raw_p == 10);
+ }
+}
+```
diff --git a/resources/code.css b/resources/code.css
index b9d9c48..18edce7 100644
--- a/resources/code.css
+++ b/resources/code.css
@@ -11,7 +11,6 @@ code {
/* Styling for fenced code blocks */
pre > code {
display: block;
- white-space: pre-wrap;
padding: 1rem;
border-radius: 0.55rem / 0.5rem;
word-wrap: normal;
diff --git a/resources/colors.css b/resources/colors.css
index 54e6cc5..5974f21 100644
--- a/resources/colors.css
+++ b/resources/colors.css
@@ -53,23 +53,29 @@ a:visited {
.site-tagline {
color: var(--dim-0);
}
-.post-body, .post-header, .post-blurbs {
+.post-body, .post-header, .post-blurbs, .tags, .tags .tag-blurb-post {
background-color: var(--bg-0);
}
-.post-blurb {
+.post-blurb, .tags .tag-blurb {
background-color: var(--bg-1);
}
-.post-title, .post-blurbs > h1 {
+:not(.tags) .tag-blurb {
+ background-color: var(--bg-0);
+}
+:not(.tags) .tag-blurb-post {
+ background-color: var(--bg-1);
+}
+.post-title, .post-blurbs h1 {
color: var(--green);
}
-.post-body > h2, .post-body > h3, .post-body > h4 {
+.post-body h2, .post-body h3, .post-body h4 {
color: var(--fg-1);
}
blockquote {
background-color: var(--bg-1);
}
-/* Colorization for code blocks */
+/* Colorization for idris code blocks */
code {
color: var(--code-fg-0);
background-color: var(--code-bg-0);
@@ -96,3 +102,35 @@ code {
.hl-data {
color: var(--code-red);
}
+
+/* Colorization for pygments code blocks */
+.hl-kd, .hl-k, .hl-kc, .hl-bp {
+ color: var(--code-green);
+}
+.hl-n, .hl-nn {
+ color: var(--code-violet);
+}
+.hl-s, .hl-se {
+ color: var(--code-cyan);
+}
+.hl-nf, .hl-fm {
+ color: var(--code-blue);
+}
+.hl-c1, .hl-cm {
+ color: var(--code-dim-0);
+}
+.hl-mf, .hl-mi {
+ color: var(--code-magenta);
+}
+.hl-kt, .hl-nb, .hl-nc {
+ color: var(--code-orange);
+}
+.hl-cp {
+ color: var(--code-red);
+}
+.hl-se {
+ font-style: italic;
+}
+.hl-fm, .hl-k, .hl-o, .hl-kp {
+ font-weight: bold;
+}
diff --git a/resources/main.css b/resources/main.css
index fad0412..4b54d5e 100644
--- a/resources/main.css
+++ b/resources/main.css
@@ -2,6 +2,7 @@
font-family: "Open Sans", sans-serif, serif;
/* Variables */
--content-width: 60rem;
+ --blurb-width: 45%;
--header-width: 35rem;
--box-padding-vert: 1rem;
--box-padding-horz: 1rem;
@@ -18,6 +19,13 @@
}
}
+/* slightly larger than blurb-width to account for padding/margins */
+@media screen and (max-width: 40rem) {
+ :root {
+ --blurb-width: 100%;
+ }
+}
+
/* Main Body and Post Flexboxs */
body, .post {
display: flex;
@@ -25,6 +33,9 @@ body, .post {
align-items: center;
gap: var(--box-gap);
}
+.post {
+ width: 100%;
+}
/* Style the site header */
.site-header {
@@ -75,11 +86,11 @@ body, .post {
border-radius: var(--box-radius);
box-sizing: border-box;
}
-.post-body > p {
+.post-body p {
margin: auto var(--box-margin-horz);
align-self: stretch;
}
-.post-title > h1 {
+.post-title h1 {
margin-top: 0px;
margin-bottom: 0px;
}
@@ -94,7 +105,7 @@ body, .post {
.post-read-time {
text-decoration: underline dotted;
}
-.post-body > h2, .post-body > h3, .post-body > h4 {
+.post-body h2, .post-body h3, .post-body h4 {
text-align: center;
}
.post-blurbs {
@@ -124,3 +135,56 @@ blockquote {
border-radius: var(--box-radius);
padding: var(--box-padding-vert) var(--box-padding-horz);
}
+
+/* Style the tags blurbs and page */
+.tags {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: var(--box-gap);
+ max-width: var(--content-width);
+ /* min-width: var(--blurb-width); */
+ padding: var(--box-padding-vert) var(--box-padding-horz);
+ border-radius: var(--box-radius);
+ box-sizing: border-box;
+}
+.tag-blurb {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: var(--box-gap);
+ border-radius: var(--box-radius);
+ box-sizing: border-box;
+}
+.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);
+ align-items: stretch;
+ box-sizing: border-box;
+ gap: var(--box-gap);
+ padding: var(--box-padding-vert) var(--box-padding-horz);
+}
+.tag-blurb-post {
+ font-size: 0.8rem;
+ width: var(--blurb-width);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: var(--box-gap);
+ box-sizing: border-box;
+ flex-shrink: 1;
+ flex-grow: 1;
+ border-radius: var(--box-radius);
+ padding: var(--box-padding-vert) var(--box-padding-horz);
+}
+.tag-blurb-title {
+ margin-top: var(--box-margin-vert);
+ margin-bottom: 0;
+ font-size: 1.5em;
+ font-weight: bold;
+}