Compare commits

103 Commits

Author SHA1 Message Date
2ac002d798 keep working on sidenotes 2026-03-11 05:19:18 -04:00
6e6351f5cf keep working on sidenotes, copy latest real post over to test 2026-03-09 21:50:51 -04:00
e2049c2e29 tweak header bar styling 2026-03-09 07:58:28 -04:00
173b5ba9f4 theme switcher 2026-03-09 07:48:39 -04:00
0070ed1c19 settle on a dark theme and implement with override 2026-03-08 10:55:41 -04:00
0f5dadbf6f add obsidian/mocha/deep-ocean themes (Gemini) 2026-03-07 13:40:33 -05:00
8fc267e6df add mise config which was missing somehow 2026-03-07 13:38:43 -05:00
827a4406bd remove extraneous themes 2026-03-07 13:25:03 -05:00
657ad09a20 all the themes together + switcher 2026-03-07 13:18:56 -05:00
6bf81e0b20 claude dark v9 "northern lights" 2026-03-07 11:09:28 -05:00
6b8c47cbb4 claude dark v8 "copper & slate" 2026-03-07 11:08:08 -05:00
1142003e40 claude dark v7 "stormfront" 2026-03-07 11:06:36 -05:00
365da1f285 claude dark v6 "midnight garden" 2026-03-07 09:27:30 -05:00
e01cf188e2 claude dark v5, salmon accent 2026-03-07 09:21:34 -05:00
6747faeb7a claude dark v4, blue/gold 2026-03-07 09:18:19 -05:00
7acf1f2c9f start work on sidenotes 2026-03-05 21:39:22 -05:00
13d0ac8de7 make dropcap switchable between ascender/descender 2026-03-05 08:45:49 -05:00
b291e93e75 move to Baskervville for headings, with proper smallcaps variant 2026-03-05 08:15:24 -05:00
b0e6576b33 add SmallCaps component and use for title 2026-03-03 20:31:46 -05:00
6b0a985ee1 finish TOC component 2026-03-01 18:34:48 -05:00
dfdf6c6e66 continue working on post layout, add typography styles 2026-02-28 15:21:36 -05:00
c28f340333 start working on posts with placeholder content 2026-02-28 09:26:10 -05:00
95b58b5615 start work on Astro port 2026-02-27 09:10:25 -05:00
c81531a092 commit unsaved work before starting work on astro migration 2026-02-26 15:03:44 -05:00
bef34007d4 advent of languages 2024 day 4 2024-12-11 05:19:17 -05:00
400da4e539 advent of languages 2024 day 3 2024-12-07 09:01:34 -05:00
2b8989e02e advent of languages day 2 2024-12-03 15:50:49 -05:00
dfc09d8861 advent of languages day 1 2024-12-02 10:34:06 -05:00
72a9a2e1f1 publish terrible-internet post 2024-11-16 12:39:10 -05:00
5b21f3b3a7 fix links for local static files 2024-11-16 12:37:47 -05:00
9b82175daa fix typo in sshkey post 2024-11-11 10:26:03 -05:00
2f61b4a453 switch next and previous links to match chronological post order 2024-11-11 10:23:38 -05:00
9c35c72989 tweaks and fixes to terrible-internet post 2024-11-11 10:10:46 -05:00
723a5625b8 initial draft of terrible-internet post 2024-11-10 16:00:47 -05:00
91a51d10f9 fix typo in ssh keys post 2024-11-10 16:00:09 -05:00
9eeb3e87bd rework sidenotes to make nesting possible 2024-07-11 06:03:34 -04:00
716792e8a6 tweak wording 2024-07-08 05:39:23 -04:00
9399bcad96 actually publish ssh key post 2024-07-07 12:19:11 -04:00
8e58d6824a add ssh key format post 2024-07-07 12:13:15 -04:00
60bc85d49a fix .com/.exe switch 2024-06-17 10:16:24 -04:00
eb3992720b bump line spacing 2024-06-17 08:43:05 -04:00
10d197e17d 26 years 2024-06-17 06:28:08 -04:00
b38f9f426a add win-gui-cli post, work on axes of fantasy a little more 2024-06-17 06:24:38 -04:00
918791baf6 book previews 2024-01-08 23:05:44 -08:00
816f3a9c0f tweak css one more time and start work on axes of fantasy post 2024-01-01 22:45:13 -08:00
ba4c2c2506 finish css overhaul 2023-12-26 20:30:09 -08:00
9a85bef2be remove old stylesheet from base template and tweak footer links 2023-12-20 10:38:01 -08:00
a6735c45f4 add mono font and rework codeblock css 2023-12-20 07:45:26 -08:00
b5ca20d739 further simplify heading links 2023-12-19 07:51:20 -08:00
9d3a59e554 just use absolute positioning for anchor links 2023-12-18 16:00:20 -08:00
dd36f0e79e switch to sass and split global styles, rework heading css 2023-12-18 13:41:01 -08:00
29e0b35ee4 start work on cli post 2023-12-03 17:56:27 -08:00
60ce9f8e8d fix sizing of code elements in sidenotes 2023-10-25 14:49:39 -07:00
2fac675c0c fix typo in vue-vs-svelte 2023-10-25 14:28:21 -07:00
8542cccd34 more work on password strength 2023-10-23 05:57:56 -07:00
7df2de6c15 start writing password strength post 2023-10-23 05:54:11 -07:00
57476b9d80 start writing kubernetes alternative post 2023-10-23 05:53:58 -07:00
924706b3b2 start working on kubernetes alternative post 2023-10-01 06:41:33 -07:00
00300167eb add 404 page 2023-09-25 06:57:50 -07:00
859c34fd82 add opengraph meta tags 2023-09-24 13:29:35 -07:00
15ab47f4d8 fix page titles 2023-09-24 09:46:57 -07:00
c231ed008d remove favicon 2023-09-24 09:42:49 -07:00
705e858170 tweak docker post 2023-09-24 09:38:49 -07:00
5529bab8a3 finish vue vs svelte post 2023-09-24 09:23:12 -07:00
5ec147f95f add about page 2023-09-24 09:20:06 -07:00
453dad1e0d add feed link to app.html 2023-09-23 20:20:35 -07:00
7b3ae4dea8 hide TOC on narrow screens 2023-09-23 20:03:00 -07:00
ce4ddf5a17 tweak next/prev links 2023-09-23 19:23:40 -07:00
c1e82ffb2c finish feed 2023-09-05 17:16:39 -07:00
7fb1f05a1e initial feed implementation 2023-09-04 22:07:58 -07:00
a28ee8b2f0 get footer links working and get rid of unnecessary json route 2023-08-27 08:52:30 -07:00
1b2d55173a upgrade to sveltekit 1 2023-08-26 20:55:35 -07:00
3a59f45e58 rework toc, sidenote fade, and footer 2023-08-26 14:41:58 -07:00
0519291bda limit toc to two levels and vary style 2023-08-25 22:22:39 -07:00
d1aa23e7c7 tweak margin of toc 2023-08-23 07:50:38 -07:00
25ce1b2d85 only show header anchors on hover 2023-08-23 06:02:12 -07:00
5817d94043 rework layout and add table of contents 2023-08-21 22:16:17 -07:00
33d6838dc4 start work on table of contents 2023-08-20 22:04:21 -07:00
b1dc3ae0ea add heading anchors 2023-08-20 16:12:04 -07:00
6431267827 start sidenotes post 2023-08-19 13:11:17 -07:00
01fce255ac tweaks 2023-08-19 12:46:00 -07:00
8272a4bd43 rework rehype plugin 2023-08-19 12:45:02 -07:00
b68220fa2e conditionally render remainder of dropcap word 2023-08-19 12:16:23 -07:00
54bcec280d fix sidenote width and add partial nesting support 2023-08-19 12:12:12 -07:00
60eea00b88 misc tweaks 2023-08-15 11:07:11 -07:00
da72464f9a change font of drop caps 2023-08-15 11:06:54 -07:00
1a77127979 tweak positioning of sidenote counter 2023-08-15 11:05:30 -07:00
15e61d858d vue vs svelte post 2023-08-14 15:46:02 -07:00
1972c5714c drop cap component 2023-08-14 15:45:52 -07:00
ce411549ad css tweaks 2023-08-14 15:45:34 -07:00
ec22cebeac under construction 2022-11-04 20:14:26 -07:00
40ea5014dd work on technology/magic post 2022-06-16 21:45:13 -07:00
05c8fcf5f4 minor twiddles to docker-lan post 2022-06-16 20:08:05 -07:00
e0c2dc2ab8 sidenote dismiss button 2022-06-16 20:00:09 -07:00
0d6542289e update all posts page 2022-05-20 20:44:26 -07:00
93ee753974 rework sidenotes with pure css 2022-05-20 20:42:44 -07:00
ca903e2d15 fix sidenote tiling bug 2022-05-14 13:30:11 -07:00
7e5960c14a code formatting and docker lan post 2022-05-14 11:18:42 -07:00
3413b7ae7e move all sidenote logic to Sidenote component 2022-05-14 11:18:11 -07:00
4658511759 finish mesh vpn post 2022-05-14 07:43:18 -07:00
dbcc82a10a fix sidenotes 2022-05-14 07:43:08 -07:00
819775f0a7 add mesh vpn post 2022-05-13 21:50:34 -07:00
3c7732f1f4 fix sidenote collisions 2022-05-13 21:50:19 -07:00
50 changed files with 2616 additions and 6433 deletions

6
.gitignore vendored
View File

@@ -1,5 +1,3 @@
.DS_Store
.astro/
dist/
node_modules
/build
/.svelte-kit
/package

View File

@@ -1,38 +0,0 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte);
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm init svelte@next
# create a new project in my-app
npm init svelte@next my-app
```
> Note: the `@next` is temporary
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
Before creating a production version of your app, install an [adapter](https://kit.svelte.dev/docs#adapters) for your target environment. Then:
```bash
npm run build
```
> You can preview the built app with `npm run preview`, regardless of whether you installed an adapter. This should _not_ be used to serve your app in production.

11
astro.config.mjs Normal file
View File

@@ -0,0 +1,11 @@
import { defineConfig } from "astro/config";
import mdx from '@astrojs/mdx';
import vue from '@astrojs/vue';
export default defineConfig({
integrations: [
mdx(),
vue(),
],
prefetch: true,
});

1291
bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +0,0 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"$lib": ["src/lib"],
"$lib/*": ["src/lib/*"]
}
},
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
}

3
mise.toml Normal file
View File

@@ -0,0 +1,3 @@
[tools]
bun = "latest"
node = "24"

5736
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,23 @@
{
"name": "blog",
"dependencies": {
"@astrojs/check": "^0.9.6",
"@astrojs/mdx": "^4.3.13",
"@astrojs/rss": "^4.0.15",
"@astrojs/sitemap": "^3.7.0",
"@astrojs/vue": "^5.1.4",
"@fontsource-variable/baskervville": "^5.2.3",
"@fontsource-variable/baskervville-sc": "^5.2.3",
"@fontsource-variable/figtree": "^5.2.10",
"astro": "^5.18.0",
"sharp": "^0.34.5"
},
"name": "astro",
"version": "0.0.1",
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build",
"preview": "svelte-kit preview"
},
"devDependencies": {
"@sveltejs/adapter-static": "^1.0.0-next.21",
"@sveltejs/kit": "next",
"mdsvex": "^0.9.8",
"node-sass": "^6.0.1",
"svelte": "^3.42.6",
"svelte-preprocess": "^4.9.8"
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"type": "module"
}

View File

@@ -0,0 +1,363 @@
---
title: 'Advent of Languages 2024, Day 4: Fortran'
date: 2024-12-10
---
import Sidenote from '@components/Sidenote.astro'
Oh, you thought we were done going back in time? Well I've got news for you, Doc Brown, you'd better not mothball the ol' time machine just yet, because we're going back even further. That's right, for Day 4 I've decided to use Fortran!<Sidenote>Apparently it's officially called `Fortran` now and not `FORTRAN` like it was in days of yore, and has been ever since the 1990s. That's right, when most languages I've used were just getting their start, Fortran was going through its mid-life identity crisis.</Sidenote><Sidenote>When I told my wife that I was going to be using a language that came out in the 1950s, she wanted to know if the next one would be expressed in Egyptian hieroglyphs.</Sidenote>
Really, though, it's because this is day _four_, and I had to replace all those missed Forth jokes with _something_.
## The old that is strong does not wither
Fortran dates back to 1958, making it the oldest programming language still in widespread use.<Sidenote>Says Wikipedia, at least. Not in the article about Fotran, for some reason, but in [the one about Lisp](https://en.wikipedia.org/wiki/Lisp_(programming_language)).</Sidenote> Exactly how widespread is debatable--the [TIOBE index](https://www.tiobe.com/tiobe-index/) puts it at #8, but the TIOBE index also puts Delphi Pascal at #11 and Assembly at #19, so it might have a different idea of what makes a language "popular" than you or I.<Sidenote>For contrast, Stack Overflow puts it at #38, right below Julia and Zig, which sounds a little more realistic to me.</Sidenote> Regardless, it's undeniable that it gets pretty heavy use even today--much more than Forth, I suspect--because of its ubiquity in the scientific and HPC sectors. The website mentions "numerical weather and ocean prediction, computational fluid dynamics, applied math, statistics, and finance" as particularly strong areas. My guess is that this largely comes down to intertia, plus Fortran being "good enough" for the things people wanted to use it for that it was easier to keep updating Fortran than to switch to something else wholesale.<Sidenote>Unlike, say, BASIC, which is so gimped by modern standards that it _doesn't even have a call stack_. That's right, you can't do recursion in BASIC, at least not without managing the stack yourself.</Sidenote>
And update they have! Wikipedia lists 12 major versions of Fortran, with the most recent being Fortran 2023. That's a pretty impressive history for a programming language. It's old enough to retire!
The later versions of Fortran have added all sorts of modern conveniences, like else-if conditionals (77), properly namespaced modules (90), growable arrays (also 90), local variables (2008), and finally, just last year, ternary expressions and the ability infer the length of a string variable from a string literal! Wow!
I have to say, just reading up on Fortran is already feeling modern than it did for Forth, or even C/C++. It's got a [snazzy website](https://fortran-lang.org/)<Sidenote>With a dark/light mode switcher, so you know it's hip.</Sidenote> with obvious links to documentation, sitewide search, and even an online playground. This really isn't doing any favors for my former impression of Fortran as a doddering almost-septegenarian with one foot in the grave and the other on a banana peel.
## On the four(tran)th day of Advent, my mainframe gave to me
The Fortran getting-started guide [literally gives you](https://fortran-lang.org/learn/quickstart/hello_world/) hello-world, so I won't bore you with that here. Instead I'll just note some interesting aspects of the language that jumped out at me:
* There's no `main()` function like C and a lot of other compiled languages, but there are mandatory `program <name> ... end program` delimiters at the start and end of your outermost layer of execution. Modules are defined outside of the `program ... end program` block. Not sure yet whether you can have multiple `program` blocks, but I'm leaning towards no?
* Variables are declared up-front, and are prefixed with their type name followed by `::`. You can leave out the type qualifier, in which case the type of the variable will be inferred not from the value to which it is first assigned, but from its _first letter_: variables whose names start with `i`, `j`, `k`, `l`, `m`, `n` are integers, everything else is a `real` (floating-point). Really not sure what drove that decision, but it's described as deprecated, legacy behavior anyway, so I plan to ignore it.
* Arrays are 1-indexed. Also, multi-dimensional arrays are a native feature! I'm starting to see that built-for-numerical-workloads heritage.
* It has `break` and `continue`, but they're named `exit` and `cycle`.
* There's a _built-in_ parallel-loop construct,<Sidenote>It uses different syntax to define its index and limit. That's what happens when your language development is spread over the last 65 years, I guess.</Sidenote> which "informs the compiler that it may use parallelization/SIMD to speed up execution". I've only ever seen this done at the library level before. If you're lucky your language has enough of a macro system to make it look semi-natural, otherwise, well, I hope you like map/reduce.
* It has functions, but it _also_ has "subroutines". The difference is that functions return values and are expected not to modify their arguments, and subroutines don't return values but may modify their arguments. I guess you're out of luck if you want to modify an argument _and_ return a value (say, a status code or something).
* Function and subroutine arguments are mentioned in the function signature (which looks like it does in most languages), but you really get down to brass tacks in the function body itself, which is where you specify the type and in-or-out-ness of the parameters. Reminds me of PowerShell, of all things.
* The operator for accessing struct fields is `%`. Where other languages do `sometype.field`, in Fortran you'd do `sometype%field`.
* Hey look, it's OOP! We can have methods! Also inheritance, sure, whatever.
Ok, I'm starting to get stuck in the infinite docs-reading rut for which I criticized myself at the start of this series, so buckle up, we're going in.
## The Puzzle
We're given a two-dimensional array of characters and asked to find the word `XMAS` everywhere it occurs, like those [word search](https://en.wikipedia.org/wiki/Word_search) puzzles you see on the sheets of paper they hand to kids at restaurants in a vain attempt to keep them occupied so their parents can have a chance to enjoy their meal.
Hey, Fortran might actually be pretty good at this! At least, multi-dimensional arrays are built in, so I'm definitely going to use those.
First things first, though, we have to load the data before we can start working on it.<Sidenote>Getting a Fortran compiler turned out to be as simple as `apt install gfortran`.</Sidenote>
My word-search grid appears to be 140 characters by 140, so I'm just going to hard-code that as the dimensions of my array. I'm sure there's a way to size arrays dynamically, but life's too short.
### Loading data is hard this time
Not gonna lie here, this part took me _way_ longer than I expected it to. See, the standard way to read a file in Fortran is with the `read()` statement. (It looks like a function call, but it's not.) You use it something like this:
```fortran
read(file_handle, *) somevar, anothervar, anothervar2
```
Or at least, that's one way of using it. But here's the problem: by default, Fortran expects to read data stored in a "record-based" format. In short, this means that it's expected to consist of lines, and each line will be parsed as a "record". Records consist of some number of elements, separated by whitespace. The "format" of the record, i.e. how the line should be parsed, can either be explicitly specified in a slightly arcane mini-language reminiscent of string-interpolation placeholders (just in reverse), or it can be inferred from the number and types of the variables specified after `read()`.
Initially, I thought I might be able to do this:
```fortran
character, dimension(140, 140) :: grid
! ...later
read(file_handle, *) grid
```
The top line is just declaring `grid` as a 2-dimensional array characters, 140 rows by 140 columns. Neat, huh?
But sadly, this kept spitting out errors about how it had encountered the end of the file unexpectedly. I think what was happening was that when you give `read()` an array, it expects to populate each element of the array with one record from the file, and remember records are separated by lines, so this was trying to assign one line per array element. My file had 140 lines, but my array had 140 * 140 elements, so this was never going to work.
My next try looked something like this:
```fortran
do row = 1, 100
read(file_handle, *) grid(row, :)
end do
```
But this also resulted in end-of-file errors. Eventually I got smart and tried running this read statement just _once_, and discovered that it was populating the first row of the array with the first letter of _each_ line in the input file. I think what's going on here is that `grid(1, :)` creates a slice of the array that's 1 row by the full width (so 140), and the `read()` statement sees that and assumes that it needs to pull 140 records from the file _each time this statement is executed_. But records are (still) separated by newlines, so the first call to `read()` pulls all 140 rows, dumps everything but the first character from each (because, I think, the type of the array elements is `character`), puts that in and continues on. So after just a single call to `read()` it's read every line but dumped most of the data.
I'm pretty sure the proper way to do this would be to figure out how to set the record separator, but it's tricky because the "records" (if we want each character to be treated as a record) within each line are smashed right up against each other, but have newline characters in between lines. So I'd have to specify that the separator is sometimes nothing, and sometimes `\n`, and I didn't feel like figuring that out because all of the references I could find about Fortran format specifiers were from ancient plain-HTML pages titled things like "FORTRAN 77 INTRINSIC SUBROUTINES REFERENCE" and hosted on sites like `web.math.utk.edu` where they probably _do_ date back to something approaching 1977.
So instead, I decided to just make it dumber.
```fortran
program advent04
implicit none
character, dimension(140, 140) :: grid
integer :: i
grid = load()
do i = 1, 140
print *, grid(i, :)
end do
contains
function load() result(grid)
implicit none
integer :: handle
character, dimension(140, 140) :: grid
character(140) :: line
integer :: row
integer :: col
open(newunit=handle, file="data/04.txt", status="old", action="read")
do row = 1, 140
! `line` is a `character(140)` variable, so Fortran knows to look for 140 characters I guess
read(handle, *) line
do col = 1, 140
! just assign each character of the line to array elements individually
grid(row, col) = line(col:col)
end do
end do
close(handle)
end function load
end program advent04
```
I am more than sure that there are several dozen vastly better ways of accomplishing this, but look, it works and I'm tired of fighting Fortran. I want to go on to the fun part!
### The fun part
The puzzle specifies that occurrences of `XMAS` can be horizontal, verical, or even diagonal, and can be written either forwards or backwards. The obvious way to do this would be to scan through the array, stop on every `X` character and cheak for the complete word `XMAS` in each of the eight directions individually, with a bunch of loops. Simple, easy, and probably more than performant enough because this grid is only 140x140, after all.<Sidenote>Although AoC has a way of making the second part of the puzzle punish you if you were lazy and went with the brute-force approach for the first part, so we'll see how this holds up when we get there.</Sidenote>
But! This is Fortran, and Fortran's whole shtick is operations on arrays, especially multidimensional arrays. So I think we can make this a lot more interesting. Let's create a "test grid" that looks like this:
```
S . . S . . S
. A . A . A .
. . M M M . .
S A M X M A S
. . M M M . .
. A . A . A .
S . . S . . S
```
Which has all 8 possible orientationS of the word `XMAS` starting from the central X. Then, we can just take a sliding "window" of the same size into our puzzle grid and compare it to the test grid. This is a native operation in Fortran--comparing two arrays of the same size results in a third array whose elements are the result of each individual comparison from the original arrays. Then we can just call `count()` on the resulting array to get the number of true values, and we know how many characters matched up. Subtract 1 for the central X we already knew about, then divide by 3 since there are 3 letters remaining in each occurrence of `XMAS`, and Bob's your uncle, right?
...Wait, no. That won't work because it doesn't account for partial matches. Say we had a "window" that looked like this (I'm only showing the bottom-right quadrant of the window for simplicity):
```
X M X S
S . . .
A . . .
X . . .
```
If we were to apply the process I just described to this piece of the grid, we would come away thinking there was 1 full match of `XMAS`, because there are one each of `X`, `M`, `A`, and `S` in the right positions. Problem is, they aren't all in the right places to be part of the _same_ XMAS, meaning that there isn't actually a match here at all.
To do this properly, we need some way of distinguishing the individual "rays" of the "star", which is how I've started thinking about the test grid up above, so that we know whether _all_ of any given "ray" is present. So what if we do it this way?
1. Apply the mask to the grid as before, but this time, instead of just counting the matches, we're going to convert them all to 1s. Non-matches will be converted to 0.
2. Pick a prime number for each "ray" of the "star". We can just use the first 8 prime numbers (excluding 1, of course). Create a second mask with these values subbed in for each ray, and 1 in the middle. So the ray extending from the central X directly to the right, for instance, would look like this, assuming we start assigning our primes from the top-left ray and move clockwise: `1 7 7 7`
3. Multiply this array by the array that we got from our initial masking operation. Now any matched characters will be represented by a prime number _specific to that ray of the star_.
4. Convert all the remaining 0s in the resulting array to 1s, then take the product of all values in the array.
5. Test whether that product is divisible by the cube of each of the primes used. E.g. if it's divisible by 8, we _know_ that there must have been three 2's in the array, so we _know_ that the top-left ray is entirely present. So we can add 1 to our count of valid `XMAS`es originating at this point.
Will this work? Is it even marginally more efficient than the stupidly obvious way of just using umpty-gazillion nested for loops--excuse me, "do loops"--to test each ray individually? No idea! It sure does sound like a lot more fun, though.
Ok, first things first. Let's adjust the data-loading code to pad the grid with 3 bogus values on each edge, so that we can still generate our window correctly when we're looking at a point near the edge of the grid.
```fortran
grid = '.' ! probably wouldn't matter if we skipped this, uninitialized memory just makes me nervous
open(newunit=handle, file="data/04.txt", status="old", action="read")
do row = 4, 143
read(handle, *) line
do col = 1, 140
grid(row, col + 3) = line(col:col)
end do
end do
```
Turns out assigning a value element to an array of that type of value (like `grid = '.'` above) just sets every array element to that value, which is very convenient.
Now let's work on the whole masking thing.
Uhhhh. Wait. We might have a problem here. When we take the product of all values in the array after the various masking and prime-ization stuff, we could _conceivably end up multiplying the cubes of the first 8 prime numbers. What's the product of the cubes of the first 8 prime numbers?
```
912585499096480209000
```
Hm, ok, and what's the max value of a 64-bit integer?
```
9223372036854775807
```
Oh. Oh, _noooo_.
It's okay, I mean, uh, it's not _that_ much higher. Only two orders of magnitude, and what are the odds of all eight versions of `XMAS` appearing in the same window, anyway? Something like 1/4<sup>25</sup>? Maybe we can still make this work.
```fortran
integer function count_xmas(row, col) result(count)
implicit none
integer, intent(in) :: row, col
integer :: i
integer(8) :: prod
integer(8), dimension(8) :: primes
character, dimension(7, 7) :: test_grid, window
integer(8), dimension(7, 7) :: prime_mask, matches, matches_prime
test_grid = reshape( &
[&
'S', '.', '.', 'S', '.', '.', 'S', &
'.', 'A', '.', 'A', '.', 'A', '.', &
'.', '.', 'M', 'M', 'M', '.', '.', &
'S', 'A', 'M', 'X', 'M', 'A', 'S', &
'.', '.', 'M', 'M', 'M', '.', '.', &
'.', 'A', '.', 'A', '.', 'A', '.', &
'S', '.', '.', 'S', '.', '.', 'S' &
], &
shape(test_grid) &
)
primes = [2, 3, 5, 7, 11, 13, 17, 19]
prime_mask = reshape( &
[ &
2, 1, 1, 3, 1, 1, 5, &
1, 2, 1, 3, 1, 5, 1, &
1, 1, 2, 3, 5, 1, 1, &
19, 19, 19, 1, 7, 7, 7, &
1, 1, 17, 13, 11, 1, 1, &
1, 17, 1, 13, 1, 11, 1, &
17, 1, 1, 13, 1, 1, 11 &
], &
shape(prime_mask) &
)
window = grid(row - 3:row + 3, col - 3:col + 3)
matches = logical_to_int64(window == test_grid)
matches_prime = matches * prime_mask
prod = product(zero_to_one(matches_prime))
count = 0
do i = 1, 8
if (mod(prod, primes(i) ** 3) == 0) then
count = count + 1
end if
end do
end function count_xmas
elemental integer(8) function logical_to_int64(b) result(i)
implicit none
logical, intent(in) :: b
if (b) then
i = 1
else
i = 0
end if
end function logical_to_int64
elemental integer(8) function zero_to_one(x) result(y)
implicit none
integer(8), intent(in) :: x
if (x == 0) then
y = 1
else
y = x
end if
end function zero_to_one
```
Those `&`s are line-continuation characters, by the way. Apparently you can't have newlines inside a function call or array literal without them. And the whole `reshape` business is a workaround for the fact that there _isn't_ actually a literal syntax for multi-dimensional arrays, so instead you have to create a 1-dimensional array and "reshape" it into the desired shape.
Now we just have to put it all together:
```fortran
total = 0
do col = 4, 143
do row = 4, 143
if (grid(row, col) == 'X') then
total = total + count_xmas(row, col)
end if
end do
end do
print *, total
```
These `elemental` functions, by the way, are functions you can ~~explain to Watson~~ apply to an array element-wise. So `logical_to_int64(array)` returns an array of the same shape with all the "logical" (boolean) values replaced by 1s and 0s.
This actually works! Guess I dodged a bullet with that 64-bit integer thing.<Sidenote>Of course I discovered later, right before posting this article, that Fortran totally has support for 128-bit integers, so I could have just used those and not worried about any of this.</Sidenote>
I _did_ have to go back through and switch out all the `integer` variables in `count_xmas()` with `integer(8)`s (except for the loop counter, of course). This changed my answer significantly. I can only assume that calling `product()` on an array of 32-bit integers, then sticking the result in a 64-bit integer, does the multiplication as 32-bit first and only _then_ converts to 64-bit, after however much rolling-over has happened. Makes sense, I guess.
Ok, great! On to part 2!
## Part 2
It's not actually too bad! I was really worried that it was going to tell me to discount all the occurrences of `XMAS` that overlapped with another one, and that was going to be a royal pain the butt with this methodology. But thankfully, all we have to do is change our search to look for _two_ occurrences of the sequence `M-A-S` arranged in an X shape, like this:
```
M . S
. A .
M . S
```
This isn't too difficult with our current approach. Unfortunately it will require four test grids applied in sequence, rather than just one, because again the sequence can be written either forwards or backwards, and we have to try all the permutations. On the plus side, we can skip the whole prime-masking thing, because each test grid is going to be all-or-nothing now. In fact, we can even skip checking any remaining test grids whenver we find a match, because there's no way the same window could match more than one.
Hmm, I wonder if there's a way to take a single starting test grid and manipulate it to reorganize the characters into the other shapes we need?
Turns out, yes! Yes there is. We can use a combination of slicing with a negative step, and transposing, which switches rows with columns, effectively rotating and flipping the array. So setting up our test grids looks like this:
```fortran
character, dimension(3, 3) :: window, t1, t2, t3, t4
t1 = reshape( &
[ &
'M', '.', 'S', &
'.', 'A', '.', &
'M', '.', 'S' &
], &
shape(t1) &
)
t2 = t1(3:1:-1, :) ! flip t1 top-to-bottom
t3 = transpose(t1) ! swap t1 rows for columns
t4 = t3(:, 3:1:-1) ! flip t3 left-to-right
```
Then we can just compare the window to each test grid:
```fortran
window = grid(row - 1:row + 1, col - 1:col + 1)
if ( &
count_matches(window, t1) == 5 &
.or. count_matches(window, t2) == 5 &
.or. count_matches(window, t3) == 5 &
.or. count_matches(window, t4) == 5 &
) then
count = 1
else
count = 0
end if
```
To my complete and utter astonishment, this actualy worked the first time I tried it, once I had figured out all of the array-flipping-and-rotating I needed to create the test grids. It always makes me suspicious when that happens, but Advent of Code confirmed it, so I guess we're good!<Sidenote>Or I just managed to make multiple errors that all cancelled each other out.</Sidenote>
It did expose a surprisingly weird limitation in the Fortran parser, though. Initially I kept trying to write the conditions like this: `if(count(window == t1) == 5)`, and couldn't understand the syntax errors it was throwing. Finally I factored out `count(array1 == array2)` into a separate function, and everything worked beautifully. My best guess is that the presence of two `==` operators inside a single `if` condition, not separated by `.and.` or `.or.`, is just a no-no. The the things we learn.
## Lessons ~~and carols~~
(Whoa now, we're not _that_ far into Advent yet.)
Despite being one of the oldest programming languages still in serious use, Fortran manages to feel surprisingly familiar. There are definite archaisms, like having to define the types of all your variables at the start of your program/module/function,<Sidenote>Even throwaway stuff like loop counters and temporary values.</Sidenote>, having to declare function/subroutine names at the beginning _and end_, and the use of the word "subroutine". But overall it's kept up surprisingly well, given--and I can't stress this enough--that it's _sixty-six years old_. It isn't even using `CAPITAL LETTERS` for everything any more,<Sidenote>Although the language is pretty much case-insensitive so you can still use CAPITALS if you want.</Sidenote> which puts it ahead of SQL,<Sidenote>Actually, I suspect the reason the CAPITALS have stuck around in SQL is that more than most languages, you frequently find yourself writing SQL _in a string_ from another language. Occasionally editors will be smart enough to syntax-highlight it as SQL for you, but for the times they aren't, using `CAPITALS` for all the `KEYWORDS` serves as a sort of minimal DIY syntax highlighting. That's what I think, at least.</Sidenote> and SQL is 10+ years younger.
It still has _support_ for a lot of really old stuff. For instance, you can label statements with numbers and then `go to` a numbered statement, but there's really no use for that in new code. We have functions, subroutines, loops, if-else-if-else conditionals--basically everything you would (as I understand it) use `goto` for back in the day.
Runs pretty fast, too. I realized after I already had a working solution that I had been compiling without optimizations the whole time, so I decided to try enabling them, only to discover that the actual execution time wasn't appreciably different. I figured the overhead of spawning a process was probably eating the difference, so I tried timing just the execution of the main loop and sure enough, without optimizations it took about 2 milliseconds whereas with optimizations it was 690 microseconds. Whee! Native-compiled languages are so fun. I'm too lazy to try rewriting this in Python just to see how much slower it would be, but I'm _pretty_ sure that this time it would be quite noticeable.
Anyway, that about wraps it up for Fortran. My only remaining question is: What is the appropriate demonym for users of Fortran? Python has Pythonistas, Rust has Rustaceans, and so on. I was going to suggest "trannies" for Fortran users, but everyone kept giving me weird looks for some reason.

8
posts/after.mdx Normal file
View File

@@ -0,0 +1,8 @@
---
title: Example after post
date: 2026-02-28
---
## After
Lorem ipsum dolor sit amet.

8
posts/before.mdx Normal file
View File

@@ -0,0 +1,8 @@
---
title: Example previous post
date: 2026-02-21
---
## Before
Lorem ipsum dolor sit amet.

50
posts/test.mdx Normal file
View File

@@ -0,0 +1,50 @@
---
title: This Is A Top-Level Heading
date: 2026-02-27
---
import Sidenote from '@components/Sidenote.astro';
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut ac consectetur mi. Phasellus non risus vitae<Sidenote>hello world</Sidenote> lorem scelerisque semper vel eget arcu. Nulla id malesuada velit. Pellentesque eu aliquam nisi. Cras lacinia enim sit amet ante tincidunt convallis. Donec leo nibh, posuere nec arcu in, congue tempus turpis. Maecenas accumsan mauris ut libero molestie, eget ultrices est faucibus. Donec sed ipsum eget erat pharetra tincidunt. Integer faucibus diam ut cursus cursus.
## A Second-level heading
Nulla at pulvinar quam. Interdum et malesuada fames ac ante ipsum primis in faucibus. In pretium laoreet egestas. Phasellus ut congue ligula, ut egestas sapien. Etiam congue dui at libero placerat, vel accumsan nunc ultrices. In ullamcorper ut nunc a elementum. Vivamus vehicula ut urna sed congue.
### Now let's try a third-level heading
Fusce varius lacinia ultrices. Cras ante velit, sagittis a commodo ac, faucibus nec tortor. Proin auctor, sapien nec elementum vestibulum, neque dolor consectetur lectus, a luctus ante nunc eget ex. Vestibulum bibendum lacus nec convallis bibendum. Nunc tincidunt elementum nulla, sit amet lacinia libero cursus non. Nam posuere ipsum sit amet elit accumsan, cursus euismod ligula scelerisque. Nam mattis sollicitudin semper. Morbi lacinia nec mi vel tempus. Cras auctor dui et turpis laoreet, ut vehicula magna dapibus. Fusce sit amet elit eget dolor varius tempus sed sit amet massa.
Fusce blandit scelerisque massa nec ultrices. Phasellus a cursus ex, sed aliquet justo. Proin sit amet lorem et urna viverra consectetur. In consectetur facilisis nulla, id accumsan metus lacinia quis. Sed nec magna pellentesque, ultricies nisi in, maximus sem. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam facilisis viverra metus, vitae auctor mi venenatis id. Fusce et risus non leo iaculis lacinia vitae ut metus. Phasellus id suscipit nisl, nec sodales velit. Morbi aliquet eros a est condimentum convallis.
#### Heading 4 coming up
Aenean facilisis eu velit vel semper. Sed imperdiet, lorem ut sagittis laoreet, turpis lorem venenatis justo, vel rhoncus enim lorem nec leo. Vestibulum sagittis orci nisl, vulputate tempor sem mattis eget. Pellentesque volutpat turpis sit amet est ultricies maximus. Aliquam sollicitudin semper enim, quis viverra mauris congue blandit. Vestibulum massa dui, efficitur quis lectus eu, bibendum vehicula dui. Suspendisse elementum, tellus a facilisis tincidunt, nulla leo viverra lacus, at lobortis massa ante non ex. Duis sed pretium nibh, eget molestie diam. Suspendisse congue augue metus, pellentesque faucibus magna auctor a. Praesent et sapien quis urna sollicitudin dapibus a ac justo. Integer lobortis, magna ac consectetur egestas, sapien dolor aliquet diam, ut tempor lectus metus sit amet libero. Fusce neque dui, mollis ac dui eget, iaculis semper lectus. Donec porttitor ante mauris, id condimentum neque ultrices in. Suspendisse commodo congue posuere.
##### Finally, we get to heading 5
Praesent non dignissim purus. Ut pharetra lectus sit amet tempor dapibus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Cras dapibus libero vel enim consequat interdum. Aliquam ac est mollis, ornare dui sed, efficitur felis. Morbi aliquam neque neque, at facilisis arcu suscipit convallis. Integer pulvinar dui lectus, et luctus sapien porttitor nec. Vivamus placerat ultrices consectetur. Etiam molestie non nibh ut viverra. Suspendisse potenti. In sagittis leo non commodo ultricies. Donec vitae ultrices lacus, id mattis purus. Nulla ante lacus, auctor vitae enim sit amet, placerat placerat orci. In tempor eget nunc eget accumsan.
Fusce venenatis dolor at tincidunt commodo. Ut lacinia eu arcu eget pellentesque. Integer a nibh nisi. Phasellus semper quam at lacus finibus pellentesque. Sed porta varius imperdiet. Aenean tempor tellus odio, id sollicitudin neque pellentesque nec. Pellentesque vel ultrices felis. Vivamus eleifend quis leo nec tincidunt. Etiam magna quam, viverra non est et, vestibulum porta tellus. Nam nisi orci, pretium a quam id, malesuada sollicitudin mauris.
Nulla placerat, sem eget bibendum tincidunt, dui tortor ullamcorper ipsum, ut sollicitudin nibh tortor ac mi. Donec efficitur interdum ullamcorper. Cras ac molestie risus, non volutpat erat. Donec vel dignissim velit. Aliquam et turpis eget lorem cursus porttitor eu at ipsum. Nunc vitae leo non quam pharetra iaculis auctor et diam. Nullam convallis quam eu aliquet elementum. Etiam consectetur maximus tincidunt. Vivamus nulla risus, viverra nec mauris at, aliquet sagittis lorem. Duis tempor nunc sem, eget euismod urna porta sed.
## Another second-level heading to test TOC
Phasellus fermentum turpis vel porta vestibulum. Cras sed nisl at magna lacinia finibus tincidunt vitae massa. Maecenas lobortis, sapien non interdum placerat, arcu massa molestie lectus, eget ultricies dolor tortor nec nisl. Aliquam elementum facilisis nisi. Quisque lobortis tincidunt mauris, vel facilisis felis faucibus id. Sed pharetra ante ut quam fringilla fermentum. Nunc porta laoreet dui, ac vestibulum elit varius non. Quisque at risus cursus, mollis nisi vel, lacinia erat. In metus erat, iaculis vitae velit et, gravida hendrerit enim. Integer eget ipsum sit amet tellus ultrices congue. Curabitur ullamcorper vehicula eros, vel aliquam est egestas quis. Cras volutpat, nisi eu ultrices pretium, diam sapien dapibus orci, et tristique nulla lectus semper lectus.
Quisque ut pellentesque eros. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec blandit orci quis iaculis dignissim. Nulla facilisi. Suspendisse vehicula aliquet odio quis feugiat. Maecenas sed aliquam lectus. Nulla ut cursus dolor. Sed iaculis vulputate finibus. Proin non posuere nunc. Sed blandit nisi et euismod hendrerit. Nullam id urna dapibus, placerat mauris auctor, tristique leo. Phasellus mollis, sem vel viverra blandit, nisl nisi suscipit lorem, ut varius orci enim non sapien. Quisque at tempus ipsum. Nulla sit amet accumsan lacus.
### And another third-level heading
Mauris a porttitor justo. Sed eu maximus turpis, eu porta lectus. Morbi quis ullamcorper libero. Cras vehicula quis lorem non varius. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse sit amet egestas odio. Quisque consectetur nunc enim, a molestie nibh congue id. Quisque efficitur accumsan luctus.
Duis viverra odio at dolor semper eleifend. Pellentesque tincidunt augue ultrices lobortis sodales. In semper felis lacus, vel fermentum dolor aliquet at. Aliquam tristique sagittis consequat. Maecenas sodales odio et mauris pulvinar varius. Phasellus imperdiet, magna id gravida efficitur, dui ipsum viverra odio, ut porttitor elit elit sed nisl. Vestibulum vestibulum eros et nisl tempus, ut dictum libero blandit. Sed tempor scelerisque elit non accumsan.
Fusce eget posuere diam, ut venenatis tellus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam quis maximus sem. Nam convallis euismod odio, feugiat volutpat risus sollicitudin eget. Vivamus et imperdiet tortor. Mauris fringilla metus eu eros convallis fringilla. Quisque sodales consequat auctor. Ut vitae ligula porttitor, consectetur mauris sit amet, scelerisque leo. Donec accumsan libero vel nulla placerat, a sagittis turpis pharetra. Fusce rutrum metus nunc. Etiam consequat lobortis blandit. Nunc sed odio ullamcorper, hendrerit dolor vel, euismod dui. Donec id bibendum est. Suspendisse massa ante, pretium ac orci et, tempus vehicula velit.
Aliquam feugiat interdum suscipit. Donec ac erat maximus, aliquam ligula sed, lacinia velit. Praesent a commodo ligula. Phasellus sed felis vel quam dictum facilisis. Nulla justo diam, tempus a efficitur ut, euismod non nisi. Pellentesque tristique rutrum erat, eu placerat tortor malesuada eu. Maecenas mollis mauris metus, sed pellentesque nisl posuere ut.
Donec malesuada sit amet diam rhoncus porttitor. Integer arcu elit, vestibulum ac condimentum a, scelerisque eget ligula. Aenean ultricies suscipit urna, at commodo tellus interdum nec. Sed feugiat nunc urna, non tincidunt libero elementum vel. Vestibulum non tincidunt ligula. Ut eu consectetur risus. Mauris vehicula ligula eget lacinia tempus.
Donec suscipit erat dui, eu porttitor augue condimentum vel. Sed et massa lacinia purus fermentum ultrices elementum sit amet tortor. Quisque feugiat, nulla non auctor egestas, sem odio mollis nisi, eget accumsan ante ipsum ac ex. Integer lobortis mi nunc, interdum blandit erat feugiat ac. Donec aliquam, felis in ultricies rhoncus, sem eros elementum lorem, non congue enim nunc quis ligula. Nulla facilisi. Vestibulum vitae justo ac justo porttitor rutrum a nec tellus. Duis congue lorem a semper maximus. Quisque consectetur dictum tellus, vel lobortis lorem sodales nec. Pellentesque sed enim felis. Aliquam non mattis sapien.

View File

@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="preload" href="/Tajawal-Regular.woff2" as="font" type="font/woff2" />
<link rel="icon" href="/favicon.png" />
<link rel="stylesheet" href="/style.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%svelte.head%
</head>
<body>
<div id="svelte">%svelte.body%</div>
</body>
</html>

23
src/components/Icon.astro Normal file
View File

@@ -0,0 +1,23 @@
---
export interface Props {
name: string,
};
const { name } = Astro.props;
const icons = import.meta.glob<{string: string}>('@components/icons/*.svg', { query: '?raw', import: 'default' });
const path = `/src/components/icons/${name}.svg`;
if (icons[path] === undefined) {
throw new Error(`Icon ${name} does not exist.`);
}
const icon = await icons[path]();
---
<Fragment set:html={icon} />
<style>
svg {
width: 100%;
height: 100%;
}
</style>

124
src/components/Post.astro Normal file
View File

@@ -0,0 +1,124 @@
---
import '@fontsource-variable/baskervville-sc';
import type { CollectionEntry } from 'astro:content';
import { render } from 'astro:content';
import Toc from '@components/Toc.vue';
import { formatDate } from '@lib/datefmt';
export interface Props {
entry: CollectionEntry<'posts'>,
prevSlug: string | null,
nextSlug: string | null,
};
const { entry, prevSlug, nextSlug } = Astro.props;
const { Content, headings } = await render(entry);
---
<style>
/* 3-column grid: left gutter, center content, and right gutter */
article {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, var(--content-width)) minmax(0, 1fr);
/* a bit of breathing room for narrow screens */
padding: 0 var(--content-padding);
}
h1 {
font-family: 'Baskervville SC Variable';
}
#left-gutter {
grid-column: 1 / 2;
justify-self: end;
}
#right-gutter {
grid-column: 3 / 4;
justify-self: start;
}
.title {
grid-column: 2 / 3;
}
.subtitle {
font-size: 0.85em;
font-style: italic;
margin-top: -0.25rem;
}
.post {
grid-column: 2 / 3;
}
footer {
grid-column: 2 / 3;
margin-bottom: 2.5rem;
display: flex;
justify-content: space-between;
& a {
font-size: 1.25rem;
color: var(--content-color-faded);
text-decoration: underline;
text-underline-offset: 0.25em;
text-decoration-color: transparent;
transition: 150ms;
&:hover {
text-decoration-color: currentColor;
text-decoration: underline;
}
}
}
article {
& :global(section.post::first-letter) {
font-family: 'Baskervville';
color: var(--accent-color);
}
&[data-dropcap-style="descender"] :global(section.post::first-letter) {
initial-letter: 2;
margin-right: 0.5rem;
}
&[data-dropcap-style="ascender"] :global(section.post::first-letter) {
font-size: 2em;
line-height: 1;
}
}
</style>
<article class="prose" data-dropcap-style={entry.data.dropcap}>
<header class="title">
<h1>
<!-- <SmallCaps text={entry.data.title} upperWeight={500} lowerWeight={800} /> -->
{ entry.data.title }
</h1>
<p class="subtitle">{ formatDate(entry.data.date) }</p>
</header>
<div id="left-gutter">
<Toc client:load {headings} />
</div>
<section class="post">
<Content />
</section>
<div id="right-gutter" />
<footer>
{prevSlug && (
<a href={`/${prevSlug}`} data-astro-prefetch>Older</a>
)}
{nextSlug && (
<a href={`/${nextSlug}`} data-astro-prefetch>Newer</a>
)}
</footer>
</article>

View File

@@ -0,0 +1,128 @@
---
import Icon from '@components/Icon.astro';
const id = crypto.randomUUID();
SIDENOTE_COUNT += 1
---
<label for={id} class="counter anchor">{ SIDENOTE_COUNT }</label>
<input {id} type="checkbox" class="toggle" />
<!-- we have to use spans for everything, otherwise Astro "helpfully" inserts
ending </p> tags before every sidenote because you technically can't have
another block-level element inside a <p> -->
<span class="sidenote">
<span class="content">
<span class="counter floating">{ SIDENOTE_COUNT }</span>
<slot />
</span>
<button class="dismiss">
<label for={id}>
<Icon name="chevron-down" />
</label>
</button>
</span>
<style>
.sidenote {
display: block;
position: relative;
font-size: var(--content-size-sm);
hyphens: auto;
/* note: our minimum desirable sidenote width is 15rem, and the gutters are symmetrical, so our
breakpoint between desktop/mobile will be content-width + 2(gap) + 2(15rem) + (scollbar buffer) */
@media(min-width: 89rem) {
--gap: 2.5rem;
--gutter-width: calc(50vw - var(--content-width) / 2);
--scrollbar-buffer: 1.5rem;
--sidenote-width: min(
24rem,
calc(var(--gutter-width) - var(--gap) - var(--scrollbar-buffer))
);
width: var(--sidenote-width);
float: right;
clear: right;
margin-right: calc(-1 * var(--sidenote-width) - var(--gap));
margin-bottom: 0.75rem;
}
@media(max-width: 89rem) {
position: fixed;
left: 0;
right: 0;
bottom: 0;
/* horizontal buffer for the counter and dismiss button */
--padding-x: calc(var(--content-padding) + 1.5rem);
padding: 1rem var(--padding-x);
background-color: var(--bg-color);
box-shadow: 0 -2px 4px -1px rgba(0, 0, 0, 0.06), 0 -2px 12px -2px rgba(0, 0, 0, 0.1);
/* show the sidenote only when the corresponding checkbox is checked */
transform: translateY(calc(100% + 2rem));
transition: transform 0.125s;
/* when moving from shown -> hidden, ease-in */
transition-timing-function: ease-in;
.toggle:checked + & {
border-top: 2px solid var(--accent-color);
transform: translateY(0);
/* when moving hidden -> shown, ease-out */
transition-timing-function: ease-out;
/* the active sidenote should be on top of any other sidenotes as well
(this isn't critical unless you have JS disabled, but it's still annoying) */
z-index: 20;
}
}
}
.content {
display: block;
max-width: var(--content-width);
margin: 0 auto;
}
.counter.anchor {
color: var(--accent-color);
font-size: 0.75em;
margin-left: 0.065rem;
position: relative;
bottom: 0.375rem;
}
.counter.floating {
position: absolute;
/* move it out to the left by its own width + a fixed gap */
transform: translateX(calc(-100% - 0.4em));
color: var(--accent-color);
}
.dismiss {
display: block;
width: 2rem;
margin: 0.5rem auto 0;
color: var(--neutral-gray);
border-radius: 100%;
background: transparent;
border: 1px solid var(--neutral-gray);
padding: 0.25rem;
&:hover, &:active {
color: var(--accent-color);
border-color: var(--accent-color);
}
cursor: pointer;
& label {
cursor: pointer;
}
}
/* this is just to track the state of the mobile sidenote, it doesn't need to be seen */
.toggle {
display: none;
}
</style>

View File

@@ -0,0 +1,73 @@
---
import Icon from '@components/Icon.astro';
---
<div class="theme-switcher">
<button id="switch-to-dark">
<Icon name="sun" />
</button>
<button id="switch-to-light">
<Icon name="moon" />
</button>
</div>
<style>
.theme-switcher {
position: relative;
isolation: isolate;
width: 1.5rem;
height: 1.5rem;
transform: translateY(0.1rem);
}
button {
position: absolute;
inset: 0;
background-color: transparent;
padding: 0;
color: var(--nav-link-color);
border: none;
&:hover {
cursor: pointer;
color: var(--accent-color);
}
/* hide by default, i.e. if JS isn't enabled and the data-theme attribute didn't get set, */
visibility: hidden;
opacity: 0;
transition:
color 0.2s ease,
opacity 0.5s ease,
transform 0.5s ease;
}
:global(html[data-theme="light"]) button#switch-to-dark {
opacity: 1;
visibility: visible;
transform: rotate(360deg);
/* whichever one is currently active should be on top */
z-index: 10;
}
:global(html[data-theme="dark"]) button#switch-to-light {
opacity: 1;
visibility: visible;
transform: rotate(-360deg);
z-index: 10;
}
</style>
<script>
document.getElementById('switch-to-dark')?.addEventListener('click', () => {
localStorage.setItem('theme-preference', 'dark');
document.documentElement.dataset.theme = 'dark';
});
document.getElementById('switch-to-light')?.addEventListener('click', () => {
localStorage.setItem('theme-preference', 'light');
document.documentElement.dataset.theme = 'light';
})
</script>

163
src/components/Toc.vue Normal file
View File

@@ -0,0 +1,163 @@
<script setup lang="ts">
import type { MarkdownHeading } from 'astro';
import { onBeforeUnmount, onMounted, ref } from 'vue';
const props = defineProps<{ headings: MarkdownHeading[] }>();
// headings deeper than h3 don't display well because they are too deeply indented
const headings = props.headings.filter(h => h.depth <= 3);
// for each heading slug, track whether the corresponding heading is above the cutoff point
// (the cutoff point being a hypothetical line 2/3 of the way up the viewport)
let headingStatuses = Object.fromEntries(headings.map(h => ([h.slug, false])));
// we need to store a reference to the observer so we can dispose of it on resize/unmount
let headingObserver: IntersectionObserver | null = null;
// the final slug that should be highlighted as "current" in the TOC
let currentSlug = ref('');
function handleIntersectionUpdate(entries: IntersectionObserverEntry[], headingElems: HTMLElement[]) {
for (const entry of entries) {
const slug = entry.target.id;
const status = entry.isIntersecting;
headingStatuses[slug] = status;
}
// headings are in DOM order, so this gives us the last heading that's still above the cutoff point
for (const elem of headingElems) {
if (headingStatuses[elem.id]) {
currentSlug.value = elem.id;
}
else {
break;
}
}
}
function setupObserver() {
// if there was already an observer, turn it off
if (headingObserver) {
headingObserver.disconnect();
}
const headingElems = headings.map(h => document.getElementById(h.slug)!);
const obs = new IntersectionObserver(
entries => handleIntersectionUpdate(entries, headingElems),
// top margin equal to body height means that the intersection zone extends up beyond
// the top of the document, i.e. elements can only enter/leave the zone at the bottom
{ rootMargin: `${document.body.clientHeight}px 0px -66% 0px` },
);
for (const elem of headingElems) {
obs.observe(elem);
}
headingObserver = obs;
}
onMounted(() => {
// create the observer once on component startup
setupObserver();
// any time the window resizes, the document height could change, so we need to recreate it
window.addEventListener('resize', setupObserver);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', setupObserver);
headingObserver?.disconnect();
});
</script>
<template>
<div id="toc">
<h5>C<span class="lower">ontents</span></h5>
<ul id="toc-list">
<li
v-for="heading in headings"
:data-current="heading.slug == currentSlug"
:style="`--depth: ${heading.depth}`"
>
<span v-show="heading.slug == currentSlug" class="marker"></span>
<a :href="`#${heading.slug}`">
{{ heading.text }}
</a>
</li>
</ul>
</div>
</template>
<style scoped>
#toc {
position: sticky;
top: 1.5rem;
margin-left: 1rem;
margin-right: 4rem;
max-width: 18rem;
font-size: var(--content-size-sm);
color: var(--content-color-faded);
/*
minimum desirable TOC width is 8rem
add 4rem for margins, giving total gutter width of 12.5rem
multiply by 2 since there are two equally-sized gutters, then add content-width (52.5rem)
*/
@media(max-width: 77.5rem) {
display: none;
}
}
h5 {
font-variant: petite-caps;
font-weight: 350;
font-size: var(--content-size);
font-family: 'Figtree Variable';
color: var(--content-color-faded);
max-width: fit-content;
margin-top: 0;
margin-bottom: 0.35em;
/*padding-bottom: 0.25em;*/
border-bottom: 1px solid currentcolor;
/* make the border stretch beyond the text just a bit, because I like the effect */
padding-right: 1.5rem;
/* SmallCaps is an Astro component so we can't use it here, but we can fake it */
& .lower {
font-weight: 500;
}
}
li {
position: relative;
margin-top: 0.45em;
margin-left: calc(0.75em * (var(--depth) - 2));
font-size: var(--content-size-sm);
/* make sure that one item wrapped across multiple lines doesn't just look like multiple items */
line-height: 1.15;
&[data-current="true"], &:hover {
color: var(--content-color);
}
}
.marker {
position: absolute;
left: -0.6rem;
top: 0.05em;
bottom: 0.2em;
width: 0.125rem;
background-color: var(--accent-color);
}
a {
color: inherit;
text-decoration: none;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
</style>

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>

After

Width:  |  Height:  |  Size: 210 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
</svg>
<style>
svg {
width: 100%;
height: 100%;
}
</style>

After

Width:  |  Height:  |  Size: 425 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
</svg>

After

Width:  |  Height:  |  Size: 377 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>

After

Width:  |  Height:  |  Size: 204 B

19
src/content.config.ts Normal file
View File

@@ -0,0 +1,19 @@
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const posts = defineCollection({
loader: glob({ pattern: '*.mdx', base: './posts' }),
schema: z.object({
title: z.string(),
date: z.date(),
draft: z.boolean().default(false),
dropcap: z.enum(['ascender', 'descender']).default('descender'),
toc: z.boolean().default(true),
})
});
export const collections = { posts };

1
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare var SIDENOTE_COUNT: number;

View File

@@ -0,0 +1,82 @@
---
import '@styles/main.css';
import '@fontsource-variable/baskervville-sc';
import ThemeSwitcher from '@components/ThemeSwitcher.astro';
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<!-- avoid FOUC by setting the color schme here in the header -->
<script>
const explicitPref = localStorage.getItem('theme-preference');
if (explicitPref) {
document.documentElement.dataset.theme = explicitPref;
} else {
const isLight = window.matchMedia('(prefers-color-scheme: light)').matches;
document.documentElement.dataset.theme = isLight ? 'light' : 'dark';
}
</script>
{/* Note: The styles are inside the document here because otherwise it breaks Astro's parsing */}
<style>
header {
background-color: var(--primary-color-faded);
padding: 0.5rem var(--content-padding);
}
nav {
max-width: var(--content-width);
margin: 0 auto;
display: flex;
gap: 1.5rem;
align-items: baseline;
& a {
font-family: 'Figtree Variable';
font-weight: 500;
font-size: 1.3rem;
color: var(--nav-link-color);
text-decoration: underline;
text-underline-offset: 0.5rem;
text-decoration-color: transparent;
transition: text-decoration-color 0.2s ease, opacity 0.2s ease;
&.home {
font-family: 'Baskervville SC Variable';
font-size: 2rem;
text-decoration-thickness: 0.125rem;
margin-right: auto;
}
&:hover, &:active {
text-decoration-color: var(--accent-color);
}
}
}
.switcher-container {
align-self: center;
}
</style>
</head>
<body>
<header>
<nav>
<a href="/" class="home" data-astro-prefetch>Joe's Blog</a>
<div class="switcher-container">
<ThemeSwitcher />
</div>
<a href="/posts" data-astro-prefetch>Posts</a>
<a href="/about" data-astro-prefetch>About</a>
</nav>
</header>
<main>
<slot />
</main>
</body>
</html>

View File

@@ -1,28 +0,0 @@
<script context="module">
import { page } from '$app/stores';
function host(url) {
try {
let u = new URL(url);
return u.host;
}
catch {
return null;
}
}
</script>
<script>
export let href; // we don't care about other attributes
</script>
{#if href.startsWith('/') || host(href) === $page.host}
<a sveltekit:prefetch {href}>
<slot></slot>
</a>
{:else}
<a {href}>
<slot></slot>
</a>
{/if}

View File

@@ -1,20 +0,0 @@
<script context="module">
import { formatDate } from './datefmt.js';
import { makeSlug } from '$lib/slug.js';
import Link from './Link.svelte';
export { Link as a };
</script>
<script>
export let title, date;
</script>
<svelte:head>
<title>{title}</title>
</svelte:head>
<div id="post">
<h1 id="{makeSlug(title)}">{title}</h1>
<p><em>{formatDate(date)}</em></p>
<slot></slot>
</div>

View File

@@ -1,97 +0,0 @@
<style lang="scss">
/* always applicable */
:global(body) {
counter-reset: sidenote;
}
.counter {
counter-increment: sidenote;
color: #444;
&:after {
font-size: 0.75rem;
position: relative;
bottom: 0.3rem;
}
}
.sidenote {
font-size: 0.8rem;
&:before {
content: counter(sidenote) " ";
position: relative;
font-size: 0.75rem;
bottom: 0.2rem;
}
}
.sidenote-toggle {
display: none;
}
/* desktop display */
@media(min-width: 70em) {
.counter:after {
content: counter(sidenote);
}
.sidenote {
position: absolute;
left: calc(50vw + var(--content-width) / 2 + 1rem);
max-width: 12rem;
}
}
/* mobile display */
@media (max-width: 70em) {
.counter:after {
content: "[" counter(sidenote) "]";
}
.counter:hover:after {
color: #000;
cursor: pointer;
}
.sidenote {
box-sizing: border-box;
position: fixed;
left: 0;
bottom: 0;
width: 100vw;
padding-top: 1rem;
padding-bottom: 1rem;
padding-right: 2rem;
background-color: #fff;
box-shadow: 0 -2px 4px -1px rgba(0, 0, 0, 0.06), 0 -2px 12px -2px rgba(0, 0, 0, 0.1);
display: none;
}
.sidenote-toggle:checked + .sidenote {
display: block;
}
}
/* slight tweaks for in between state */
@media (min-width: 52.5em) and (max-width: 70em) {
.sidenote {
padding-left: calc(50vw - 19rem);
}
}
@media (max-width: 52.5em) {
.sidenote {
padding-left: 2rem;
}
}
</style>
<script>
let id = Math.random().toString().slice(2);
</script>
<label for={id} class="counter"></label>
<input type="checkbox" class="sidenote-toggle" {id}/>
<span class="sidenote">
<slot></slot>
</span>

View File

@@ -17,15 +17,11 @@ const weekdays = [
];
export function formatDate(timestr) {
const datestr = timestr.slice(0, 10);
const [year, month, monthday] = datestr.split('-').map(n => parseInt(n));
// for some reason the Date constructor expects the month index instead of ordinal
const weekdayIdx = new Date(year, month - 1, monthday).getDay();
const names = {
month: months[month - 1],
monthday: ordinals[monthday - 1],
weekday: weekdays[weekdayIdx],
}
return `${names.weekday}, the ${names.monthday} of ${names.month}, A.D. ${year}`;
export function formatDate(date: Date) {
const year = date.getFullYear();
const month = months[date.getMonth() - 1];
const monthday = ordinals[date.getDate() - 1];
const weekday = weekdays[date.getDay() - 1];
return `${weekday}, the ${monthday} of ${month}, A.D. ${year}`;
}

View File

@@ -1,50 +0,0 @@
const nonAlphaNum = /[^A-Za-z0-9\-]/g;
const space = /\s/g
export function makeSlug(text) {
return text
.toLowerCase()
.replace(space, '-')
.replace(nonAlphaNum, '')
}
function apply(node, types, fn) {
if (typeof types === 'string') {
types = new Set([types]);
}
else if (!(types instanceof Set)) {
types = new Set(types)
console.log(types)
}
if (types.has(node.type)) {
fn(node);
}
if ('children' in node) {
for (let child of node.children) {
apply(child, types, fn);
}
}
}
function getTextContent(node) {
let segments = [];
apply(node, 'text', textNode => {
// skip all-whitespace strings
if (textNode.value.match(/^\s+$/)) return;
segments.push(textNode.value.trim());
});
return segments.join(' ');
}
export default function slug() {
return (tree) => {
apply(tree, 'element', e => {
if (e.tagName.match(/h[1-6]/)) {
let text = getTextContent(e);
e.properties.id = makeSlug(text);
}
})
}
}

7
src/middleware.ts Normal file
View File

@@ -0,0 +1,7 @@
import { defineMiddleware } from 'astro:middleware';
// set SIDENOTE_COUNT to 0 at the start of every request so that as sidenotes are rendered, it only counts them on the current page
export const onRequest = defineMiddleware((_context, next) => {
globalThis.SIDENOTE_COUNT = 0;
return next();
})

27
src/pages/[slug].astro Normal file
View File

@@ -0,0 +1,27 @@
---
import { getCollection } from 'astro:content';
import BaseLayout from '@layouts/BaseLayout.astro';
import Post from '@components/Post.astro';
export async function getStaticPaths() {
const entries = await getCollection('posts');
entries.sort((a, b) => a.data.date.getTime() - b.data.date.getTime())
// for each route, the page gets passed the entry itself, plus the previous and next slugs
// (if any), so that it can render links to them
return entries.map((entry, idx) => {
const prevSlug = entries[idx - 1]?.id || null;
const nextSlug = entries[idx + 1]?.id || null;
return {
params: { slug: entry.id },
props: { entry, prevSlug, nextSlug },
}
});
}
---
<BaseLayout>
<Post {...Astro.props} />
</BaseLayout>

7
src/pages/index.astro Normal file
View File

@@ -0,0 +1,7 @@
---
import BaseLayout from '@layouts/BaseLayout.astro';
---
<BaseLayout>
<p>Index file</p>
</BaseLayout>

View File

@@ -1,25 +0,0 @@
<script context="module">
export async function load({ url, params }) {
try {
let post = await import(`./_posts/${params.slug}.svx`);
return {
props: {
post: post.default
}
}
}
catch (err) {
console.log(err);
return {
status: 404,
error: `Not found: ${url.pathname}`,
}
}
}
</script>
<script>
export let post;
</script>
<svelte:component this={post} />

View File

@@ -1,45 +0,0 @@
<style>
:global(main) {
--content-width: 42rem;
box-sizing: border-box;
max-width: var(--content-width);
margin: 0 auto;
padding: 0 15px;
}
#header {
background-color: #4f5f68;
}
#nav-main {
max-width: 30rem;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(3, minmax(6rem, 8rem));
justify-content: space-between;
}
#nav-main a {
font-size: 1.5rem;
color: white;
text-decoration: none;
text-align: center;
padding: 0.25rem 0;
}
#nav-main a:hover {
background-color: #00000025;
}
</style>
<div id="header">
<nav id="nav-main">
<a sveltekit:prefetch href="/">Home</a>
<a sveltekit:prefetch href="/posts">Posts</a>
<a sveltekit:prefetch href="/">About</a>
</nav>
</div>
<main>
<slot></slot>
</main>

View File

@@ -1,33 +0,0 @@
---
title: Imagining A Passwordless Future
description: Can replace passwords with something more user-friendly?
date: 2021-04-30
---
<script>
import Sidenote from '$lib/Sidenote.svelte';
</script>
Passwords are the *worst*.
How many times have you groaned becuase *yet another* password-related thing
was messed up? Forgotten passwords, passwords that you're *sure* you wrote
down but can't find for some reason, passwords that you definitely *did* make
a record of but the site is inexplicably refusing to accept, passwords that
get silently truncated because your bank is still using 3DES for some reason,
the list goes on. It's constant point of pain for almost everyone, and even
after 20+ years of trying to make it work we *still* haven't figured out a
foolproof method. Password managers help, but they aren't perfect. How many
times have you created a password for a new account somewhere, saved it, and
then discovered that your save didn't go through - maybe it didn't meet the
requirements (because your 24-character string of gibberish didn't includ a s
p e c i a l c h a r a c t e r), or maybe your cable box got hit by
lightning just as you clicked Save, or *whatever*. The fact is that passwords
are a pain, and it seems to be a pretty intractable problem.
You know what aren't a pain, or at least not nearly to the same extent? Keys.
That's right, physical stick-em-in-a-lock-and-turn metal keys. They've been
around since forever,<Sidenote>This is an example sidenote.</Sidenote>
and I doubt they'll be going anywhere any time soon.
I really hate passwords.
I use them, of course, because I can't not. And I use a password manager, because to my mind that's the current best compromise between being secure and absolutely losing your mind, but it still isn't great. Sometimes my password manager bugs out and refuses to auto-fill the password box, so I have to go hunt it down and copy-paste it in.<Sidenote>If I'm lucky. If I'm unlucky, the site will have disabled pasting into password inputs because "security," and I'm stuck having to type in a 16-character string of gibberish on a mobile phone, because that's how life is.</Sidenote> Other times I'll create a password, the password manager will happily file it away, and then I'll discover that it didn't meet the site's requirements, because my auto-generated gibberish string didn't include the *right* special characters, and now I have the wrong password saved.

View File

@@ -1,130 +0,0 @@
---
title: Let's Design A Simpler SocketIO
date: 2021-10-16
description: >-
SocketIO is packed with features.
But do we really need all of them all the time?
---
Listen, don't get me wrong. SocketIO is great. It provides tons of features,
fantastic platform support, is widely deployed by a hugely diverse set of
companies, and has been around long enough that it probably has most of the
easily-encountered bugs ironed out.
So why wouldn't you want to use it? Well, a couple of reasons occur to me.
One, it's not exactly small. The unpacked library weighs in at just over 1MB,
which isn't a lot if it's a core component of your application (e.g. if
you're building a real time chat app) but is a bit much if you're only
looking for a small subset of its features.
Two, it's reasonably complex. Again, not insurmountably so, but complex enough
that it probably isn't worth hacking together a basic SocketIO client in your
REPL of choice if you just want to test something real quick. And on the
server side, it's complex enough that you'll probably want to avoid rolling
your own if possible. This becomes especially troublesome if you already have
a working application and you just want to sprinkle in a little real-time
interactivity, rather than building your whole application around that
assumption. In my (admittedly limited) experience, the existing SocketIO
integrations don't always play very nicely with the existing server
frameworks, although if you stick to the most widely-used stuff you're
probably fine.
And honestly, it's just a lot of complexity to introduce if you just want a
simple event stream. You could argue that you don't even need websockets for
this - Server Sent Events are a thing, as are simple HTTP requests with a
streaming response - but in this day and age, the set of solutions to the
problem of "persistent communication between server and client" has pretty
firmly coalesed around websockets. They're there, they're supported, there
are lots of libraries - you might as well just go with the flow.
## Ok, so what are you saying?
Basically, that we need something that's _like_ SocketIO but lighter-weight,
and solves a more limited set of problems. Specifically, the problem I'm
looking to solve is _event streams_ - given a web service and a client, how
does the client detect that _things are happening_ in the background so that
it can update itself accordingly?
The use cases for this are pretty extensive. Most obviously, you can use it to
implement a notifications system on pretty much any webapp that
involves "users want to know when things happen," which is pretty broad.
Maybe you're running an ecommerce shop and you want to notify your customers
that the item they've had their eye on is back in stock. Or you've just
opened up a new promotion and they should check it out. Maybe you're running
a website that displays a stock ticker, and you need up-to-the-second data on
stock prices. Or you've got a dashboard with some kind of real-time
monitoring chart, whatever it's measuring, and you want to keep it up to date
with a minimum of overhead. Pub/sub is a powerful concept, which is why
people keep re-implementing it. Like I'm doing here. Get over it.
## But why can't I just use SocketIO for this, again?
I mean, you _can._ But SocketIO does so much _more_ than simple pub/sub.
Connection multiplexing, automatic reconnects, receipt acknowledgements, the
list goes on. All of these features are great if, again, you are implementing
an instant messenger. They even have a feature called "rooms," which mentions
in its documentation that it "makes it easy to implement private messages,"
among other things, so it's pretty clear who their target is.
And it's a great target! Lots of people need instant messaging. Every website
in the world seems to pop up a bubble saying "Hi, I'm [friendly-sounding
name]! Do you want to talk about giving us money?" 30 seconds after you hit
their page. Everyone with a customer-service team has discovered or will soon
discover that most issues are easier to resolve over text than over the
phone, especially if your CS is outsourced to some foriegn country so your
reps all have an accent. SocketIO exists for a reason, and it's a very good
reason, and if that's what you need then go for it, knock yourself out.
But if all you need is a simple event stream with pub/sub semantics, then keep
reading, because that's what I want to talk about.
## Fine. Make your pitch, I'll listen.
The protocol I'm imagining should solve three basic problems:
* Authentication
* Connection management (keepalive, automatic reconnects)
* ...and the actual pub/sub itself, of course.
Let's go through each of these in turn.
### Authentication
The protocol purists might start to set up a bit of a racket here. Ignore
those guys, they suck. Listen, every web-based protocol in the world should
at least be _aware_ of the question of authentication. Maybe the awareness
should stop at "that's being handled so I don't need to think about it," but
at least that much is pretty necessary. I don't know exactly how much Web
traffic is authenticated vs. unauthenticated (ask Cloudflare, they might) but
according to some quick Googling an Akamai bigwig said in 2019 that 83% of
the traffic they see is API traffic. I imagine that API traffic is
overwhelmingly authenticated, and when you factor in the fact that a large
part of the rest is social media, which is also going to be
mostly-authenticated, I imagine you'll end up with somewhere between "a whole
lot" and "the vast majority."
So you need authentication, and websockets don't give it to you. Well, they
leave it open, kinda - RFC 6455 says that the websocket opening handshake
(which starts with an HTTP request) can include
> Optionally, other header
fields, such as those used to send cookies or request authentication to a
server.
But in practice, this still kinda sucks. It sucks because you can't
have _one_ authentication method that's dead-simple and works for everybody.
Either you're coming from the browser, in which case you're stuck with
session cookies or URL params and that's it, or you're coming from some kind
of scripting environment where you'd love to be able to just stick a bearer
token in the `Authorization` header like everybody else does, but that's not
how the browser does it so tough luck.
The only solution that works easily with all clients is to put the
auth in a URL param. So let's just do that. Unfortunately, that creates a new
issue: we can't just use a plain bearer token any more, because now it's in
the URL and URL's go all sorts of places - server logs, CDNs, browser address
bars, etc. Probably the best thing to do here is to simply sign the URL
[a la AWS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/RESTAuthentication.html#RESTAuthenticationQueryStringAuth).
Fortunately, since we're only dealing with a very specific type of request,
we don't need to bother with the authenticated headers business that AWS
does.
The browser has very limited capabilities when it comes to modifying the request, so we should probably stick to a signature that can be included directly in the URL as a couple of querystring params.

View File

@@ -1,18 +0,0 @@
<script context="module">
export async function load({ fetch }) {
const resp = await fetch('/latest.json');
const postMeta = await resp.json();
const post = await import(`./_posts/${postMeta.slug}.svx`);
return {
props: {
post: post.default,
}
}
}
</script>
<script>
export let post;
</script>
<svelte:component this={post} />

View File

@@ -1,5 +0,0 @@
import { postData } from './posts.js';
export async function get() {
return {body: postData[0]};
}

View File

@@ -1,22 +0,0 @@
const posts = import.meta.globEager('./_posts/*.svx');
export let postData = [];
for (const path in posts) {
const slug = path.slice(9, -4)
posts[path].metadata.slug = slug;
postData.push(posts[path].metadata);
}
postData.sort((a, b) => {
// sorting in reverse, so we flip the intuitive order
if (a.date > b.date) return -1;
if (a.date < b.date) return 1;
return 0;
})
export async function get() {
return {
body: {postData}
};
}

View File

@@ -1,30 +0,0 @@
<script>
export let postData;
</script>
<style>
#posts {
text-align: center;
margin-top: 1.25rem;
}
.post-link {
text-decoration: none;
}
.post-link:hover {
text-decoration: underline;
}
</style>
<svelte:head>
<title>Posts</title>
</svelte:head>
<div id="posts">
<h1>All Posts</h1>
{#each postData as post}
<p>
<a sveltekit:prefetch class="post-link" href="/{post.slug}"><h3>{post.title}</h3></a>
</p>
{/each}
</div>

17
src/styles/main.css Normal file
View File

@@ -0,0 +1,17 @@
@import '@fontsource-variable/figtree';
@import 'reset.css';
@import 'vars.css';
@import 'prose.css';
body {
font-family: 'Figtree Variable', sans-serif;
font-weight: 350;
font-size: var(--content-size);
line-height: var(--content-line-height);
color: var(--content-color);
background-color: var(--bg-color);
}
a {
color: var(--link-color);
}

56
src/styles/prose.css Normal file
View File

@@ -0,0 +1,56 @@
@import '@fontsource-variable/baskervville';
.prose {
h1, h2, h3, h4, h5, h6 {
font-family: 'Baskervville Variable', serif;
font-weight: 650;
margin-bottom: 0.25rem;
color: var(--heading-color);
letter-spacing: 0.015em;
line-height: 1.25
}
h1 {
margin-top: 0.5em;
font-size: 2.25em;
}
h2 {
font-size: 1.75em;
}
h3 {
font-size: 1.4em;
}
h4 {
font-size: 1.2em;
}
h5, h6 {
font-size: 1em;
font-weight: 700;
}
p, ul, ol {
margin-bottom: 0.8em;
}
ul, ol, blockquote {
padding: 0;
margin-left: 2em;
}
blockquote {
position: relative;
font-style: italic;
}
blockquote::before {
content: '';
position: absolute;
left: -01em;
height: 100%;
border-right: 3px solid var(--accent-color);
}
}

22
src/styles/reset.css Normal file
View File

@@ -0,0 +1,22 @@
/* This reset lifted largely from Josh Comeau's "CSS for JS Devs" course */
/* Use a more-intuitive box-sizing model. */
*, *::before, *::after {
box-sizing: border-box;
}
/* Remove default margin */
* {
margin: 0;
}
/* Allow percentage-based heights in the application */
html, body {
min-height: 100%;
}
/* Improve media defaults */
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}

75
src/styles/vars.css Normal file
View File

@@ -0,0 +1,75 @@
:root {
--content-size: 1.25rem;
--content-size-sm: 1rem;
--content-line-height: 1.5;
--content-width: 52.5rem;
--content-padding: 0.65rem;
/* squish things down a little on mobile so more text fits on the screen */
@media(max-width: 640px) {
--content-line-height: 1.25;
--content-size: 1.15rem;
--content-size-sm: 0.9rem;
}
/* light-mode colors */
--bg-color: hsl(0deg 0% 100%);
/* text */
--content-color: hsl(0deg 0% 20%);
--content-color-faded: #555;
/* links */
--primary-color: hsl(202deg 72% 28%);
--primary-color-faded: hsl(202deg 14% 36%);
/* indicators, hover effects, etc */
--accent-color: hsl(0deg 92% 29%);
--accent-color-faded: hsl(0deg 25% 55%);
/* misc */
--heading-color: hsl(0deg 0% 27%);
--link-color: var(--primary-color);
--nav-link-color: white;
--neutral-gray: hsl(0deg 0% 30%);
/* dark-mode colors (defined here so that we only have to update them in one place) */
--dark-bg-color: hsl(220deg 10% 13%);
--dark-content-color: hsl(30deg 10% 75%);
--dark-content-color-faded: hsl(25deg 6% 50%);
--dark-primary-color: hsl(220deg 15% 40%);
--dark-primary-color-faded: hsl(220deg 12% 18%);
--dark-accent-color: hsl(18deg 70% 55%);
--dark-accent-color-faded: hsl(18deg 30% 45%);
--dark-heading-color: hsl(35deg 25% 88%);
--dark-link-color: hsl(202deg 50% 50%);
--dark-nav-link-color: var(--dark-heading-color);
--dark-neutral-gray: hsl(220deg 10% 45%);
&[data-theme="dark"] {
--bg-color: var(--dark-bg-color);
--content-color: var(--dark-content-color);
--content-color-faded: var(--dark-content-color-faded);
--primary-color: var(--dark-primary-color);
--primary-color-faded: var(--dark-primary-color-faded);
--accent-color: var(--dark-accent-color);
--accent-color-faded: var(--accent-color-faded);
--heading-color: var(--dark-heading-color);
--link-color: var(--dark-link-color);
--nav-link-color: var(--dark-nav-link-color);
--neutral-gray: var(--dark-neutral-gray);
}
&:not([data-theme="light"]) {
@media(prefers-color-scheme: dark) {
color-scheme: dark;
--bg-color: var(--dark-bg-color);
--content-color: var(--dark-content-color);
--content-color-faded: var(--dark-content-color-faded);
--primary-color: var(--dark-primary-color);
--primary-color-faded: var(--dark-primary-color-faded);
--accent-color: var(--dark-accent-color);
--accent-color-faded: var(--accent-color-faded);
--heading-color: var(--dark-heading-color);
--link-color: var(--dark-link-color);
--nav-link-color: var(--dark-nav-link-color);
--neutral-gray: var(--dark-neutral-gray);
}
}
}

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,82 +0,0 @@
/* ### TYPOGRAPHY ### */
@font-face {
font-family: 'Tajawal';
font-style: normal;
font-weight: 400;
src: url(/Tajawal-Regular.woff2) format('woff2');
font-display: block;
}
html {
font-family: 'Tajawal', sans-serif;
font-size: 20px;
line-height: 1.3;
letter-spacing: -0.005em;
}
body {
margin: 0;
}
h1, h2, h3, h4, h5, h6 {
font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, Arial, sans-serif;;
font-weight: 600;
color: #464646;
}
h1 {
font-variant: petite-caps;
margin-top: 0.75rem;
}
h1, h2 {
margin-bottom: 0.75rem;
}
h3 {
font-size: 1.2rem;
}
h4 {
font-size: 1.1rem;
}
h3, h4 {
margin-bottom: 0.5rem;
}
h5, h6 {
font-size: 1rem;
margin-bottom: 0;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
/*ul, ol {
margin: 0.5rem 0;
}*/
code {
background: #eee;
border-radius: 0.2rem;
font-family: Consolas, monospace;
font-size: 0.85rem;
padding: 0 0.15rem;
}
pre {
padding: 0.5rem;
line-height: 1.1;
border-radius: 0.15rem;
}
pre > code {
padding: 0;
font-size: 0.8rem;
background-color: transparent;
}
/* TESTING */

View File

@@ -1,22 +0,0 @@
import { mdsvex } from 'mdsvex';
import staticAdapter from '@sveltejs/adapter-static';
import svp from 'svelte-preprocess';
import slug from './src/lib/slug.js';
const config = {
extensions: ['.svelte', '.svx'],
preprocess: [
mdsvex({
layout: './src/lib/Post.svelte',
rehypePlugins: [slug],
}),
svp.scss(),
],
kit: {
// hydrate the <div id="svelte"> element in src/app.html
adapter: staticAdapter(),
}
};
export default config;

13
tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": "astro/tsconfigs/strictest",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"],
"compilerOptions": {
"paths": {
"@components/*": ["./src/components/*"],
"@layouts/*": ["./src/layouts/*"],
"@lib/*": ["./src/lib/*"],
"@styles/*": ["./src/styles/*"]
}
}
}