git.m455.casa

m455.casa

clone url: git://git.m455.casa/m455.casa


posts/meet-lol-my-new-website-generator.txt

1 title: meet lol, my new website generator!
2
3 2022-08-08 00:00
4
5 i've been pretty busy lately, so i haven't had a lot of time to explore
6 computers, which is what i love doing. in the last few months, i've been trying
7 to fight back against busyness by writing a new website generator after
8 everyone goes to bed. i can't recommend doing this, because it gets exhausting
9 after a while, but it gives me my kicks and makes me happy.
10
11 my old website generator, [https://git.m455.casa/wg|wg], was a wrapper around
12 pandoc, and was written in [https://fennel-lang.org/|fennel]. i used separate
13 fennel scripts to generate a list of posts and an rss feed as post-thoughts to
14 the website generator. also, I didn't know about
15 [https://www.wireguard.com/|wireguard] when I programmed `wg`. for those of you
16 who don't know, wireguard uses the command-line name `wg`, so it was best that
17 i didn't compete with that haha.
18
19 i still love pandoc and fennel, but I wanted to try to program something that
20 had the following features:
21
22 - uses a configuration file
23 - copies the directory structure from the source directory to the build
24 directory
25 - generates a list of posts from a directory that's specified in the
26 configuration file
27 - generates an RSS feed of the posts and their content.
28 - replaces `{{variables}}` in markdown files with values that are declared in
29 the configuration file
30 - provides HTML and RSS templates
31
32 i also wanted an excuse to make a bigger programming project in
33 [https://call-cc.org/|chicken scheme] haha.
34
35 i think what I'm most proud of for this project is that I was able to implement
36 string templates. for example, the `{{im-an-example}}` in the text below would
37 be replaced with the value that corresponds to the `im-an-example` key in a
38 `config.scm` file.
39
40 ```
41 Hey there, this a sentence, and my name is {{im-an-example}}.
42 ```
43
44 though, this wasn't that easy.
45
46 first, i had to implement string replacement... okay, okay, string replacement
47 exists in chicken scheme using the `string-translate`, `string-translate*`,
48 `irregex-replace`, or `irregex-replace/all` procedures, but where's the fun in
49 using those? i don't get to build anything!
50
51 my first step was to write a procedure that replaced the first occurrence of a
52 string. i ended up using the `string-append`, `substring`, and `string-length`
53 procedures to implement the following procedure:
54
55 ```
56 (define (str-replace str from-str to-str)
57 (let* ((from-index (string-contains str from-str))
58 (index-offset (+ from-index (string-length from-str))))
59 (if from-index
60 (string-append (substring str 0 from-index)
61 to-str
62 (substring str
63 index-offset
64 (string-length str)))
65 str)))
66 ```
67
68 which isn't very useful if you plan on having several of the same placeholder
69 values in one string, so i also needed to write a procedure to replace all
70 occurrences of the string. it will drop into an infinite loop if i try to
71 replace `l` with `ll`, but this is personal programming, not some software that
72 needs to be battle tested, so i settled with my implementation below:
73
74 ```
75 (define (str-replace-all str from-str to-str)
76 (let* ((from-index (string-contains str from-str))
77 (index-offset (+ from-index (string-length from-str))))
78 (if from-index
79 (let ((rest-of-string (substring str
80 index-offset
81 (string-length str))))
82 (string-append
83 (substring str 0 from-index)
84 to-str
85 (str-replace-all rest-of-string from-str to-str)))
86 str)))
87 ```
88
89 next, i needed somehow to take a list of pairs, convert the first item in each
90 pair to a string, and then surround the string with `{{` and `}}`, so it
91 resembles one of the placeholder values that i mentioned earlier. after it
92 changed the first element in each pair, i then took the first element of each
93 pair, searched for it in the provided string, and then replaced it with the
94 second element, using the `str-replace-all` procedure to ensure all instances
95 of that placeholder were replaced.
96
97 i actually ended up having to split this algorithm into two procedures to keep
98 things maintainable for myself in case i needed to go back to fix or update the
99 code around this functionality. here are those two procedures:
100
101 ```
102 (define (key->mustached-key pair)
103 (if (pair? pair)
104 (let* ((key (symbol->string (car pair)))
105 (mustached-key (string-append "{{" key "}}"))
106 (value (cadr pair)))
107 `(,mustached-key ,value))
108 pair))
109
110 (define (string-populate str alist)
111 (if (null? alist)
112 str
113 (let* ((mustached-keys (map key->mustached-key alist))
114 (first-pair (car mustached-keys))
115 (key (car first-pair))
116 (val (cadr first-pair)))
117 (string-populate
118 (str-replace-all str key val)
119 (cdr alist)))))
120 ```
121
122 this ended up helping me get really good at quasiquoting in scheme as well!
123
124 apart from the `string-populate` procedure, and the core procedures that it's
125 built on, most of the other features aren't anything special, though i did
126 enjoy that i can just read arbitrary s-expressions from a string using scheme's
127 `read` procedure. the `read` procedure made it super easy to read a
128 configuration file that was all s-expressions. for example, all i needed to do
129 was load an
130 [https://www.gnu.org/software/mit-scheme/documentation/stable/mit-scheme-ref/Association-Lists.html|alist]
131 in a file with the following procedure:
132
133 ```
134 (define (load-config-file)
135 (if (file-exists? config-file)
136 (with-input-from-file config-file read)
137 #f))
138 ```
139
140 this procedure returns a quoted alist, so i wrote the following helper
141 procedure to read it:
142
143 ```
144 (define (get alist key)
145 (if (and (pair? alist)
146 (pair? (car alist))
147 (symbol? key))
148 (cadr (assq key alist))
149 alist))
150 ```
151
152 functional programming purists will hate me for this, but this then allowed me
153 set a globally mutated variable with `(set! config-data (load-config-file))`,
154 and then read the variable with a `(get config-data 'source-dir)`.
155
156 i've been using this method for reading and reloading configuration files for
157 other projects as well, so that was a great learning experience.
158
159 as for generating my list of posts and rss feed, all i needed to do was parse
160 each markdown file in a directory that's specified in the configuration file.
161 to make things easy, the title of a post was extracted from the first line of a
162 file, which should always be a markdown h1 heading. i would then take the
163 markdown heading, for example, `# hey i'm a heading`, and remove the number
164 sign and space proceeding the number sign, leaving me with `hey i'm a heading`.
165
166 the remaining string would be used as the title for each post in the list of
167 posts page, and the title of each rss item. the way i generated links for my
168 list of posts page was by converting the source path from, as an example,
169 `<source-dir>/path/to/post.md` to `https://<domain>/path/to/post.html`.
170
171 because dates are pretty important to rss feeds, although not required, if
172 you're following the spec, i chose to put dates on the third line of each post,
173 in the format of `yyyy-mm-dd`, so i could convert `yyyy-mm-dd` to a number that
174 resembled `yyyymmdd`, and then reverse sort by each number, resulting in a
175 "latest post first, oldest post last" order.
176
177 to kind of finish this off, i think one of the major annoyances was converting
178 all fenced code blocks to use indentation instead, because chicken scheme's
179 lowdown egg replicates what the original markdown parser does. that, and
180 replacing all of my pandoc-centric markdown stuff such as its markdown version
181 of `<div>` blocks:
182
183 ```
184 :::{.im-a-class}
185 hey im a div
186 :::
187 ```
188
189 the upside to using old school, feature-less markdown is that the markdown for
190 my website will work on most markdown parsers i guess? haha.
191
192 the downside to using the lowdown markdown parser is that heading anchors
193 aren't generated, so all of my links to heading anchors are broken, but i got
194 to have fun with programming in scheme at least? plus, this isn't my
195 professional website, so things are allowed to be broken here, and i don't want
196 to get rid of old posts because they bring back good programming adventure
197 memories for me.
198
199 i figured this blog could use a new post, so here it is!
200
201 have a good one!
202
203 if you want to check out the source code for my new website generator, you can
204 view it [https://git.m455.casa/lol/|here].