1
Fork 0

Add an actual page structure and whatnot

This commit is contained in:
prescientmoon 2024-11-02 04:33:36 +01:00
parent 363ea62983
commit 632f35ce62
Signed by: prescientmoon
SSH key fingerprint: SHA256:WFp/cO76nbarETAoQcQXuV+0h7XJsEsOCI0UsyPIy6U
9 changed files with 223 additions and 86 deletions

View file

@ -1,6 +1,6 @@
[home](/)
# My thoughts on games
# Games I like
This article contains a highly subjective tier list of most of the games I've played. You can click on the different tiers or games to be taken to the section of the article exploring my thoughts on said game. Everything in this article is spoiler-free, unless otherwise stated in a certain game's section. Enjoy!
{ role="tier-list" }
``` =yaml
@ -116,11 +116,13 @@ If you're reading the scribbles I left in this forgotten cave, you're probably a
Factorio's ability to simulate gigantic bases on pretty much any old laptop is a technical marvel. In an age where [Hollow Knight](#hollow-knight) struggles to run a pre-rendered cutscene at more than 1 frame / second on my laptop (I am not kidding, this is not a hyperbole. I had to search the cutscenes for that game on YouTube, half a decade after it's release....), the developers have done an amazing job at not only optimizing the living soul out of this game, but decided to then follow in Dante's footsteps throughout the circles of hell, only to optimize the (now dead) soul of this game even futher. Ok, I have no idea where I was going with that. The game is fast, which is super cool.
The gameplay is very polished, and infinitely replayable. There's not much I can do to explain that without... writing a giant essay about every single mechanic Factorio has to offer. Again, there's a reason Factorio managed to spawn an entire genre — be it the core belt logistics made all the more interesting by each belt being split into two independent lanes, the majestic interlocking systems trains enable, the explosive automation that would seem absurd without the existence of bots, the super fun circuit system (I have on multiple occasions started working on programming languages meant to compile to Factorio combinators, and I am not even close to the first, nor the only one) — these mechanics come together to build an amazing environment. I haven't even mentioned the military side of the game, nor any of the features added by the amazing recent expansion. There's also official modding support, with mods downloadable directly from the in-game menu (no laggy steam workshop support involved). There's at least a dozen quality overhaul mods which turn Factorio's campaign into something completely different. Just... go and play it.
The gameplay is very polished, and infinitely replayable. There's not much I can do to explain that without... writing a giant essay about every single mechanic Factorio has to offer. Again, there's a reason Factorio managed to spawn an entire genre — be it the core belt logistics made all the more interesting by each belt being split into two independent lanes, the majestic interlocking systems trains enable, the explosive automation that would seem absurd without the existence of bots, the super fun circuit system (I have on multiple occasions started working on programming languages meant to compile to Factorio combinators, and I am not even close to the first, nor the only one) — these mechanics come together to build an amazing sense of depth. I haven't even mentioned the military side of the game, nor any of the features added by the amazing expansion. There's also official modding support, with mods downloadable directly from the in-game menu (no laggy steam workshop support involved). There's at least a dozen quality overhaul mods which turn Factorio's campaign into something completely different. Just... go and play it.
Some people might be put off by the 35$ (70$ with the expansion) price for an indie game which, by principle, has never been on sale (in fact, the price of the game increases periodically to keep up with inflation). Having played the game, I've started to appreciate this stance. It might not be the best for me in terms of money spent, but it shows that the developers believe their game is actually worth the price they ask for. I love the [The Talos Principle](#talos), and while the base price for that game is technically around 30$, it sure doesn't feel the game is worth that much when every few months, you can buy it for under 3$ during one of the various Steam sales. I don't think this point is worth circling too long around. The game has a demo you can try for free, not to mention that uhhh, you can always pirate the game, you know? — that's what I originally did when I was a poor high school student.
The developers have a (at times weekly) [blog](https://www.factorio.com/blog/) where they discuss a range set of topics, from technical details about belt optimizations, to an entire year worth of hype that railed the community together on a weekly basis before the release of the Space Age expansion. This is incredibly cool, and I wish more developers would put up similar things.
The developers have a (sometimes weekly) [blog](https://www.factorio.com/blog/) where they discuss a range of topics, from technical details about belt optimizations, to an entire year worth of hype that railed the community together on a weekly basis before the release of the Space Age expansion. This is incredibly cool, and I wish more developers would put up similar things.
I highly recommend not to have your first experience with this game be a co-op session with someone who's played before. Even if the other person is trying their best to let you figure stuff out, they _will_ slip out details or tehniques, if for no other reason than practices feeling _wrong_ to someone who's seen "the light". That's not even mentioning the subset of players whose preferred playstyles involves placing down other people's blueprints. While there's nothing inherently wrong with that, it can take a lot away from one's initial sense of discovery with the game. I'm saying this because I initially bounced away from the game after trying to play with such a friend. In the end, it's up to you.
{ #arcaea }
## Arcaea

View file

@ -1,8 +1,7 @@
#page-content {
body {
font: 100%/1.6 sans-serif;
max-width: 70ch;
margin-left: auto;
margin-right: auto;
margin: auto;
padding: 1em;
}

12
scripts/last_modified.sh Executable file
View file

@ -0,0 +1,12 @@
#!/usr/bin/env bash
git show --quiet \
--format=%ad \
--date=format:"%F" \
$(git log --format="%ad %H" \
--date=format:%s \
--grep="\[date skip\]" \
--invert-grep $1 \
| sort --reverse \
| awk '{ print $2 }' \
| head -n1
)

View file

@ -3,6 +3,7 @@
use std::collections::HashMap;
use anyhow::anyhow;
use anyhow::bail;
use jotdown::Alignment;
use jotdown::Container;
use jotdown::Event;
@ -11,16 +12,18 @@ use jotdown::ListKind;
use jotdown::OrderedListNumbering::*;
use jotdown::SpanLinkType;
use crate::metadata::PageMetadata;
use crate::template;
use crate::template::TemplateRenderer;
// {{{ Renderer
/// Render djot content as HTML.
pub fn render_html<'s>(
metadata: &'s PageMetadata,
mut events: impl Iterator<Item = Event<'s>>,
mut out: impl std::fmt::Write,
) -> anyhow::Result<()> {
let mut w = Writer::new();
let mut w = Writer::new(Some(metadata));
events.try_for_each(|e| w.render_event(&e, &mut out))?;
w.render_epilogue(&mut out)?;
@ -32,6 +35,7 @@ pub struct Writer<'s> {
list_tightness: Vec<bool>,
states: Vec<State<'s>>,
footnotes: Footnotes<'s>,
metadata: Option<&'s PageMetadata>,
}
#[derive(Debug, Clone)]
@ -41,14 +45,16 @@ enum State<'s> {
Raw,
Math(bool),
Aside(TemplateRenderer<'s>),
Article(TemplateRenderer<'s>),
}
impl<'s> Writer<'s> {
pub fn new() -> Self {
pub fn new(metadata: Option<&'s PageMetadata>) -> Self {
Self {
list_tightness: Vec::new(),
states: Vec::new(),
footnotes: Footnotes::default(),
metadata,
}
}
@ -176,7 +182,14 @@ impl<'s> Writer<'s> {
Container::DescriptionDetails => out.write_str("<dd")?,
Container::Table => out.write_str("<table")?,
Container::TableRow { .. } => out.write_str("<tr")?,
Container::Section { .. } => out.write_str("<section")?,
Container::Section { id } => match self.metadata {
Some(meta) if &meta.title.id == id => {
let renderer = template!("templates/post.html", &mut out)?;
assert_eq!(renderer.current(), Some("attrs"));
self.states.push(State::Article(renderer));
}
_ => out.write_str("<section")?,
},
Container::Div {
class: class @ ("aside" | "long-aside" | "char-aside"),
} => {
@ -184,17 +197,15 @@ impl<'s> Writer<'s> {
self.list_tightness.push(true);
}
let template = if *class == "aside" {
template!("templates/aside.html")?
let mut renderer = if *class == "aside" {
template!("templates/aside.html", &mut out)?
} else if *class == "char-aside" {
template!("templates/character-aside.html")?
template!("templates/character-aside.html", &mut out)?
} else {
template!("templates/long-aside.html")?
template!("templates/long-aside.html", &mut out)?
};
let mut renderer = TemplateRenderer::new(template);
while let Some(label) = renderer.current(&mut out)? {
while let Some(label) = renderer.current() {
if label == "character" {
let character = attrs.get_value("character").ok_or_else(|| {
anyhow!("Cannot find `character` attribute on `aside` element")
@ -330,7 +341,14 @@ impl<'s> Writer<'s> {
out.write_str(r#">"#)?;
self.states.push(State::Math(*display));
}
_ => out.write_char('>')?,
_ => match self.states.last_mut() {
Some(State::Article(renderer))
if renderer.current() == Some("attrs") =>
{
renderer.next(&mut out)?;
}
_ => out.write_char('>')?,
},
}
}
@ -411,7 +429,16 @@ impl<'s> Writer<'s> {
Container::DescriptionDetails => out.write_str("</dd>")?,
Container::Table => out.write_str("</table>")?,
Container::TableRow { .. } => out.write_str("</tr>")?,
Container::Section { .. } => out.write_str("</section>")?,
Container::Section { id, .. } => match self.metadata {
Some(meta) if &meta.title.id == id => {
let Some(State::Article(renderer)) = self.states.pop() else {
bail!("Arrived at the end of the main <section> element without being in the `Article` state.")
};
renderer.finish(&mut out)?;
}
_ => out.write_str("</section>")?,
},
Container::Div {
class: class @ ("aside" | "long-aside" | "char-aside"),
} => {
@ -420,15 +447,59 @@ impl<'s> Writer<'s> {
}
let state = self.states.pop().unwrap();
let State::Aside(mut renderer) = state else {
let State::Aside(renderer) = state else {
panic!("Finished `aside` element without being in the `Aside` state.")
};
assert_eq!(renderer.current(&mut out)?, Some("content"));
assert_eq!(renderer.current(), Some("content"));
renderer.finish(&mut out)?;
}
Container::Div { .. } => out.write_str("</div>")?,
Container::Heading { level, .. } => write!(out, "</h{}>", level)?,
Container::Heading { level, .. } => {
write!(out, "</h{}>", level)?;
if let Some(State::Article(renderer)) = self.states.last_mut() {
if renderer.current() == Some("title") {
// SAFETY: we can never enter into the `article` state without having
// some metadata on hand.
let meta = self.metadata.unwrap();
while let Some(label) = renderer.next(&mut out)? {
if label == "updated_on" {
write!(&mut out, "{}", meta.last_modified)?;
} else if label == "word_count" {
let wc = meta.word_count;
if wc < 400 {
write!(&mut out, "{}", wc)?;
} else if wc < 1000 {
write!(&mut out, "{}", wc / 100 * 100)?;
} else {
write!(&mut out, "{} thousand", wc / 1000)?;
}
} else if label == "reading_duration" {
let minutes = meta.word_count / 200;
if minutes < 10 {
write!(&mut out, "short {minutes} minute")?;
} else if minutes < 20 {
write!(&mut out, "somewhat short {minutes} minute")?;
} else if minutes < 30 {
write!(&mut out, "somewhat long {minutes}")?;
} else if minutes < 60 {
write!(&mut out, "long {minutes}")?;
} else {
let hours = minutes / 60;
let minutes = minutes % 60;
write!(
&mut out,
"very long {hours} hour and {minutes} minute"
)?;
}
} else if label == "content" {
break;
}
}
}
}
}
Container::TableCell { head: false, .. } => out.write_str("</td>")?,
Container::TableCell { head: true, .. } => out.write_str("</th>")?,
Container::Caption => out.write_str("</caption>")?,

View file

@ -1,3 +1,4 @@
use std::fmt::Write;
use std::fs::{self};
use std::path::{Path, PathBuf};
use std::process::Command;
@ -5,7 +6,7 @@ use std::str::FromStr;
use anyhow::{anyhow, Context};
use html::render_html;
use template::TemplateRenderer;
use metadata::PageMetadata;
mod html;
mod metadata;
@ -24,17 +25,21 @@ fn generate_page(path: &Path) -> anyhow::Result<()> {
let djot_input = std::fs::read_to_string(content_path).unwrap();
let mut out = String::new();
let page_template = template!("templates/page.html")?;
let mut page_renderer = TemplateRenderer::new(page_template);
let mut page_renderer = template!("templates/page.html", &mut out)?;
// let events = jotdown::Parser::new(&djot_input);
// let meta = PageMetadata::new(events)?;
// println!("Metadata: {meta:?}");
let events = jotdown::Parser::new(&djot_input);
let metadata = PageMetadata::new(events, path.to_owned())?;
while let Some(label) = page_renderer.next(&mut out)? {
while let Some(label) = page_renderer.current() {
if label == "content" {
let events = jotdown::Parser::new(&djot_input);
render_html(events, &mut out)?;
render_html(&metadata, events, &mut out)?;
page_renderer.next(&mut out)?;
} else if label == "navigation" {
out.write_str(r#"<a href="/"><code>~</code></a>"#)?;
out.write_str(" / ")?;
out.write_str(r#"<a href="/posts"><code>posts</code></a>"#)?;
page_renderer.next(&mut out)?;
} else {
break;
}

View file

@ -1,5 +1,5 @@
#![allow(dead_code)]
use std::fmt::Write;
use std::{fmt::Write, path::PathBuf, process::Command};
use anyhow::{anyhow, bail, Context};
use jotdown::{Container, Event};
@ -32,13 +32,19 @@ impl PageConfig {
#[derive(Debug)]
pub struct PageMetadata {
title: Heading,
config: PageConfig,
toc: Vec<Heading>,
pub title: Heading,
pub config: PageConfig,
pub toc: Vec<Heading>,
pub word_count: usize,
pub path: PathBuf,
pub last_modified: String,
}
impl PageMetadata {
pub fn new<'s>(mut events: impl Iterator<Item = Event<'s>>) -> anyhow::Result<Self> {
pub fn new<'s>(
mut events: impl Iterator<Item = Event<'s>>,
path: PathBuf,
) -> anyhow::Result<Self> {
let mut w = Writer::new();
events.try_for_each(|e| w.render_event(&e))?;
@ -47,16 +53,26 @@ impl PageMetadata {
.first()
.ok_or_else(|| anyhow!("No heading found to infer title from"))?;
let last_modified_output = Command::new("scripts/last_modified.sh")
.arg(&path)
.output()
.with_context(|| anyhow!("Could not read the last modification date for file"))?
.stdout;
let last_modified = String::from_utf8(last_modified_output)?;
Ok(Self {
title: title.to_owned(),
config: w.config,
toc: w.toc,
word_count: w.word_count,
path,
last_modified,
})
}
}
#[derive(Debug, Clone)]
struct Heading {
pub struct Heading {
pub level: u8,
pub id: String,
pub text: String,
@ -67,29 +83,41 @@ struct Heading {
enum State {
Toplevel,
Heading,
Toml,
Config,
}
struct Writer<'s> {
/// This renderer is used for generating the html for the titles
html_renderer: html::Writer<'s>,
config: PageConfig,
toc: Vec<Heading>,
toml_text: String,
state: State,
word_count: usize,
}
impl<'s> Writer<'s> {
fn new() -> Self {
Self {
html_renderer: html::Writer::new(),
html_renderer: html::Writer::new(None),
config: PageConfig::default(),
toc: Vec::new(),
toml_text: String::new(),
state: State::Toplevel,
word_count: 0,
}
}
fn render_event(&mut self, e: &jotdown::Event<'s>) -> anyhow::Result<()> {
if let Event::Str(content) = e {
if self.state != State::Config {
self.word_count += content
.split(" ")
.filter(|w| w.contains(|c: char| c.is_alphabetic()))
.count()
}
}
match e {
Event::Start(Container::Heading { level, id, .. }, _) => {
assert_eq!(self.state, State::Toplevel);
@ -109,12 +137,12 @@ impl<'s> Writer<'s> {
assert_eq!(self.state, State::Toplevel);
if let Some(role_attr) = attrs.get_value("role") {
if format!("{}", role_attr) == "config" {
self.state = State::Toml
self.state = State::Config
}
}
}
Event::End(Container::RawBlock { format: "toml" }) => {
if self.state == State::Toml {
if self.state == State::Config {
self.state = State::Toplevel;
let config: PageConfig = toml::from_str(&self.toml_text)
@ -124,7 +152,7 @@ impl<'s> Writer<'s> {
self.toml_text.clear();
}
}
Event::Str(str) if self.state == State::Toml => {
Event::Str(str) if self.state == State::Config => {
self.toml_text.write_str(str)?;
}
other if self.state == State::Heading => {

View file

@ -25,10 +25,11 @@ impl<'s> Template<'s> {
let mut current_stop: Option<Stop> = None;
let mut prev_ix = None;
for (ix, _) in text.char_indices() {
for (ix, c) in text.char_indices() {
let l = c.len_utf8();
if let Some(prev) = prev_ix {
// This char, together with the previous one
let last_two = &text[prev..=ix];
let last_two = &text[prev..ix + l];
if close_stop == last_two {
if let Some(mut stop) = current_stop.take() {
// I think this is safe, as { and } are ascii
@ -54,37 +55,35 @@ impl<'s> Template<'s> {
}
// }}}
// {{{ Template rendering
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
enum RendererState {
Started,
InStop(usize),
Finished,
}
#[derive(Clone, Debug)]
pub struct TemplateRenderer<'a> {
template: &'a Template<'a>,
state: RendererState,
stop_index: Option<usize>,
}
impl<'a> TemplateRenderer<'a> {
#[inline]
pub fn new(template: &'a Template) -> Self {
Self {
pub fn start(template: &'a Template, mut w: impl std::fmt::Write) -> anyhow::Result<Self> {
let stop_index = if !template.stops.is_empty() {
Some(0)
} else {
None
};
let result = Self {
template,
state: RendererState::Started,
}
stop_index,
};
let (next_pos, _) = result.current_stop_range();
w.write_str(&template.text[..next_pos])?;
Ok(result)
}
/// Get the current placeholder label
pub fn current(&mut self, w: impl std::fmt::Write) -> anyhow::Result<Option<&'a str>> {
let current_label = match self.state {
RendererState::Started => self.next(w)?,
RendererState::InStop(ix) => Some(self.template.stops[ix].label),
RendererState::Finished => None,
};
Ok(current_label)
pub fn current(&self) -> Option<&'a str> {
self.stop_index.map(|ix| self.template.stops[ix].label)
}
/// Attempt to finish rendering.
@ -99,43 +98,33 @@ impl<'a> TemplateRenderer<'a> {
// {{{ Advance to the next placeholder
/// Move onto the next placeholder
pub fn next(&mut self, mut w: impl std::fmt::Write) -> anyhow::Result<Option<&'a str>> {
if self.state == RendererState::Finished {
let Some(stop_index) = self.stop_index else {
return Ok(None);
}
};
let (_, current_pos) = self.current_stop_range();
let next_stop_ix = match self.state {
RendererState::Started => 0,
RendererState::InStop(stop_ix) => stop_ix + 1,
RendererState::Finished => unreachable!(),
};
let next_stop_ix = stop_index + 1;
self.state = if next_stop_ix < self.template.stops.len() {
RendererState::InStop(next_stop_ix)
self.stop_index = if next_stop_ix < self.template.stops.len() {
Some(next_stop_ix)
} else {
RendererState::Finished
None
};
let (next_pos, _) = self.current_stop_range();
w.write_str(&self.template.text[current_pos..next_pos])?;
let current_label = match self.state {
RendererState::InStop(ix) => Some(self.template.stops[ix].label),
_ => None,
};
Ok(current_label)
Ok(self.current())
}
fn current_stop_range(&self) -> (usize, usize) {
match self.state {
RendererState::Started => (0, 0),
RendererState::InStop(stop_ix) => {
match self.stop_index {
Some(stop_ix) => {
let stop = &self.template.stops[stop_ix];
(stop.start, stop.start + stop.length)
}
RendererState::Finished => (self.template.text.len(), self.template.text.len()),
None => (self.template.text.len(), self.template.text.len()),
}
}
@ -145,14 +134,15 @@ impl<'a> TemplateRenderer<'a> {
// {{{ Macro
#[macro_export]
macro_rules! template {
($path:literal) => {{
($path:literal,$w:expr) => {{
use once_cell::sync::OnceCell;
use $crate::template::Template;
use $crate::template::{Template, TemplateRenderer};
static TEMPLATE_TEXT: &str = include_str!($path);
static CELL: OnceCell<Template<'static>> = OnceCell::new();
CELL.get_or_try_init(|| Template::parse(TEMPLATE_TEXT))
.and_then(|t| TemplateRenderer::start(t, $w))
}};
}
// }}}

View file

@ -24,7 +24,15 @@
crossorigin="anonymous"
/>
</head>
<body>
<div id="page-content">{{content}}</div>
<header>
<nav>
{{navigation}}
</nav>
</header
<main>
{{content}}
</main>
</body>
</html>

22
src/templates/post.html Normal file
View file

@ -0,0 +1,22 @@
<article {{attrs}}>
<header>
{{title}}
<ul>
<li>
<!-- TODO: use actual date -->
Posted on 2020-11-23 by <a href="about:blank">prescientmoon</a> on their
<a href="moonythm.dev">website</a>
</li>
<li>
Last updated on {{updated_on}}. <a href="about:blank">Source</a> |
<a href="about:blank">Changelog</a>
</li>
<li>About {{word_count}} words; a {{reading_duration}} read</li>
</ul>
<hr />
</header>
{{content}}
</article>