Compare commits

10 Commits

25 changed files with 381 additions and 79 deletions

View File

@@ -1,6 +1,7 @@
import { defineConfig } from "astro/config";
import mdx from '@astrojs/mdx';
import vue from '@astrojs/vue';
import { remarkDescription } from './src/plugins.ts';
export default defineConfig({
integrations: [
@@ -9,6 +10,7 @@ export default defineConfig({
],
prefetch: true,
markdown: {
remarkPlugins: [remarkDescription],
shikiConfig: {
themes: {
light: 'dracula',

View File

@@ -49,7 +49,7 @@ My word-search grid appears to be 140 characters by 140, so I'm just going to ha
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
```f90
read(file_handle, *) somevar, anothervar, anothervar2
```
@@ -57,7 +57,7 @@ Or at least, that's one way of using it. But here's the problem: by default, For
Initially, I thought I might be able to do this:
```fortran
```f90
character, dimension(140, 140) :: grid
! ...later
@@ -70,7 +70,7 @@ But sadly, this kept spitting out errors about how it had encountered the end of
My next try looked something like this:
```fortran
```f90
do row = 1, 100
read(file_handle, *) grid(row, :)
end do
@@ -82,7 +82,7 @@ I'm pretty sure the proper way to do this would be to figure out how to set the
So instead, I decided to just make it dumber.
```fortran
```f90
program advent04
implicit none
@@ -139,7 +139,7 @@ S A M X M A S
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?
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):
@@ -164,7 +164,7 @@ Will this work? Is it even marginally more efficient than the stupidly obvious w
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
```f90
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")
@@ -196,7 +196,7 @@ 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
```f90
integer function count_xmas(row, col) result(count)
implicit none
@@ -277,7 +277,7 @@ Those `&`s are line-continuation characters, by the way. Apparently you can't ha
Now we just have to put it all together:
```fortran
```f90
total = 0
do col = 4, 143
do row = 4, 143
@@ -314,7 +314,7 @@ Hmm, I wonder if there's a way to take a single starting test grid and manipulat
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
```f90
character, dimension(3, 3) :: window, t1, t2, t3, t4
t1 = reshape( &
@@ -332,7 +332,7 @@ t4 = t3(:, 3:1:-1) ! flip t3 left-to-right
Then we can just compare the window to each test grid:
```fortran
```f90
window = grid(row - 1:row + 1, col - 1:col + 1)
if ( &
count_matches(window, t1) == 5 &

View File

@@ -1,6 +1,7 @@
---
title: Example after post
date: 2026-02-28
draft: true
---
## After

View File

@@ -1,9 +1,12 @@
---
export interface Props {
name: string,
width?: string,
height?: string,
display?: string,
};
const { name } = Astro.props;
const { name, width, height, display } = Astro.props;
const icons = import.meta.glob<{string: string}>('@components/icons/*.svg', { query: '?raw', import: 'default' });
const path = `/src/components/icons/${name}.svg`;
@@ -13,11 +16,12 @@ if (icons[path] === undefined) {
const icon = await icons[path]();
---
<Fragment set:html={icon} />
<span class="icon" set:html={icon} />
<style>
svg {
width: 100%;
height: 100%;
<style define:vars={{ width, height, display }}>
.icon :global(svg) {
display: var(--display, block);
width: var(--width, 100%);
height: var(--height, 100%);
}
</style>

View File

@@ -8,16 +8,54 @@ import { render } from 'astro:content';
import Toc from '@components/Toc.vue';
import { formatDate } from '@lib/datefmt';
import Icon from '@components/Icon.astro';
import { headingElements } from '@components/headings';
export interface Props {
entry: CollectionEntry<'posts'>,
prevSlug: string | null,
nextSlug: string | null,
prevSlug?: string | null,
nextSlug?: string | null,
};
const { entry, prevSlug, nextSlug } = Astro.props;
const { Content, headings } = await render(entry);
---
<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 components={headingElements} />
</section>
<div id="right-gutter" />
<footer>
{prevSlug && (
<div class="footer-link left">
<Icon name="arrow-left" height="1em" />
<a href={`/${prevSlug}`} data-astro-prefetch>Older</a>
</div>
)}
{nextSlug && (
<div class="footer-link right">
<a href={`/${nextSlug}`} data-astro-prefetch>Newer</a>
<Icon name="arrow-right" height="1em" />
</div>
)}
</footer>
</article>
<style>
/* 3-column grid: left gutter, center content, and right gutter */
article {
@@ -59,7 +97,11 @@ footer {
grid-column: 2 / 3;
margin-bottom: 2.5rem;
display: flex;
justify-content: space-between;
color: var(--content-color-faded);
margin-top: 1.5rem;
padding-top: 2.25rem;
border-top: 1px solid var(--content-color-faded);
& a {
font-size: 1.25rem;
@@ -77,6 +119,32 @@ footer {
}
}
/* basic styles */
.footer-link {
display: flex;
gap: 0.25rem;
align-items: center;
&.right {
margin-left: auto;
}
}
/* animate on hover */
.footer-link {
& :global(svg) {
transition: transform 150ms;
}
&.right:hover :global(svg) {
transform: translateX(50%);
}
&.left:hover :global(svg) {
transform: translateX(-50%);
}
}
article {
& :global(section.post::first-letter) {
font-family: 'Baskervville';
@@ -94,32 +162,3 @@ article {
}
}
</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

@@ -38,26 +38,15 @@ button {
}
/* 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;
display: none;
}
: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;
display: block;
}
:global(html[data-theme="dark"]) button#switch-to-light {
opacity: 1;
visibility: visible;
transform: rotate(-360deg);
z-index: 10;
display: block;
}
</style>

View File

@@ -0,0 +1,7 @@
---
import Heading from './Heading.astro';
---
<Heading tag="h1" {...Astro.props}>
<slot />
</Heading>

View File

@@ -0,0 +1,7 @@
---
import Heading from './Heading.astro';
---
<Heading tag="h2" {...Astro.props}>
<slot />
</Heading>

View File

@@ -0,0 +1,7 @@
---
import Heading from './Heading.astro';
---
<Heading tag="h3" {...Astro.props}>
<slot />
</Heading>

View File

@@ -0,0 +1,7 @@
---
import Heading from './Heading.astro';
---
<Heading tag="h4" {...Astro.props}>
<slot />
</Heading>

View File

@@ -0,0 +1,7 @@
---
import Heading from './Heading.astro';
---
<Heading tag="h5" {...Astro.props}>
<slot />
</Heading>

View File

@@ -0,0 +1,7 @@
---
import Heading from './Heading.astro';
---
<Heading tag="h6" {...Astro.props}>
<slot />
</Heading>

View File

@@ -0,0 +1,45 @@
---
import Icon from '@components/Icon.astro';
export interface Props extends astroHTML.JSX.HTMLAttributes {
tag: string,
}
const { tag: Tag, ...rest } = Astro.props;
---
<Tag {...rest}>
<slot />
<a href={`#${Astro.props.id}`} class="deep-link" >
<Icon name="link" height="0.9em" width="0.9em" display="inline" />
</a>
</Tag>
<style>
h1, h2, h3, h4, h5, h6 {
position: relative;
&:hover :global(svg) {
color: var(--accent-color);
}
}
a {
& :global(svg) {
color: var(--content-color-faded);
padding-bottom: 0.15em;
}
&:hover :global(svg), &:active :global(svg) {
color: var(--accent-color);
}
@media(min-width: 60rem) {
position: absolute;
top: 0.1em;
left: -1.25em;
padding-right: 0.5em;
}
}
</style>

View File

@@ -0,0 +1,15 @@
import H1 from './H1.astro';
import H2 from './H2.astro';
import H3 from './H3.astro';
import H4 from './H4.astro';
import H5 from './H5.astro';
import H6 from './H6.astro';
export const headingElements = {
h1: H1,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
h6: H6,
};

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="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg>

After

Width:  |  Height:  |  Size: 219 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="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
</svg>

After

Width:  |  Height:  |  Size: 219 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="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" />
</svg>

After

Width:  |  Height:  |  Size: 353 B

View File

@@ -1,10 +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="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>

Before

Width:  |  Height:  |  Size: 425 B

After

Width:  |  Height:  |  Size: 364 B

View File

@@ -2,6 +2,12 @@
import '@styles/main.css';
import '@fontsource-variable/baskervville-sc';
import ThemeSwitcher from '@components/ThemeSwitcher.astro';
export interface Props {
pageTitle: string,
}
const { pageTitle } = Astro.props;
---
<html lang="en">
@@ -9,6 +15,8 @@ import ThemeSwitcher from '@components/ThemeSwitcher.astro';
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{pageTitle}</title>
<!-- avoid FOUC by setting the color schme here in the header -->
<script>
const explicitPref = localStorage.getItem('theme-preference');

10
src/lib/content.ts Normal file
View File

@@ -0,0 +1,10 @@
import { getCollection } from 'astro:content';
export async function listPosts() {
// all posts in dev, exlucde draft posts in prod
let entries = await getCollection('posts', ({ data }) => import.meta.env.DEV || !data.draft);
// sort by date descending
entries.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
return entries;
}

View File

@@ -1,17 +1,17 @@
---
import { getCollection } from 'astro:content';
import { listPosts } from '@lib/content.ts';
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())
const entries = await listPosts();
// 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;
// entries are sorted in by date descending, so prev has a higher index and next has lower
const prevSlug = entries[idx + 1]?.id || null;
const nextSlug = entries[idx - 1]?.id || null;
return {
params: { slug: entry.id },
props: { entry, prevSlug, nextSlug },
@@ -20,8 +20,9 @@ export async function getStaticPaths() {
}
const { entry, prevSlug, nextSlug } = Astro.props;
---
<BaseLayout>
<Post {...Astro.props} />
<BaseLayout pageTitle={entry.data.title}>
<Post {entry} {prevSlug} {nextSlug} />
</BaseLayout>

24
src/pages/about.astro Normal file
View File

@@ -0,0 +1,24 @@
---
import BaseLayout from '@layouts/BaseLayout.astro';
---
<BaseLayout pageTitle="Joe's Blog - About">
<article class="prose">
<h1>About Me</h1>
<p>(Joe's wife wrote this because Joe feels weird writing about himself.)</p>
<p>Joe is a quirky, techy Tolkienite with a beautiful singing voice, an uncanny ability to do mental math, a bony, un-cuddleable frame, and a big mushy heart. He enjoys bike riding, computers, watching TV, reading about computers, playing Breath of the Wild, building computers, talking about something called "programming languages", and spending time with his family (which often involves fixing their computers). He graduated with a Liberal Arts degree from Thomas Aquinas College, the school of his forebears. He often remarks that he has greatly benefitted from the critical thinking skills he acquired at his alma mater in his current line of work.</p>
<p>He has spent, at the current time, about 2 years working on this blog. Most of his posts are about all of the work it took and everything he learned making this blog. Unlike most "bloggers", he has started with many blog posts and no blog, rather than a blog without posts. "Someday", he says, "I will actually get that blog up". I always nod encouragingly.</p>
<p>If you are reading this, then that day has arrived. We hope you enjoy it, and maybe even learn something along the way.</p>
</article>
</BaseLayout>
<style>
article {
max-width: var(--content-width);
margin: 0 auto;
}
</style>

View File

@@ -1,7 +1,14 @@
---
import { listPosts } from '@lib/content.ts';
import BaseLayout from '@layouts/BaseLayout.astro';
import Post from '@components/Post.astro';
const entries = await listPosts();
// there will always be at leaste one entry
const entry = entries[0]!;
const prevSlug = entries[1] ? entries[1]?.id : null;
---
<BaseLayout>
<p>Index file</p>
<BaseLayout pageTitle={entry.data.title}>
<Post {entry} {prevSlug} />
</BaseLayout>

80
src/pages/posts.astro Normal file
View File

@@ -0,0 +1,80 @@
---
import '@styles/prose.css';
import { render } from 'astro:content';
import { listPosts } from '@lib/content.ts';
import BaseLayout from '@layouts/BaseLayout.astro';
// return all posts in dev, only non-draft in prod
const entries = await listPosts();
const posts = await Promise.all(entries.map(async entry => {
const { remarkPluginFrontmatter } = await render(entry);
return { ...entry, remarkPluginFrontmatter };
}));
---
<BaseLayout pageTitle="Posts">
<div class="prose">
<h1>All Posts</h1>
<ul>
{posts.map( (p, idx) =>
<li>
{idx > 0 && <hr></hr>}
<h2>
<a href={`/${p.id}`} data-astro-prefetch>
{p.data.title}
</a>
{p.data.draft && <span class="draft-notice">Draft</span>}
</h2>
<p>
{p.remarkPluginFrontmatter.description}
</p>
</li>
)}
</ul>
</div>
</BaseLayout>
<style>
div.prose {
max-width: var(--content-width);
padding: var(--content-padding);
margin: 0 auto;
& h2 a {
color: var(--content-color);
}
}
h1 {
text-align: center;
margin-bottom: 2rem;
}
ul {
list-style: none;
margin-left: 0;
}
h2 {
display: flex;
gap: 01rem;
align-items: center;
}
.draft-notice {
font-size: 0.5em;
font-family: 'Figtree Variable', sans-serif;
color: var(--nav-link-color);
background-color: var(--accent-color);
padding: 0.05em 0.5em;
border-radius: 20% / 50%;
}
hr {
border: none;
height: 1px;
margin: 2.5rem 0;
background-color: var(--content-color);
}
</style>

33
src/plugins.ts Normal file
View File

@@ -0,0 +1,33 @@
import type { Root, Paragraph, PhrasingContent } from 'mdast';
import type { VFile } from 'vfile';
import { visit } from 'unist-util-visit';
import { toString } from 'mdast-util-to-string';
export function remarkDescription() {
return (tree: Root, vfile: VFile) => {
let description: string | null = null;
visit(tree, 'paragraph', (node: Paragraph) => {
if (description !== null) return;
description = summarize(node);
});
// Astro exposes this as `remarkPluginFrontmatter` from render()
const astro = (vfile.data.astro ??= { frontmatter: {} }) as { frontmatter: Record<string, unknown> };
astro.frontmatter.description = description;
};
}
/**
* Convert a paragraph node to a plain-text string, stripping any
* MDX JSX elements (e.g. <Sidenote>) so their content doesn't
* leak into the summary.
*/
function summarize(par: Paragraph): string {
const filtered = par.children.filter(
(child: PhrasingContent) => !(child.type as string).startsWith('mdxJsx')
);
return toString({ type: 'paragraph', children: filtered });
}