Higo Post-“Mortem”
https://cohost.org/pesterblog/post/4871245-higo-post-mortem
an old, embarassing post about an even older and more embarassing pair of programs
About seven months ago I made a static site generator. Then I made it again.
The first “generator”#
Let’s look at it piece by piece.
fn main() { let out = replace( "Hey guys! this is {**{*so*} cool**}.\nThis is {`programming`} I heard you like lorem ipsum so i gave you {=KEYBOARD SPAM=} ... annoyingly long line with keyspam separated by <br>s omitted ... fkadjf", "test lol", ); println!("{out}"); }
This… looks pretty good!
Obviously a main function should be much more interactive, but this is very much a test so it’s cool.
Other than that, pretty nice, and nice use of a let and the lovely
format args capture;1{** **}
, {* *}
, and {` `}
are pretty self evident, but I have no clue what {= =}
is supposed to be.
Well let’s find out, then we can look at the tests:
fn replace(thing: &str, title: &str) -> String { let mut content = String::from(thing); for (from, to) in [ ("{**", "<b>"), ("**}", "</b>"), ("{*", "<i>"), ("*}", "</i>"), ("{``", "<pre>"), ("``}", "</pre>"), ("{`", "<code>"), ("`}", "</code>"), ("{=", "<h1>"), ("=}", "</h1>"), ("\n\n", "<br>"), ] { content = content.replace(from, to); } format!( r#"<!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8"> <title>{title}</title> <link rel="stylesheet" href="./style.css"> </head> <body> <!-- partial:index.partial.html --> <body class="center"> {content} </body> </body> </html>"# ) }
Oh.
That is. Well, it’s technically2
Let’s start with the good parts: This truly is readable! I think someone with basic rust knowledge (and a touch of syntax highlighting for the format! macro) could read this and understand what it’s doing. Tuple destructuring is put to great effect, here’s what it would look like without it:
for replace in [ //... ] { let from = replace.0; let to = replace.1; content = content.replace(from, to); // Or even worse: content = content.replace(replace.0, replace.1); }
Ew.
Speaking of ew: the bad parts. This is really stupid. I mean come on, even for a super early test this is just absurd. Customization is critical for any even remotely serious static site generator. And the syntax, my gosh the syntax! This abortion of a “markdown” is about as far from HTML in verbosity as Canada is from Canada. {**}
is literally the exact same length as <b>
I mean come on. Ease of implementation is nice, and all, except wait no it isn’t? That is just an absurd thing to prioritize, even for a hobby project. Imagine bragging about how easy your shoes were for someone else to make, GPI almighty.
Ranting aside, just kidding hahahahah why on earth would you use curly braces of all things, they’re annoying to type, especially on my keyboard that I was using at the time, where [
and ]
are behind a layer which means that curly braces require pressing three keys at once. Parentheses would have been much better, note to no one.
Ranting actually aside, this is just okay. The HTML being generated with format!
is sketchy but who cares, this wasn’t made for untrusted input.
The first “generator”’s tests#
Did I mention that I named it hugo_but_wo_rs_e
?
The second generator#
First of all: If you want to look at everything for some reason (there’s some mildly funny stuff I guess) here it is.
Now this version is more interesting thanks in large part to this crazy feature it has: actually working. Thankfully for current me, this is another single file affair, which makes commentification much easier.
Pretty standard uses:
use std::env::args; use std::error::Error; use std::fs::{remove_dir_all, DirBuilder, File}; use std::io::prelude::*; use std::process::Command;
I would probably just import std::env
and std::fs
, but that’s a nitpick.
fn main() -> Result<(), Box<dyn Error>> { gen( &args().nth(1).ok_or("YOU GOTTA GIVE ME AN IN PATH MAN")?, &args().nth(2).ok_or("YOU GOTTA GIVE ME AN OUT PATH MAN")?, ) }
A clean main, but the use of ok_or
is… weird. I guess it makes sense since gen
is also fallible but still. It’s not like there’s actual error handling, so it’s really just less clear.
const TEMPLATE: &str = include_str!("article.dumb_template");
The choice to not use a full templating language like liquid or handlebars was definitely right for this stage of the project. The template is just HTML with {{ TITLE }}
and {{ CONTENT }}
placed in a reasonable place. I understand why I chose to use include_str!
(don’t have to worry about the path at runtime, ease of use, actually mainly easy of use), but it once again absolutely destroys any customizability. Did I really not care about having to compile every time I made a change???
Moving on:
fn gen_html(raw: &str, title: &str) -> String { let content = markdown::to_html(raw) .replace("<<", "<img src=\"../") .replace(">>", "\">"); TEMPLATE .replace("{{ TITLE }}", title) .replace("{{ CONTENT }}", &content) }
This is good. Good function boundary, that img shortcut is kind of weird but it works. Not much to say, although I can see now how having a global template reduces the amount of state to track of, which is good.
fn deal_with_dir(in_path: &str, article_path: &str, out_path: &str) -> Result<(), Box<dyn Error>> { let mut content = File::open(format!("{in_path}/{article_path}.md"))?; let mut raw = String::new(); content.read_to_string(&mut raw)?; let html = gen_html(&raw, article_path); let dir = DirBuilder::new(); dir.create(format!("{out_path}/{article_path}"))?; File::create(format!("{out_path}/{article_path}/index.html"))?.write_all(html.as_bytes())?; Ok(()) }
This is a bit weird but not too bad. Using Path
’s join
method (which I just found out about) would be better for sure, but the format!
macro is easy and arguably even more clear. The Box<dyn Error>
is a common theme in this program, which is convenient but has problems. Namely, it makes error recovery much harder, and it has a solid amount of overhead. I understand rolling your own error type is a pain, but it definitely has benefits.
const CSS: &[u8] = include_str!("style.css").as_bytes(); const LIST_TEMPLATE: &str = include_str!("list.dumb_template");
I get that I was trying to keep things near where they’re used but that’s kind of stupid. It’s almost certainly better to just keep constants near each other. Of course, I could have had the best of both worlds if I had actually split stuff up logically.
fn gen(in_path: &str, out_path: &str) -> Result<(), Box<dyn Error>> { let articles = String::from_utf8(Command::new("ls").arg("-t").arg(in_path).output()?.stdout)?; remove_dir_all(out_path)?; let dir = DirBuilder::new(); dir.create(out_path)?; File::create(format!("{out_path}/style.css"))?.write_all(CSS)?; let list = LIST_TEMPLATE.replace( "{{ CONTENT }}", &format!( "<ul>{}</ul>", articles .lines() .filter_map(|file| { if file.ends_with(".md") { let article = file.replace(".md", ""); Some(format!("<li><a href=\"{article}\">{article}</a></li>")) } else { None } }) .collect::<Vec<_>>() .join("\n") ), ); File::create(format!("{out_path}/index.html"))?.write_all(list.as_bytes())?; articles .lines() .par_bridge() .into_par_iter() .for_each(|dir| { if dir.ends_with(".md") { deal_with_dir(in_path, &dir.replace(".md", ""), out_path).unwrap(); } else { Command::new("cp") .arg(format!("{in_path}/{dir}")) .arg(out_path) .output() .unwrap(); } }); Ok(()) }
Oh boy. The ?
is doing a lot of work, which is… great! That’s great and makes this much better. It gets five asterisks: *****. Hooray! It starts by removing the entire out directory, not accounting for its potential non-existence. It stops early if it doesn’t exist but it really just adds unnecessary friction. Then it remakes the out directory and puts in the basic CSS.3
And then.
The attack of the iterator adapters. It’s not completely unreadable, but it really would benefit from some letification (Hint: A map is just a Thing, you can assign it to a variable for readablity). Also the rayon parallelism is just stupid. I’m not knocking the library, just my usage of it. It’s just so pointless. And I knew that at the time too! The first and only commit message is: “add rayon as if it is at all necessary, will probably just slow it down” Let’s just blow that up for maximum sadness:
add rayon as if it is at all necessary, will probably just slow it down
So sad, so so sad. I genuinely cannot think of a way to convey more defeatedness in the same amount of space, impressive.
Oh and it’s called higo because it’s “like hugo but for i instead of u”
I’m using hugo now.4