meet lol, my new website generator!

2022-08-08 00:00

I've been pretty busy lately, so I haven't had a lot of time to explore computers, which is what I love doing. In the last few months, I've been trying to fight back against busyness by writing a new website generator after everyone goes to bed. I can't recommend doing this, because it gets exhausting after a while, but it gives me my kicks and makes me happy.

My old website generator, wg, was a wrapper around Pandoc, and was written in Fennel. I used separate fennel scripts to generate a list of posts and an RSS feed as post-thoughts to the website generator. Also, I didn't know about Wireguard when I programmed wg. For those of you who don't know, Wireguard uses the command-line name wg, so it was best that I didn't compete with that haha.

I still love Pandoc and Fennel, but I wanted to try to program something that had the following features:

I also wanted an excuse to make a bigger programming project in Chicken Scheme haha.

I think what I'm most proud of for this project is that I was able to implement string templates. For example, the {{im-an-example}} in the text below would be replaced with the value that corresponds to the im-an-example key in a config.scm file.

Hey there, this a sentence, and my name is {{im-an-example}}.

Though, this wasn't that easy.

First, I had to implement string replacement... Okay, okay, string replacement exists in Chicken Scheme using the string-translate, string-translate*, irregex-replace, or irregex-replace/all procedures, but where's the fun in using those? I don't get to build anything!

My first step was to write a procedure that replaced the first occurrence of a string. I ended up using the string-append, substring, and string-length procedures to implement the following procedure:

(define (str-replace str from-str to-str)
  (let ((from-index (string-contains str from-str)))
    (if from-index
      (string-append (substring str 0 from-index)
                     to-str
                     (substring str
                                (+ from-index (string-length from-str))
                                (string-length str)))
      str)))

This isn't very useful if you plan on having several of the same placeholder values in one string, so I also needed to write a procedure to replace all occurrences of the string. It will drop into an infinite loop if I try to replace l with ll, but this is personal programming, not some software that needs to be battle tested, so I settled with my implementation below:

(define (str-replace-all str from-str to-str)
  (let ((from-index (string-contains str from-str)))
    (if from-index
      (let ((rest-of-string (substring str
                                       (+ from-index (string-length from-str))
                                       (string-length str))))
        (string-append (substring str 0 from-index)
                       to-str
                       (str-replace-all rest-of-string from-str to-str)))
      str)))

Next, I needed somehow to take a list of pairs, convert the first item in each pair to a string, and then surround the string with {{ and }}, so it resembles one of the placeholder values that I mentioned earlier. After it changed the first element in each pair, I then took the first element of each pair, searched for it in the provided string, and then replaced it with the second element, using the str-replace-all procedure to ensure all instances of that placeholder were replaced.

I actually ended up having to split this algorithm into two procedures to keep things maintainable for myself in case I needed to go back to fix or update the code around this functionality. Here are those two procedures:

(define (key->mustached-key pair)
  (if (pair? pair)
    (let* ((key (symbol->string (car pair)))
           (mustached-key (string-append "{{" key "}}"))
           (value (cadr pair)))
    `(,mustached-key ,value))
    pair))

(define (string-populate str kv-replacements)
  (if (null? kv-replacements)
    str
    (let* ((mustached-keys (map key->mustached-key kv-replacements))
           (first-pair (car mustached-keys))
           (key (car first-pair))
           (val (cadr first-pair)))
      (string-populate
        (str-replace-all str key val)
        (cdr kv-replacements)))))

This ended up helping me get really good at quasiquoting in Scheme as well!

Apart from the string-populate procedure, and the core procedures that it's built on, most of the other features aren't anything special, though I did enjoy that I can just read arbitrary s-expressions from a string using Scheme's read procedure. The read procedure made it super easy to read a configuration file that was all s-expressions. For example, all I needed to do was load an alist in a file with the following procedure:

(define (load-config-file)
  (if (file-exists? config-file)
    (with-input-from-file config-file read)
    #f))

This procedure returns a quoted alist, so I wrote the following helper procedure to read it:

(define (get alist key)
  (if (and (pair? alist)
           (pair? (car alist))
           (symbol? key))
    (cadr (assq key alist))
    alist))

Functional programming purists will hate me for this, but this then allowed me set a globally mutated variable with (set! config-data (load-config-file)), and then read the variable with a (get config-data 'source-dir).

I've been using this method for reading and reloading configuration files for other projects as well, so that was a great learning experience.

As for generating my list of posts and RSS feed, all I needed to do was parse each Markdown file in a directory that's specified in the configuration file. To make things easy, the title of a post was extracted from the first line of a file, which should always be a Markdown H1 heading. I would then take the Markdown heading, for example, # hey i'm a heading, and remove the number sign and space proceeding the number sign, leaving me with hey i'm a heading.

The remaining string would be used as the title for each post in the list of posts page, and the title of each RSS item. The way I generated links for my list of posts page was by converting the source path from, as an example, <source-dir>/path/to/post.md to https://<domain>/path/to/post.html.

Because dates are pretty important to RSS feeds, although not required, if you're following the spec, I chose to put dates on the third line of each post, in the format of yyyy-mm-dd, so I could convert yyyy-mm-dd to a number that resembled yyyymmdd, and then reverse sort by each number, resulting in a "latest post first, oldest post last" order.

To kind of finish this off, I think one of the major annoyances was converting all fenced code blocks to use indentation instead, because Chicken Scheme's lowdown egg replicates what the original Markdown parser does. That, and replacing all of my Pandoc-centric Markdown stuff such as its Markdown version of <div> blocks:

:::{.im-a-class}
hey im a div
:::

The upside to using old school, feature-less Markdown is that the Markdown for my website will work on most Markdown parsers I guess? Haha.

The downside to using the lowdown Markdown parser is that heading anchors aren't generated, so all of my links to heading anchors are broken, but I got to have fun with programming in Scheme at least? Plus, this isn't my professional website, so things are allowed to be broken here, and I don't want to get rid of old posts because they bring back good programming adventure memories for me.

I figured this blog could use a new post, so here it is!

Have a good one!

If you want to check out the source code for my new website generator, you can view it here.