Higo Post-“Mortem”

Start­ed
Pub­lished
Last up­dat­ed (ty­pos & com­mas)

About sev­en months ago I made a sta­t­ic site gen­er­a­tor. Then I made it again.

The first “gen­er­a­tor”#

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 pret­ty good! Ob­vi­ous­ly a main func­tion should be much more in­ter­ac­tive, but this is very much a test so it’s cool. Oth­er than that, pret­ty nice, and nice use of a let and the love­ly for­mat args cap­ture;1 a younger me would have omit­ted the in­ter­me­di­ate vari­able and kept it to a sin­gle stat­ment, much to the detri­ment of read­abil­i­ty. That re­place func­tion is so sim­ple, but what does the sec­ond arg do? Oh also what the flarp is that syn­tax, yeesh. {** **}, {* *}, and {` `} are pret­ty self ev­i­dent, but I have no clue what {= =} is sup­posed 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 tech­ni­cal­ly2 well done, but, con­cep­tu­al­ly, it’s, uh, flawed.

Let’s start with the good parts: This tru­ly is read­able! I think some­one with ba­sic rust knowl­edge (and a touch of syn­tax high­light­ing for the for­mat! macro) could read this and un­der­stand what it’s do­ing. Tu­ple de­struc­tur­ing is put to great ef­fect, here’s what it would look like with­out 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.

Speak­ing of ew: the bad parts. This is real­ly stu­pid. I mean come on, even for a su­per ear­ly test this is just ab­surd. Cus­tomiza­tion is crit­i­cal for any even re­mote­ly se­ri­ous sta­t­ic site gen­er­a­tor. And the syn­tax, my gosh the syn­tax! This abor­tion of a “mark­down” is about as far from HTML in ver­bosi­ty as Cana­da is from Cana­da. {**} is lit­er­al­ly the ex­act same length as <b> I mean come on. Ease of im­ple­men­ta­tion is nice, and all, ex­cept wait no it isn’t? That is just an ab­surd thing to pri­or­i­tize, even for a hob­by project. Imag­ine brag­ging about how easy your shoes were for some­one else to make, GPI almighty.

Rant­i­ng aside, just kid­ding ha­ha­ha­hah why on earth would you use curly braces of all things, they’re an­noy­ing to type, es­pe­cial­ly on my key­board that I was us­ing at the time, where [ and ] are be­hind a lay­er which means that curly braces re­quire press­ing three keys at once. Paren­the­ses would have been much bet­ter, note to no one.

Rant­i­ng ac­tu­al­ly aside, this is just okay. The HTML be­ing gen­er­at­ed with format! is sketchy but who cares, this wasn’t made for un­trust­ed in­put.

The first “gen­er­a­tor”’s tests#

Did I men­tion that I named it hugo_but_wo_rs_e?

The sec­ond gen­er­a­tor#

First of all: If you want to look at every­thing for some rea­son (there’s some mild­ly fun­ny stuff I guess) here it is.

Now this ver­sion is more in­ter­est­ing thanks in large part to this crazy fea­ture it has: ac­tu­al­ly work­ing. Thank­ful­ly for cur­rent me, this is an­oth­er sin­gle file af­fair, which makes com­men­tifi­ca­tion much eas­i­er.

Pret­ty stan­dard 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 prob­a­bly just im­port std::env and std::fs, but that’s a nit­pick.

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 fal­li­ble but still. It’s not like there’s ac­tu­al er­ror han­dling, so it’s real­ly just less clear.

const TEMPLATE: &str = include_str!("article.dumb_template");

The choice to not use a full tem­plat­ing lan­guage like liq­uid or han­dle­bars was def­i­nite­ly right for this stage of the project. The tem­plate is just HTML with {{ TITLE }} and {{ CONTENT }} placed in a rea­son­able place. I un­der­stand why I chose to use include_str! (don’t have to wor­ry about the path at run­time, ease of use, ac­tu­al­ly main­ly easy of use), but it once again ab­solute­ly de­stroys any cus­tomiz­abil­i­ty. Did I real­ly not care about hav­ing to com­pile every time I made a change???

Mov­ing on:

fn gen_html(raw: &str, title: &str) -> String {
    let content = markdown::to_html(raw)
        .replace("&lt;&lt;", "<img src=\"../")
        .replace("&gt;&gt;", "\">");
    TEMPLATE
        .replace("{{ TITLE }}", title)
        .replace("{{ CONTENT }}", &content)
}

This is good. Good func­tion bound­ary, that img short­cut is kind of weird but it works. Not much to say, al­though I can see now how hav­ing a glob­al tem­plate re­duces 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. Us­ing Path’s join method (which I just found out about) would be bet­ter for sure, but the format! macro is easy and ar­guably even more clear. The Box<dyn Error> is a com­mon theme in this pro­gram, which is con­ve­nient but has prob­lems. Name­ly, it makes er­ror re­cov­ery much hard­er, and it has a sol­id amount of over­head. I un­der­stand rolling your own er­ror type is a pain, but it def­i­nite­ly has ben­e­fits.

const CSS: &[u8] = include_str!("style.css").as_bytes();
const LIST_TEMPLATE: &str = include_str!("list.dumb_template");

I get that I was try­ing to keep things near where they’re used but that’s kind of stu­pid. It’s al­most cer­tain­ly bet­ter to just keep con­stants near each oth­er. Of course, I could have had the best of both worlds if I had ac­tu­al­ly split stuff up log­i­cal­ly.

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 do­ing a lot of work, which is… great! That’s great and makes this much bet­ter. It gets five as­ter­isks: *****. Hooray! It starts by re­mov­ing the en­tire out di­rec­to­ry, not ac­count­ing for its po­ten­tial non-ex­is­tence. It stops ear­ly if it doesn’t ex­ist but it real­ly just adds un­nec­es­sary fric­tion. Then it re­makes the out di­rec­to­ry and puts in the ba­sic CSS.3

And then.

The at­tack of the it­er­a­tor adapters. It’s not com­plete­ly un­read­able, but it real­ly would ben­e­fit from some leti­fi­ca­tion (Hint: A map is just a Thing, you can as­sign it to a vari­able for read­abli­ty). Also the ray­on par­al­lelism is just stu­pid. I’m not knock­ing the li­brary, just my us­age of it. It’s just so point­less. And I knew that at the time too! The first and only com­mit mes­sage is: “add ray­on as if it is at all nec­es­sary, will prob­a­bly just slow it down” Let’s just blow that up for max­i­mum sad­ness:

add ray­on as if it is at all nec­es­sary, will prob­a­bly just slow it down

So sad, so so sad. I gen­uine­ly can­not think of a way to con­vey more de­feat­ed­ness in the same amount of space, im­pres­sive.

Oh and it’s called higo be­cause it’s “like hugo but for i in­stead of u”

I’m us­ing hugo now.4


  1. which fol­lows the un­for­tu­nate rust theme of be­ing just sli­i­ight­ly worse than it could be, name­ly by not let­ting you ac­cess struct fields ↩︎
  2. I mean tech­ni­cal­ly as in how it’s specif­i­cal­ly ex­e­cut­ed, I’m not say­ing that this is well done in gen­er­al (be­cause it isn’t). ↩︎
  3. The CSS is hi­lar­i­ous by the way, in­clud­ing a CSS re­set, which just shows ut­ter des­per­a­tion. Again, hi­lar­i­ous. ↩︎
  4. up­date: i’m ob­vi­ous­ly not any­more ↩︎