From f1515aab06a2373223be3b0e42efff103421a8ce Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Thu, 6 Feb 2025 23:45:26 -0500 Subject: [PATCH 01/13] Initial tags support --- lib/Atom.rakumod | 6 ++++++ lib/Config.rakumod | 31 ++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/lib/Atom.rakumod b/lib/Atom.rakumod index 0c0ad7b..0efd238 100644 --- a/lib/Atom.rakumod +++ b/lib/Atom.rakumod @@ -41,6 +41,12 @@ sub post-to-item(BlogMeta:D $meta, Int:D $id, Post:D $post --> XML::Element) { XML::Element.new(:name, :attribs({:href($link), :rel})); $xml.append: XML::Element.new(:name, :nodes([$desc])) if $desc; + if $post.tags { + for $post.tags -> $tag { + $xml.append: + XML::Element.new(:name, :attribs({:term($tag)})); + } + } $xml } diff --git a/lib/Config.rakumod b/lib/Config.rakumod index 5e043fc..85cab41 100644 --- a/lib/Config.rakumod +++ b/lib/Config.rakumod @@ -149,12 +149,41 @@ method post-read-time(Post:D $post) { ] } +method post-tag(Str:D $tag) { + span :class, [ + a :href("/tags/$tag.html"), [ + icon 'hash'; + $tag; + ] + ] +} + +method post-tags(Post:D $post){ + sub intersperse (\element, +list) { + gather for list { + FIRST .take, next; + take slip element, $_; + } + } + my @tags = $post.tags; + if @tags { + @tags.=map(-> $tag {self.post-tag($tag)}); + div :class, [ + icon 'purchase-tag'; + ' '; + intersperse(', ', @tags); + ] + } else { + [] + } +} + method post-info(Post:D $post) { div :class, [ self.post-date: $post; self.post-edit: $post; self.post-read-time: $post; - # TODO: Add tags once we have support for that + self.post-tags: $post; ]; } From 20ecef3b3f7e606dfb4abdddc76f2a0305122146 Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Fri, 7 Feb 2025 01:43:52 -0500 Subject: [PATCH 02/13] Generate tags page --- lib/Config.rakumod | 73 +++++++++++++++++++++++++++++++++++++++++++- lib/DB.rakumod | 3 ++ resources/colors.css | 4 +-- resources/main.css | 61 ++++++++++++++++++++++++++++++++++++ 4 files changed, 138 insertions(+), 3 deletions(-) diff --git a/lib/Config.rakumod b/lib/Config.rakumod index 85cab41..58d7b7d 100644 --- a/lib/Config.rakumod +++ b/lib/Config.rakumod @@ -69,6 +69,13 @@ method site-header(BlogMeta:D $meta) { 'Archive'; ]; ]; + a :href, [ + icon 'purchase-tag-alt'; + ' '; + span [ + 'Tags'; + ]; + ]; a :href, [ icon 'info-circle'; ' '; @@ -169,7 +176,7 @@ method post-tags(Post:D $post){ if @tags { @tags.=map(-> $tag {self.post-tag($tag)}); div :class, [ - icon 'purchase-tag'; + icon 'purchase-tag-alt'; ' '; intersperse(', ', @tags); ] @@ -299,6 +306,70 @@ method generate-archive($db) { "$html" } +method generate-tag-blurb($db, $tag, $limit?) { + sub post-to-link($id, $post) { + my $desc = $post.description; + my @slugs = $post.all-slugs; + # Use the primary slug if there is one, the id if there isn't + my $link = do if @slugs.elems { + "/posts/by-slug/{@slugs[*-1]}.html" + } else { + "/posts/by-id/$id.html" + } + div :class, [ + div :class, [ + a :href($link), span [ + h3 $post.title; + ]; + self.post-info: $post; + if $desc ~~ Str:D { + div :class, [ + p $post.description; + ]; + } else { + [] + } + ]; + ] + } + my @posts = $db.sorted-posts.grep(-> $a { $tag (elem) $a.value.tags }); + if $limit { + @posts.=head($limit); + } + if @posts { + div :class, [ + span :class, [ + a :href("/tags/$tag.html"), [ + icon 'hash'; + $tag; + ]; + ]; + div :class, + @posts.map(-> $a {post-to-link $a.key, $a.value}); + ] + } else { + [] + } +} + +method generate-tags-page($db, @tags) { + my $head = self.generate-head(Nil, $db.meta); + my $body = body [ + self.site-header: $db.meta; + div :class, [ + h1 "Tags"; + ], @tags.map(-> $tag {self.generate-tag-blurb($db, $tag, 4)}); + ]; + + my $html = + html :lang, [ + $head, + $body + ]; + + "$html" +} + sub icon($icon) { i(:class("bx bx-$icon")) } diff --git a/lib/DB.rakumod b/lib/DB.rakumod index e56ec67..87614e4 100644 --- a/lib/DB.rakumod +++ b/lib/DB.rakumod @@ -105,6 +105,9 @@ class PostDB { my $about-path = $out-dir.add('about.html'); $about-path.unlink if $about-path.l; $by-id.add("{$!meta.about-id}.html").symlink: $about-path; + # Generate the tags pages + my @tags = %!posts.values.map(*.tags).flat.unique; + $out-dir.add('tags.html').spurt: $config.generate-tags-page(self, @tags); # Render the rss/atom feed my $atom-path = $out-dir.add('atom.xml'); my $atom = posts-to-atom self; diff --git a/resources/colors.css b/resources/colors.css index 54e6cc5..fb8f588 100644 --- a/resources/colors.css +++ b/resources/colors.css @@ -53,10 +53,10 @@ a:visited { .site-tagline { color: var(--dim-0); } -.post-body, .post-header, .post-blurbs { +.post-body, .post-header, .post-blurbs, .tags, .tag-blurb-post { background-color: var(--bg-0); } -.post-blurb { +.post-blurb, .tag-blurb { background-color: var(--bg-1); } .post-title, .post-blurbs > h1 { diff --git a/resources/main.css b/resources/main.css index fad0412..5958cec 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; @@ -124,3 +132,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; +} From 2072f88711ecde661c4eca9b04e5836724b104a8 Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Fri, 7 Feb 2025 01:58:44 -0500 Subject: [PATCH 03/13] Generate pages for individual tags --- lib/Config.rakumod | 16 ++++++++++++++++ lib/DB.rakumod | 6 ++++++ resources/colors.css | 14 ++++++++++---- resources/main.css | 6 +++--- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/lib/Config.rakumod b/lib/Config.rakumod index 58d7b7d..6fd7525 100644 --- a/lib/Config.rakumod +++ b/lib/Config.rakumod @@ -370,6 +370,22 @@ method generate-tags-page($db, @tags) { "$html" } +method generate-tag-page($db, $tag) { + my $head = self.generate-head(Nil, $db.meta); + my $body = body [ + self.site-header: $db.meta; + self.generate-tag-blurb($db, $tag, 4); + ]; + + my $html = + html :lang, [ + $head, + $body + ]; + + "$html" +} + sub icon($icon) { i(:class("bx bx-$icon")) } diff --git a/lib/DB.rakumod b/lib/DB.rakumod index 87614e4..88a1140 100644 --- a/lib/DB.rakumod +++ b/lib/DB.rakumod @@ -108,6 +108,12 @@ class PostDB { # Generate the tags pages my @tags = %!posts.values.map(*.tags).flat.unique; $out-dir.add('tags.html').spurt: $config.generate-tags-page(self, @tags); + my $tags-dir = $out-dir.add('tags/'); + mkdir $tags-dir unless $tags-dir.e; + for @tags -> $tag { + $tags-dir.add("$tag.html").spurt: + $config.generate-tag-page(self, $tag); + } # Render the rss/atom feed my $atom-path = $out-dir.add('atom.xml'); my $atom = posts-to-atom self; diff --git a/resources/colors.css b/resources/colors.css index fb8f588..bfd642f 100644 --- a/resources/colors.css +++ b/resources/colors.css @@ -53,16 +53,22 @@ a:visited { .site-tagline { color: var(--dim-0); } -.post-body, .post-header, .post-blurbs, .tags, .tag-blurb-post { +.post-body, .post-header, .post-blurbs, .tags, .tags .tag-blurb-post { background-color: var(--bg-0); } -.post-blurb, .tag-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 { diff --git a/resources/main.css b/resources/main.css index 5958cec..a23b136 100644 --- a/resources/main.css +++ b/resources/main.css @@ -83,11 +83,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; } @@ -102,7 +102,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 { From 07a9b29c0008ae9dd250b645c58f4883c21189a0 Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Fri, 7 Feb 2025 02:01:01 -0500 Subject: [PATCH 04/13] Add meta tag to my new blog post --- db/meta.json | 2 +- db/posts/0.json | 6 +++--- db/posts/1.json | 11 ++++++----- db/posts/2.json | 12 ++++++------ db/posts/3.json | 8 ++++---- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/db/meta.json b/db/meta.json index 664acff..eeef098 100644 --- a/db/meta.json +++ b/db/meta.json @@ -1,7 +1,7 @@ { "title": "Stranger Systems", - "placeholder-id": 0, "base-url": "https://www.stranger.systems", + "placeholder-id": 0, "tagline": "Making software better by making it weird", "about-id": 2 } \ No newline at end of file diff --git a/db/posts/0.json b/db/posts/0.json index bf292ac..6f07b26 100644 --- a/db/posts/0.json +++ b/db/posts/0.json @@ -1,12 +1,12 @@ { + "source": "/dev/null", + "hidden": true, "slugs": [ ], - "source": "/dev/null", + "posted-at": "2025-02-05T04:54:41.218425-05:00", "edited-at": [ ], "tags": [ ], - "posted-at": "2025-02-05T04:54:41.218425-05:00", - "hidden": true, "placeholder": true } \ No newline at end of file diff --git a/db/posts/1.json b/db/posts/1.json index 145b0c1..dc60a5b 100644 --- a/db/posts/1.json +++ b/db/posts/1.json @@ -1,12 +1,13 @@ { - "tags": [ - ], "slugs": [ ], + "source": "/home/nathan/Projects/Blog/projects/Markdown/MyNewBlog.md", "edited-at": [ ], - "posted-at": "2025-02-05T06:00:49.553777-05:00", "hidden": false, - "source": "/home/nathan/Projects/Blog/projects/Markdown/MyNewBlog.md", - "markdown": true + "markdown": true, + "posted-at": "2025-02-05T06:00:49.553777-05:00", + "tags": [ + "meta" + ] } \ No newline at end of file diff --git a/db/posts/2.json b/db/posts/2.json index f356e0f..617775d 100644 --- a/db/posts/2.json +++ b/db/posts/2.json @@ -1,12 +1,12 @@ { - "posted-at": "2025-02-05T06:01:16.693698-05:00", "markdown": true, - "edited-at": [ + "tags": [ ], + "source": "/home/nathan/Projects/Blog/projects/Markdown/About.md", "slugs": [ ], - "hidden": true, - "source": "/home/nathan/Projects/Blog/projects/Markdown/About.md", - "tags": [ - ] + "posted-at": "2025-02-05T06:01:16.693698-05:00", + "edited-at": [ + ], + "hidden": true } \ No newline at end of file diff --git a/db/posts/3.json b/db/posts/3.json index ac66a8d..f9e475d 100644 --- a/db/posts/3.json +++ b/db/posts/3.json @@ -1,13 +1,13 @@ { - "hidden": false, - "posted-at": "2021-11-29T00:00:00Z", + "markdown": true, + "source": "/home/nathan/Projects/Blog/projects/Markdown/CryptoSuite.md", "tags": [ "cryptography" ], - "source": "/home/nathan/Projects/Blog/projects/Markdown/CryptoSuite.md", "edited-at": [ ], + "posted-at": "2021-11-29T00:00:00Z", "slugs": [ ], - "markdown": true + "hidden": false } \ No newline at end of file From 9a87e7825a4050ff7cc447d1bf9afce6d083bcfe Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Fri, 7 Feb 2025 02:02:40 -0500 Subject: [PATCH 05/13] Sort json keys --- db/meta.json | 4 ++-- db/posts/0.json | 14 +++++++------- db/posts/1.json | 6 +++--- db/posts/2.json | 16 ++++++++-------- db/posts/3.json | 12 ++++++------ lib/DB.rakumod | 4 ++-- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/db/meta.json b/db/meta.json index eeef098..2aae88d 100644 --- a/db/meta.json +++ b/db/meta.json @@ -1,7 +1,7 @@ { - "title": "Stranger Systems", + "about-id": 2, "base-url": "https://www.stranger.systems", "placeholder-id": 0, "tagline": "Making software better by making it weird", - "about-id": 2 + "title": "Stranger Systems" } \ No newline at end of file diff --git a/db/posts/0.json b/db/posts/0.json index 6f07b26..d978506 100644 --- a/db/posts/0.json +++ b/db/posts/0.json @@ -1,12 +1,12 @@ { - "source": "/dev/null", - "hidden": true, - "slugs": [ - ], - "posted-at": "2025-02-05T04:54:41.218425-05:00", "edited-at": [ ], - "tags": [ + "hidden": true, + "placeholder": true, + "posted-at": "2025-02-05T04:54:41.218425-05:00", + "slugs": [ ], - "placeholder": true + "source": "/dev/null", + "tags": [ + ] } \ No newline at end of file diff --git a/db/posts/1.json b/db/posts/1.json index dc60a5b..56936ec 100644 --- a/db/posts/1.json +++ b/db/posts/1.json @@ -1,12 +1,12 @@ { - "slugs": [ - ], - "source": "/home/nathan/Projects/Blog/projects/Markdown/MyNewBlog.md", "edited-at": [ ], "hidden": false, "markdown": true, "posted-at": "2025-02-05T06:00:49.553777-05:00", + "slugs": [ + ], + "source": "/home/nathan/Projects/Blog/projects/Markdown/MyNewBlog.md", "tags": [ "meta" ] diff --git a/db/posts/2.json b/db/posts/2.json index 617775d..fa46788 100644 --- a/db/posts/2.json +++ b/db/posts/2.json @@ -1,12 +1,12 @@ { - "markdown": true, - "tags": [ - ], - "source": "/home/nathan/Projects/Blog/projects/Markdown/About.md", - "slugs": [ - ], - "posted-at": "2025-02-05T06:01:16.693698-05:00", "edited-at": [ ], - "hidden": true + "hidden": true, + "markdown": true, + "posted-at": "2025-02-05T06:01:16.693698-05:00", + "slugs": [ + ], + "source": "/home/nathan/Projects/Blog/projects/Markdown/About.md", + "tags": [ + ] } \ No newline at end of file diff --git a/db/posts/3.json b/db/posts/3.json index f9e475d..527339e 100644 --- a/db/posts/3.json +++ b/db/posts/3.json @@ -1,13 +1,13 @@ { - "markdown": true, - "source": "/home/nathan/Projects/Blog/projects/Markdown/CryptoSuite.md", - "tags": [ - "cryptography" - ], "edited-at": [ ], + "hidden": false, + "markdown": true, "posted-at": "2021-11-29T00:00:00Z", "slugs": [ ], - "hidden": false + "source": "/home/nathan/Projects/Blog/projects/Markdown/CryptoSuite.md", + "tags": [ + "cryptography" + ] } \ No newline at end of file diff --git a/lib/DB.rakumod b/lib/DB.rakumod index 88a1140..c3c5426 100644 --- a/lib/DB.rakumod +++ b/lib/DB.rakumod @@ -62,10 +62,10 @@ class PostDB { mkdir $posts-dir unless $posts-dir.e; # Write out metadata # TODO: Track changes and only write changed files - $dir.add('meta.json').spurt: $!meta.to-json; + $dir.add('meta.json').spurt: $!meta.to-json(:sorted-keys); # Write out posts (ids are the filename) for %!posts.kv -> $key, $value { - $posts-dir.add("$key.json").spurt: $value.to-json; + $posts-dir.add("$key.json").spurt: $value.to-json(:sorted-keys); } } From 68e20af5d7de3bd4edb1edec6f37008939ee0cdb Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Fri, 7 Feb 2025 02:26:17 -0500 Subject: [PATCH 06/13] Factor out generate-head --- lib/Config.rakumod | 51 ++++++----------------------------------- lib/Render/Head.rakumod | 42 +++++++++++++++++++++++++++++++++ lib/Render/Util.rakumod | 18 +++++++++++++++ 3 files changed, 67 insertions(+), 44 deletions(-) create mode 100644 lib/Render/Head.rakumod create mode 100644 lib/Render/Util.rakumod diff --git a/lib/Config.rakumod b/lib/Config.rakumod index 6fd7525..838d8c0 100644 --- a/lib/Config.rakumod +++ b/lib/Config.rakumod @@ -1,50 +1,13 @@ use v6.e.PREVIEW; use HTML::Functional; + +use Render::Head; use DB::BlogMeta; use DB::Post; unit class Config; -method generate-head($title, BlogMeta:D $meta, $description?) { - 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 - do if $description ~~ Str:D { - meta :description :content($description) - } else { - [] - } - # 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; - ]; -} - method site-header(BlogMeta:D $meta) { header :class, [ div :class, [ @@ -206,7 +169,7 @@ method post-header(Post:D $post) { # TODO: Support GFM admonitions method generate-post(Post:D $post, BlogMeta:D $meta) { my $content = $post.render-html; - my $head = self.generate-head($post.title, $meta, $post.description); + my $head = generate-head($meta, $post.title, $post.description); my $body = body [ self.site-header: $meta; @@ -264,7 +227,7 @@ method generate-index($db) { self.generate-blurb: $pair.key, $db }); - my $head = self.generate-head(Nil, $db.meta); + my $head = generate-head($db.meta); my $body = body [ self.site-header: $db.meta; div :class, [ @@ -289,7 +252,7 @@ method generate-archive($db) { self.generate-blurb: $pair.key, $db }); - my $head = self.generate-head(Nil, $db.meta); + my $head = generate-head($db.meta); my $body = body [ self.site-header: $db.meta; div :class, [ @@ -353,7 +316,7 @@ method generate-tag-blurb($db, $tag, $limit?) { } method generate-tags-page($db, @tags) { - my $head = self.generate-head(Nil, $db.meta); + my $head = generate-head($db.meta); my $body = body [ self.site-header: $db.meta; div :class, [ @@ -371,7 +334,7 @@ method generate-tags-page($db, @tags) { } method generate-tag-page($db, $tag) { - my $head = self.generate-head(Nil, $db.meta); + my $head = generate-head($db.meta); my $body = body [ self.site-header: $db.meta; self.generate-tag-blurb($db, $tag, 4); diff --git a/lib/Render/Head.rakumod b/lib/Render/Head.rakumod new file mode 100644 index 0000000..a344911 --- /dev/null +++ b/lib/Render/Head.rakumod @@ -0,0 +1,42 @@ +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; + ] +} diff --git a/lib/Render/Util.rakumod b/lib/Render/Util.rakumod new file mode 100644 index 0000000..5a24dd6 --- /dev/null +++ b/lib/Render/Util.rakumod @@ -0,0 +1,18 @@ +use v6.e.PREVIEW; +unit module Render::Util; + +sub opt($test, $item) is export { + if $test { + $item + } else { + [] + } +} + +sub optl($test, &item) is export { + if $test { + item + } else { + [] + } +} From 72bd2a238c9c801bb3fa7d6774a9b3a580d14648 Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Fri, 7 Feb 2025 02:33:40 -0500 Subject: [PATCH 07/13] factor out post-link method --- lib/Config.rakumod | 17 +++-------------- lib/Render/Util.rakumod | 13 +++++++++++++ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/Config.rakumod b/lib/Config.rakumod index 838d8c0..298d2ed 100644 --- a/lib/Config.rakumod +++ b/lib/Config.rakumod @@ -3,6 +3,7 @@ use v6.e.PREVIEW; use HTML::Functional; use Render::Head; +use Render::Util; use DB::BlogMeta; use DB::Post; @@ -194,13 +195,7 @@ method generate-post(Post:D $post, BlogMeta:D $meta) { method generate-blurb(Int:D $id, $db) { my $post = $db.posts{$id}; my $desc = $post.description; - my @slugs = $post.all-slugs; - # Use the primary slug if there is one, the id if there isn't - my $link = do if @slugs.elems { - "/posts/by-slug/{@slugs[*-1]}.html" - } else { - "/posts/by-id/$id.html" - } + my $link = post-link $id, $post; div :class, [ div :class, [ a :href($link), span [ @@ -272,13 +267,7 @@ method generate-archive($db) { method generate-tag-blurb($db, $tag, $limit?) { sub post-to-link($id, $post) { my $desc = $post.description; - my @slugs = $post.all-slugs; - # Use the primary slug if there is one, the id if there isn't - my $link = do if @slugs.elems { - "/posts/by-slug/{@slugs[*-1]}.html" - } else { - "/posts/by-id/$id.html" - } + my $link = post-link $id, $post; div :class, [ div :class, [ a :href($link), span [ diff --git a/lib/Render/Util.rakumod b/lib/Render/Util.rakumod index 5a24dd6..4cf7778 100644 --- a/lib/Render/Util.rakumod +++ b/lib/Render/Util.rakumod @@ -1,6 +1,8 @@ use v6.e.PREVIEW; unit module Render::Util; +use DB::Post; + sub opt($test, $item) is export { if $test { $item @@ -16,3 +18,14 @@ sub optl($test, &item) is export { [] } } + +#| 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" + } +} From 87e18dbf608dbbd2df7c7656c0e61a837bb49372 Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Fri, 7 Feb 2025 02:44:35 -0500 Subject: [PATCH 08/13] Factor out and clean up site-header --- lib/Config.rakumod | 63 ++++------------------------------------- lib/Render/Head.rakumod | 26 +++++++++++++++++ lib/Render/Util.rakumod | 6 ++++ 3 files changed, 37 insertions(+), 58 deletions(-) diff --git a/lib/Config.rakumod b/lib/Config.rakumod index 298d2ed..a7982e6 100644 --- a/lib/Config.rakumod +++ b/lib/Config.rakumod @@ -9,55 +9,6 @@ use DB::Post; unit class Config; -method site-header(BlogMeta:D $meta) { - header :class, [ - div :class, [ - # TODO: Use a real image here - $meta.title - ]; - div :class, [ - $meta.tagline - ]; - div :class, [ - a :href, [ - icon 'home'; - ' '; - span [ - 'Home'; - ]; - ]; - a :href, [ - icon 'archive'; - ' '; - span [ - 'Archive'; - ]; - ]; - a :href, [ - icon 'purchase-tag-alt'; - ' '; - span [ - 'Tags'; - ]; - ]; - a :href, [ - icon 'info-circle'; - ' '; - span [ - 'About'; - ]; - ]; - a :href, [ - icon 'rss'; - ' '; - span [ - 'Feed'; - ]; - ]; - ]; - ] -} - method post-date(Post:D $post) { my $datetime = $post.posted-at; my $timestamp = sprintf( @@ -173,7 +124,7 @@ method generate-post(Post:D $post, BlogMeta:D $meta) { my $head = generate-head($meta, $post.title, $post.description); my $body = body [ - self.site-header: $meta; + site-header $meta; article :class, [ self.post-header: $post; div :class, [ @@ -224,7 +175,7 @@ method generate-index($db) { my $head = generate-head($db.meta); my $body = body [ - self.site-header: $db.meta; + site-header $db.meta; div :class, [ h1 "Recent Posts" ], @most-recent; @@ -249,7 +200,7 @@ method generate-archive($db) { my $head = generate-head($db.meta); my $body = body [ - self.site-header: $db.meta; + site-header $db.meta; div :class, [ h1 "All Posts" ], @most-recent; @@ -307,7 +258,7 @@ method generate-tag-blurb($db, $tag, $limit?) { method generate-tags-page($db, @tags) { my $head = generate-head($db.meta); my $body = body [ - self.site-header: $db.meta; + site-header $db.meta; div :class, [ h1 "Tags"; ], @tags.map(-> $tag {self.generate-tag-blurb($db, $tag, 4)}); @@ -325,7 +276,7 @@ method generate-tags-page($db, @tags) { method generate-tag-page($db, $tag) { my $head = generate-head($db.meta); my $body = body [ - self.site-header: $db.meta; + site-header $db.meta; self.generate-tag-blurb($db, $tag, 4); ]; @@ -337,7 +288,3 @@ method generate-tag-page($db, $tag) { "$html" } - -sub icon($icon) { - i(:class("bx bx-$icon")) -} diff --git a/lib/Render/Head.rakumod b/lib/Render/Head.rakumod index a344911..e7eb435 100644 --- a/lib/Render/Head.rakumod +++ b/lib/Render/Head.rakumod @@ -40,3 +40,29 @@ sub generate-head(BlogMeta:D $meta, $title?, $description?) is export { :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/Util.rakumod b/lib/Render/Util.rakumod index 4cf7778..244b561 100644 --- a/lib/Render/Util.rakumod +++ b/lib/Render/Util.rakumod @@ -3,6 +3,8 @@ unit module Render::Util; use DB::Post; +use HTML::Functional; + sub opt($test, $item) is export { if $test { $item @@ -29,3 +31,7 @@ sub post-link(Int:D $id, Post:D $post --> Str:D) is export { "/posts/by-id/$id.html" } } + +sub icon($icon) is export { + i(:class("bx bx-$icon")) +} From 731d7aa19c0925a31fc544cab46e5497499610d1 Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Fri, 7 Feb 2025 02:54:20 -0500 Subject: [PATCH 09/13] Factor out post generation components --- lib/Config.rakumod | 118 ++-------------------------------------- lib/Render/Post.rakumod | 100 ++++++++++++++++++++++++++++++++++ lib/Render/Util.rakumod | 17 ++++++ 3 files changed, 122 insertions(+), 113 deletions(-) create mode 100644 lib/Render/Post.rakumod diff --git a/lib/Config.rakumod b/lib/Config.rakumod index a7982e6..68c8172 100644 --- a/lib/Config.rakumod +++ b/lib/Config.rakumod @@ -2,122 +2,14 @@ use v6.e.PREVIEW; use HTML::Functional; -use Render::Head; use Render::Util; +use Render::Head; +use Render::Post; use DB::BlogMeta; use DB::Post; unit class Config; -method post-date(Post:D $post) { - 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 - ] -} - -method post-edit(Post:D $post) { - 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("Laste Edited At $timestamp"), [ - icon 'edit'; - ' '; - $timestamp - ] -} - -sub mins-to-string($mins) { - if $mins < 60 { - $mins.Str ~ "m" - } else { - my $h = $mins div 60; - my $m = $mins mod 60; - $h.Str ~ "h" ~ $m.Str ~ "m" - } -} - -method post-read-time(Post:D $post) { - my ($slow, $average, $fast) = $post.readtimes; - div :class, :title, [ - icon 'timer'; - ' '; - mins-to-string $slow; - ' '; - '/'; - ' '; - mins-to-string $average; - ' '; - '/'; - ' '; - mins-to-string $fast; - ] -} - -method post-tag(Str:D $tag) { - span :class, [ - a :href("/tags/$tag.html"), [ - icon 'hash'; - $tag; - ] - ] -} - -method post-tags(Post:D $post){ - sub intersperse (\element, +list) { - gather for list { - FIRST .take, next; - take slip element, $_; - } - } - my @tags = $post.tags; - if @tags { - @tags.=map(-> $tag {self.post-tag($tag)}); - div :class, [ - icon 'purchase-tag-alt'; - ' '; - intersperse(', ', @tags); - ] - } else { - [] - } -} - -method post-info(Post:D $post) { - div :class, [ - self.post-date: $post; - self.post-edit: $post; - self.post-read-time: $post; - self.post-tags: $post; - ]; -} - -method post-header(Post:D $post) { - header :class, [ - div :class, [ - h1 $post.title; - ]; - self.post-info: $post; - ] -} - # TODO: Support GFM admonitions method generate-post(Post:D $post, BlogMeta:D $meta) { my $content = $post.render-html; @@ -126,7 +18,7 @@ method generate-post(Post:D $post, BlogMeta:D $meta) { body [ site-header $meta; article :class, [ - self.post-header: $post; + post-header $post; div :class, [ $content; ] @@ -153,7 +45,7 @@ method generate-blurb(Int:D $id, $db) { h2 $post.title; ]; ]; - self.post-info: $post; + post-info $post; if $desc ~~ Str:D { div :class, [ p $post.description; @@ -224,7 +116,7 @@ method generate-tag-blurb($db, $tag, $limit?) { a :href($link), span [ h3 $post.title; ]; - self.post-info: $post; + post-info $post; if $desc ~~ Str:D { div :class, [ p $post.description; diff --git a/lib/Render/Post.rakumod b/lib/Render/Post.rakumod new file mode 100644 index 0000000..37fbd0b --- /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; + 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 index 244b561..05630a7 100644 --- a/lib/Render/Util.rakumod +++ b/lib/Render/Util.rakumod @@ -35,3 +35,20 @@ sub post-link(Int:D $id, Post:D $post --> Str:D) is export { 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, $_; + } +} From 19e06488c42433b5ba15bed9c9c83ca4342e7657 Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Fri, 7 Feb 2025 05:48:05 -0500 Subject: [PATCH 10/13] Functional syntax highlighting for rust --- lib/DB/MarkdownPost.rakumod | 18 ++- lib/Pandoc.rakumod | 2 +- lib/Pygments.rakumod | 40 ++++++ projects/Markdown/RustPosting.md | 206 +++++++++++++++++++++++++++++++ resources/code.css | 1 - resources/colors.css | 34 ++++- resources/main.css | 3 + 7 files changed, 299 insertions(+), 5 deletions(-) create mode 100644 lib/Pygments.rakumod create mode 100644 projects/Markdown/RustPosting.md diff --git a/lib/DB/MarkdownPost.rakumod b/lib/DB/MarkdownPost.rakumod index 0f189d5..bbddc8e 100644 --- a/lib/DB/MarkdownPost.rakumod +++ b/lib/DB/MarkdownPost.rakumod @@ -1,9 +1,13 @@ use v6.e.PREVIEW; use Pandoc; -use JSON::Class:auth; +use Pygments; use DB::Post; + +use JSON::Class:auth; +use File::Temp; + #| A plain markdown post unit class MarkdownPost does Post is json(:pretty); @@ -20,7 +24,17 @@ method title(--> Str:D) { # Simply provide our source file to pandoc method render-html(--> Str:D) { - markdown-to-html $!source + # Test to see if this posts contains any fenced code blocks, if so, + # pygmentize it through a temporary file + my $contents = $!source.slurp; + if $contents ~~ /'```'/ { + my $output = highlight-code $contents; + my ($filename, $filehandle) = tempfile; + $filehandle.spurt: $output, :close; + markdown-to-html $filename.IO + } else { + markdown-to-html $!source + } } # Return our summary, if we have one, otherwise extract the first paragraph of diff --git a/lib/Pandoc.rakumod b/lib/Pandoc.rakumod index 713b376..2b0c91f 100644 --- a/lib/Pandoc.rakumod +++ b/lib/Pandoc.rakumod @@ -6,7 +6,7 @@ use JSON::Fast; #| Run pandoc with the given arguments, dieing on failure sub pandoc(*@args --> Str:D) { # Call into pandoc - my $pandoc = run 'pandoc', @args, :out, :err; + my $pandoc = run 'pandoc', '--no-highlight', @args, :out, :err; # Collect the output my $output = $pandoc.out.slurp: :close; diff --git a/lib/Pygments.rakumod b/lib/Pygments.rakumod new file mode 100644 index 0000000..527012f --- /dev/null +++ b/lib/Pygments.rakumod @@ -0,0 +1,40 @@ +#| Interaction with pygments +unit module Pygments; + +my token fence { '```' } +my token info-string { \w+ } +# TODO: Be more precise about this so we can handle backticks in code blocks +my token code { <-[`]>+ } +my token code-block { + <&fence> \h* \v + + <&fence> +} + +sub pygment(Str:D $code, Str:D $lang --> Str:D) { + my $pygments = run , $lang, :out, :in; + $pygments.in.spurt: $code, :close; + $pygments.out.slurp: :close; +} + +sub highlight-code(Str:D $input --> Str:D) is export { + my $text = $input; + # TODO: Figure out a way to exclude idris code so we can process both in the + # same file + while $text ~~ &code-block { + my $match = $/; + # Extract the match and have pygments colorize the code + my $code = $match.Str; + my $lang = $match.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/projects/Markdown/RustPosting.md b/projects/Markdown/RustPosting.md new file mode 100644 index 0000000..e673f02 --- /dev/null +++ b/projects/Markdown/RustPosting.md @@ -0,0 +1,206 @@ +# 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 bfd642f..5974f21 100644 --- a/resources/colors.css +++ b/resources/colors.css @@ -75,7 +75,7 @@ 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); @@ -102,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 a23b136..4b54d5e 100644 --- a/resources/main.css +++ b/resources/main.css @@ -33,6 +33,9 @@ body, .post { align-items: center; gap: var(--box-gap); } +.post { + width: 100%; +} /* Style the site header */ .site-header { From 946ce908e9e6a94f3cbbd7608bb95458bc8cb8ac Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Fri, 7 Feb 2025 05:58:33 -0500 Subject: [PATCH 11/13] Add raku tag to new blog post --- db/posts/1.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/db/posts/1.json b/db/posts/1.json index 56936ec..62e539a 100644 --- a/db/posts/1.json +++ b/db/posts/1.json @@ -8,6 +8,7 @@ ], "source": "/home/nathan/Projects/Blog/projects/Markdown/MyNewBlog.md", "tags": [ - "meta" + "meta", + "raku" ] } \ No newline at end of file From 88e355730b3996750c1f3d8a1eb45756305402ca Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Fri, 7 Feb 2025 06:02:53 -0500 Subject: [PATCH 12/13] Sort tags alphabetically for consistent ordering --- lib/DB.rakumod | 2 +- lib/Render/Post.rakumod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/DB.rakumod b/lib/DB.rakumod index c3c5426..dac655d 100644 --- a/lib/DB.rakumod +++ b/lib/DB.rakumod @@ -106,7 +106,7 @@ class PostDB { $about-path.unlink if $about-path.l; $by-id.add("{$!meta.about-id}.html").symlink: $about-path; # Generate the tags pages - my @tags = %!posts.values.map(*.tags).flat.unique; + my @tags = %!posts.values.map(*.tags).flat.unique.sort; $out-dir.add('tags.html').spurt: $config.generate-tags-page(self, @tags); my $tags-dir = $out-dir.add('tags/'); mkdir $tags-dir unless $tags-dir.e; diff --git a/lib/Render/Post.rakumod b/lib/Render/Post.rakumod index 37fbd0b..826582e 100644 --- a/lib/Render/Post.rakumod +++ b/lib/Render/Post.rakumod @@ -68,7 +68,7 @@ sub post-tag(Str:D $tag) is export { } sub post-tags(Post:D $post) is export { - my @tags = $post.tags; + my @tags = $post.tags.sort; if @tags { @tags.=map(*.&post-tag); div :class, [ From b095fe356be597b7de5fa9864fd618844341c460 Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Fri, 7 Feb 2025 06:50:13 -0500 Subject: [PATCH 13/13] Advent of bugs --- db/posts/4.json | 14 +++ lib/Pandoc.rakumod | 1 + projects/Markdown/2025/01-Jan/AdventOfBugs.md | 98 +++++++++++++++++++ projects/Markdown/RustPosting.md | 1 - 4 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 db/posts/4.json create mode 100644 projects/Markdown/2025/01-Jan/AdventOfBugs.md diff --git a/db/posts/4.json b/db/posts/4.json new file mode 100644 index 0000000..9effd7d --- /dev/null +++ b/db/posts/4.json @@ -0,0 +1,14 @@ +{ + "edited-at": [ + ], + "hidden": false, + "markdown": true, + "posted-at": "2025-02-07T06:45:08.536663-05:00", + "slugs": [ + ], + "source": "/home/nathan/Projects/Blog/projects/Markdown/2025/01-Jan/AdventOfBugs.md", + "tags": [ + "idris", + "advent-of-code" + ] +} \ No newline at end of file diff --git a/lib/Pandoc.rakumod b/lib/Pandoc.rakumod index 2b0c91f..6018021 100644 --- a/lib/Pandoc.rakumod +++ b/lib/Pandoc.rakumod @@ -88,6 +88,7 @@ sub markdown-first-paragraph(IO::Path:D $file --> Str:D) is export { $para ~= "\n"; } when "Link" { + # TODO: Properly descend into links $para ~= $component[1][0]; } default { 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 index e673f02..632d2f1 100644 --- a/projects/Markdown/RustPosting.md +++ b/projects/Markdown/RustPosting.md @@ -107,7 +107,6 @@ impl Point { ``` - Modules and imports ```rust