feat(articles): 0002-confgen-solving-config-files
This commit is contained in:
parent
893ffee3bb
commit
6bf4b97108
5 changed files with 391 additions and 18 deletions
31
confgen.lua
31
confgen.lua
|
@ -18,25 +18,20 @@ cg.opt.renderMarkdown = function(src)
|
|||
if node_type == cmark.NODE_CODE_BLOCK then
|
||||
-- Syntax highlighting with Bat and AHA
|
||||
local lang = cmark.node_get_fence_info(cur)
|
||||
if #lang > 0 then
|
||||
local tmpfp = os.tmpname()
|
||||
local proc = io.popen(
|
||||
"bat --color always --style plain --language " .. lang .. " | aha --no-header >" .. tmpfp,
|
||||
"w"
|
||||
)
|
||||
proc:write(cmark.node_get_literal(cur))
|
||||
proc:close()
|
||||
local tmpfp = os.tmpname()
|
||||
local proc = #lang > 0 and io.popen(
|
||||
"bat --color always --style plain --language " .. lang .. " | aha --no-header >" .. tmpfp,
|
||||
"w"
|
||||
) or io.popen("aha --no-header >" .. tmpfp, "w")
|
||||
proc:write(cmark.node_get_literal(cur))
|
||||
proc:close()
|
||||
|
||||
local tmpf = io.open(tmpfp, "r")
|
||||
local new = cmark.node_new(cmark.NODE_HTML_BLOCK)
|
||||
cmark.node_set_literal(
|
||||
new,
|
||||
[[<pre class="codeblock"><code>]] .. tmpf:read "*a" .. [[</code></pre>]]
|
||||
)
|
||||
cmark.node_replace(cur, new)
|
||||
tmpf:close()
|
||||
os.remove(tmpfp)
|
||||
end
|
||||
local tmpf = io.open(tmpfp, "r")
|
||||
local new = cmark.node_new(cmark.NODE_HTML_BLOCK)
|
||||
cmark.node_set_literal(new, [[<pre class="codeblock"><code>]] .. tmpf:read "*a" .. [[</code></pre>]])
|
||||
cmark.node_replace(cur, new)
|
||||
tmpf:close()
|
||||
os.remove(tmpfp)
|
||||
elseif node_type == cmark.NODE_LINK then
|
||||
-- This basically reimplements links in order to make them open in a new tab.
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ function main() {
|
|||
.fetch("/articles.json")
|
||||
.then(r -> r.json())
|
||||
.then((j:Array<Article>) -> {
|
||||
j.reverse();
|
||||
filter.articles = j;
|
||||
for (a in j) {
|
||||
filter.articleDiv.appendChild(new ArticleElement(a));
|
||||
|
|
7
meta/0002-confgen-solving-config-files.lua
Normal file
7
meta/0002-confgen-solving-config-files.lua
Normal file
|
@ -0,0 +1,7 @@
|
|||
return {
|
||||
id = "0002-confgen-solving-config-files",
|
||||
title = "Confgen: Solving Config Files",
|
||||
summary = "The hassle of managing config files and other text-based content is no more!",
|
||||
date = "2024-08-21",
|
||||
tags = { "code", "confgen", "config", "lua", "template-engine" },
|
||||
}
|
358
src/a/0002-confgen-solving-config-files/index.html.cgt
Normal file
358
src/a/0002-confgen-solving-config-files/index.html.cgt
Normal file
|
@ -0,0 +1,358 @@
|
|||
<!
|
||||
-- vim: ft=markdown
|
||||
local meta = opt.articles[2]
|
||||
tmpl:setPostProcessor(opt.mkArticle(meta))
|
||||
|
||||
local oe, ce = "<" .. "%", "%" .. ">"
|
||||
local oc, cc = "<" .. "!", "!" .. ">"
|
||||
!>
|
||||
If you've known me for any amount of time, you'll probably have witnessed me bragging about this,
|
||||
only to be disappointed when I ultimately had to answer that it's kind of a steep learning curve.
|
||||
This is what I'm trying to solve here by giving you an introduction as gentle as I'm able to make it
|
||||
to [Confgen](https://git.mzte.de/LordMZTE/confgen), the last template engine you will ever need.
|
||||
|
||||
Despite its namesake, Confgen is good at far more than generating config files. It ultimately
|
||||
evolved into a universal template engine for generating... anything you could think of.
|
||||
|
||||
## What even is this?
|
||||
Confgen is a template engine. If you've never heard that term before, it's basically a tool that
|
||||
takes in a file with placeholders in it, and then inserts some data in place of them. This might
|
||||
sound unspectacular at first, but that's also a gross oversimplification. Confgen, for one, doesn't
|
||||
only let you have placeholders (expression blocks), but also lets you have control flow (code
|
||||
blocks). `for`s, `if`s and functions are all fair game. What makes Confgen even more special is
|
||||
that, unlike most template engines, it doesn't implement all these daunting concepts itself, but
|
||||
instead harnesses the full power of the [Lua](https://lua.org) programming language (basic knowledge
|
||||
of which will be considered a prerequisite for the rest of this article, but it's an easy language
|
||||
to learn!).
|
||||
|
||||
If you're still not convinced, the page you are reading right now is, in fact, generated by Confgen,
|
||||
from Markdown. Syntax highlighting and all.
|
||||
|
||||
> Markdown, for those uninitiated, is a very easy to read markup language, which is fun to write in!
|
||||
> Think of it as HTML in easy mode.
|
||||
|
||||
This is roughly what the source of this page looks:
|
||||
```markdown
|
||||
## Header here!
|
||||
This paragraph contains a **bold** statement.
|
||||
|
||||
> I'm a quote!
|
||||
```
|
||||
..and so on.
|
||||
|
||||
All that is turned into HTML by Confgen, so your browser can show it while I can still keep the last
|
||||
remaining bit of my sanity while writing. So now that you're convinced, let's get you started!
|
||||
|
||||
> **Note**: Bring some form of brain coolant :D
|
||||
|
||||
## Installation
|
||||
Please refer to [the Confgen repository README](https://git.mzte.de/LordMZTE/confgen) for
|
||||
instructions. If you're on NixOS, a simple `nix shell git+https://git.mzte.de/LordMZTE/confgen` will
|
||||
suffice. You can also find Linux binaries built on a Debian CI on the
|
||||
[releases tab](https://git.mzte.de/LordMZTE/confgen/releases), but those may be outdated.
|
||||
On other platforms (except Windows, which it wasn't worth having **backwards**-compatibility for),
|
||||
you'll need to build from source using a [Zig](https://ziglang.org) compiler.
|
||||
|
||||
## Your first Confgenfile
|
||||
Now that you've got Confgen ready to roll, let's experiment with it! You can do this in a blank
|
||||
directory, your dotfiles, or, if you're feeling adventurous, your website's source code.
|
||||
|
||||
Confgen is centered around the so-called *Confgenfile*, commonly named `confgen.lua`. It describes
|
||||
the layout of the project, declares common values, functions and tells confgen what files to
|
||||
process. Assuming that you want to generate your configuration files, this is what it might look
|
||||
like.
|
||||
|
||||
> **Note**: This file would be placed in your dotfiles *source*, not your home directory. The `.config`
|
||||
> here is not `~/.config`!
|
||||
|
||||
```lua
|
||||
-- confgen.lua
|
||||
|
||||
cg.addPath(".config")
|
||||
|
||||
cg.opt = {
|
||||
font = "MyFavoriteFont",
|
||||
font_size = "11",
|
||||
}
|
||||
```
|
||||
Alright, now what does this actually mean? Let's start with the basic order of operations Confgen
|
||||
works in.
|
||||
- Confgen starts off by evaluating the *Confgenfile*, like a normal Lua program. Here, you may
|
||||
declare functions, options by adding to the `cg.opt` table, or execute any arbitrary Lua code, for
|
||||
example to load machine-local options.
|
||||
- During this evaluation, Confgen builds a list of input files. As the user, it's your job to add to
|
||||
this list. Here, `cg.addPath` will recursively add all files inside the given directory to the list.
|
||||
There are more functions available, such as `cg.addFile`. For a full documentation of the API,
|
||||
please refer to `man 3 confgen`.
|
||||
- Then, Confgen will iterate through the file list built during the evaluation phase. Each file
|
||||
ending in `.cgt` (Confgen Template) will be evaluated as a template (more on that in a second),
|
||||
while other files will be copied to the output directory.
|
||||
|
||||
## The first template (of many)
|
||||
If you now run `confgen confgen.lua output` (specifying your Confgenfile and an output directory),
|
||||
you will end up with a disappointingly empty directory. Let's fix that by creating a configuration
|
||||
file template, in this case for a hypothetical terminal emulator called `coolterm`.
|
||||
```ini
|
||||
# .config/coolterm/config.ini.cgt
|
||||
|
||||
font = "<% oe %> opt.font <% ce %>:<% oe %> opt.font_size <% ce %>"
|
||||
|
||||
[keybinds]
|
||||
zoom-in = "ctrl+plus"
|
||||
zoom-out = "ctrl+minus"
|
||||
```
|
||||
First, note the `.cgt` we added to that filename (since coolterm expects a file called
|
||||
`~/.config/coolterm/config.ini`). This is how Confgen knows that it should not just copy the file.
|
||||
Confgen will strip this second extension during evaluation.
|
||||
|
||||
If we now run `confgen confgen.lua output` again, we'll find a fully generated configuration file:
|
||||
```ini
|
||||
# output/.config/coolterm/config.ini
|
||||
|
||||
font = "MyFavoriteFont:11"
|
||||
|
||||
[keybinds]
|
||||
zoom-in = "ctrl+plus"
|
||||
zoom-out = "ctrl+minus"
|
||||
```
|
||||
|
||||
Now, this might seem like a massively overcomplicated way of declaring a font in a terminal
|
||||
configuration, but hear me out. Let's say we also want to configure GTK (or anything else that wants
|
||||
to know a font). We can simply add this to our files:
|
||||
```ini
|
||||
# .config/gtk-3.0/settings.ini.cgt
|
||||
|
||||
[Settings]
|
||||
gtk-font-name=<% oe %> opt.font <% ce %> <% oe %> opt.font_size <% ce %>
|
||||
```
|
||||
We've just deduplicated our font name and size! Whereas previously, if you changed your mind about
|
||||
your favorite font, you'd had to have to change two files, you now only need to adjust it in your
|
||||
`confgen.lua`! Sure, sounds unspectacular at first, after all, why go to all of this trouble for a
|
||||
slight gain in convenience in a very rare situation, but just look at my (admittedly
|
||||
mind-bogglingly gigantic) config files:
|
||||
```bash
|
||||
$ rg --hidden 'opt.font' | wc -l
|
||||
30
|
||||
```
|
||||
Not so insignificant anymore, is it? And this is also far from all Confgen has to offer!
|
||||
|
||||
## But wait, there's more (*way* more)!
|
||||
|
||||
Say that you'd like to use [Waybar](https://github.com/Alexays/Waybar), but you're also not sure
|
||||
if you want to use Hyprland or River as your wayland compositor of choice, but you want workspaces
|
||||
and the title of the focused window in your bar no matter, which one you're using. Let's create a
|
||||
config!
|
||||
|
||||
```json
|
||||
// .config/waybar/config.jsonc.cgt
|
||||
|
||||
{
|
||||
"modules-left": [
|
||||
<% oc %> if opt.compositor == "river" then <% cc %>
|
||||
"river/tags",
|
||||
<% oc %> elseif opt.compositor == "hyprland" then <% cc %>
|
||||
"hyprland/workspaces",
|
||||
<% oc %> end <% cc %>
|
||||
|
||||
"<% oe %> opt.compositor <% ce %>/window"
|
||||
],
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Now, it's just a matter of adding a `cg.opt.compositor = "X"` to your Confgenfile and rebuilding!
|
||||
And of course, this is not limited to just `if`s! `for`s as well as anything else Lua allows is
|
||||
possible! Sky's the limit!
|
||||
|
||||
> **Note**: Seems inconvenient to rebuild your config every time you switch compositors, doesn't it?
|
||||
> This problem is solved through ConfgenFS, where the config file is able to automagically change
|
||||
> each time either compositor starts. You can even have non-deterministic config files (sorry for
|
||||
> destroying your hopes and dreams, dear NixOS user reading). Article coming soon!
|
||||
|
||||
This is still not optimal, though. After all, it's JSON and that's inconvenient to work with.
|
||||
Wouldn't it be nice if this were Lua too? Well, this is where post-processors come into play!
|
||||
|
||||
```lua
|
||||
-- .config/waybar/config.jsonc.cgt
|
||||
<% oc %> tmpl:setPostProcessor(function(prev)
|
||||
local value = loadstring(prev)() -- Load and evaluate the Lua code this template generates.
|
||||
return cg.fmt.json.serialize(value) -- Serialize it into JSON using Confgen's builtin serializer.
|
||||
end) <% cc %>
|
||||
|
||||
return {
|
||||
["modules-left"] = {
|
||||
<% oc %> if opt.compositor == "river" then <% cc %>
|
||||
"river/tags",
|
||||
<% oc %> elseif opt.compositor == "hyprland" then <% cc %>
|
||||
"hyprland/workspaces",
|
||||
<% oc %> end <% cc %>
|
||||
|
||||
"<% oe %> opt.compositor <% ce %>/window"
|
||||
},
|
||||
-- ...
|
||||
}
|
||||
```
|
||||
|
||||
While this snippet will probably prevent any experienced Lua programmer from sleeping well for a
|
||||
week, it's still kinda cool, don't you agree? Since this might be a little hard to digest, I'll
|
||||
break it down.
|
||||
|
||||
Templates (referenced through the `tmpl` global variable inside the template's code) may have a
|
||||
post-processor. A post-processor is nothing more than a function that will be called by Confgen once
|
||||
the template itself is already done generating. The function will be passed the result of the
|
||||
template and return a modified version. In this case, the content before the post-processor is the
|
||||
Lua code under the "header", and the modified version is the JSON value Waybar will see. This
|
||||
post-processor is set by calling the `setPostProcessor` function on the template. In this case, it
|
||||
has a rather simple implementation.
|
||||
|
||||
And this is precisely how the source code of this article has been converted from Markdown to HTML.
|
||||
It's a post-processor, albeit one with a far more complicated implementation.
|
||||
|
||||
## How is this sorcery possible?!
|
||||
As you write more complex templates, it's always important to remember the gist of how they work
|
||||
under the hood. While a Lua runtime certainly is happy evaluating single statements as those you
|
||||
might know from `<% oe %> ... <% ce %>` blocks, it definetely won't swallow partial statements and
|
||||
unclosed delimiters such as those you'll often have in `<% oc %> ... <% cc %>` blocks. The trick
|
||||
Confgen uses to get around this is to **compile the template to Lua**, which is then evaluated in
|
||||
one go per template. For debugging your templates, Confgen provides a command to compile a template
|
||||
to this intermediate Lua, `confgen --compile`. Let's try it on our waybar config by invoking
|
||||
`confgen -c .config/waybar/config.jsonc.cgt` (cleaned up a bit for ease of reading):
|
||||
|
||||
```lua
|
||||
tmpl:pushLitIdx(tmplcode, 0)
|
||||
|
||||
tmpl:setPostProcessor(function(prev)
|
||||
local value = loadstring(prev)() -- Load and evaluate the Lua code this template generates.
|
||||
return cg.fmt.json.serialize(value) -- Serialize it into JSON using Confgen's builtin serializer.
|
||||
end)
|
||||
|
||||
tmpl:pushLitIdx(tmplcode, 1)
|
||||
|
||||
if opt.compositor == "river" then
|
||||
tmpl:pushLitIdx(tmplcode, 2)
|
||||
elseif opt.compositor == "hyprland" then
|
||||
tmpl:pushLitIdx(tmplcode, 3)
|
||||
end
|
||||
|
||||
tmpl:pushLitIdx(tmplcode, 4)
|
||||
|
||||
tmpl:pushValue(opt.compositor)
|
||||
|
||||
tmpl:pushLitIdx(tmplcode, 5)
|
||||
```
|
||||
|
||||
This should look familiar. All our `<% oc %> ... <% cc %>` blocks have been inserted here verbatim,
|
||||
like our post-processor! This is what makes arbitrary control flow possible. Contrarily, our
|
||||
`<% oe %> ... <% ce %>` blocks have been replaced with `tmpl:pushValue(...)`. `pushValue` is simply a
|
||||
mostly invisible function from Confgen that turns the given value to a string and appends it to the
|
||||
output. Something else that should be jumps out here are all those calls to `pushLitIdx` (push
|
||||
literal (at) index). In short, Confgen uses this number to look up a span from your template's
|
||||
source code, stored in the opaque `tmplcode` object to append to the output.
|
||||
|
||||
> `tmplcode` is a separate object from `tmpl`, which is required when templates either don't have
|
||||
> their own source or use the code from another template file. You'll (possibly) understand in a
|
||||
> second.
|
||||
|
||||
## Subtemplates
|
||||
That's right, as if this wasn't complicated enough already, you can nest them.
|
||||
|
||||
> *"We were so preoccupied with asking if we could, that we didn't stop to think if we should."*
|
||||
|
||||
The reason for the existance of subtemplates is only hard, and not impossible to justify. Let me
|
||||
try.
|
||||
|
||||
Say you had an HTML file you want to generate. It contains some boring enclosing tags on the
|
||||
outside, and a huge chunk of text in the middle that also contained some template expressions, that
|
||||
you'd love to write in Markdown because HTML is just a pain to work with after all. The Markdown to
|
||||
HTML Lua function is also already written (or pulled in from an external Lua library because that
|
||||
does in fact work) and ready to rock, but if you just use that as a post-processor, all your
|
||||
surrounding HTML breaks. Whatever shall you do? Subtemplates to the rescue!
|
||||
With subtemplates, you can run a *region* of your template file through a separate post-processor,
|
||||
or even get the resulting, templated string value returned to your outer template's Lua code.
|
||||
Here's an example:
|
||||
```html
|
||||
<% oc %> -- my_page.html.cgt
|
||||
-- This is a comment in Confgen, by the way. You can probably guess how it works. <% cc %>
|
||||
<html>
|
||||
<head>
|
||||
...
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<% oc %> tmpl:pushSubtmpl(function(tmpl) tmpl:setPostProcessor(opt.myMarkdownRenderer) <% cc %>
|
||||
# Welcome to my awesome website!
|
||||
|
||||
May I introduce you to *Confgen*, this absolutely amazing template engine I learnt about recently!
|
||||
Also, two times two is <% oe %> 2 * 2 <% ce %>.
|
||||
<% oc %> end) <% cc %>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
"Alright, now he's gone completely nuts" I hear you say. First, you are probably completely
|
||||
corrent, and second, if you don't understand this on a syntactical level, please re-read the
|
||||
previous section (repeat this procedure until you have internalized how expression blocks and code
|
||||
blocks work :P).
|
||||
|
||||
A subtemplate is nothing but another instance of a template (`tmpl`) that is passed to the function
|
||||
we wrote here. This changes all the generated statements inside our function to refer to that new,
|
||||
inner template in place of the outer one. This inner template is then fully evaluated by Confgen,
|
||||
and its output is pushed to the outer template's output.
|
||||
|
||||
> **Note**: If you want the result of the inner template returned as a string rather than have it
|
||||
> pushed to the outer template, use `tmpl:subtmpl` instead of `tmpl:pushSubtmpl`.
|
||||
|
||||
## Library Templates
|
||||
|
||||
What if you wanted to share a snippet between templates, and didn't want to write it all in Lua?
|
||||
Well, Confgen's got you covered, as always! The `opt` (or `cg.opt` table as it's called in the
|
||||
Confgenfile) is always writable. You just shouldn't modify it in regular template file, as the order
|
||||
they're evaluated in is unspecified. However, you can use Confgen's powerful API to evaluate a
|
||||
template during the Confgenfile evaluation phase. Let's adjust our Confgenfile accordingly:
|
||||
```lua
|
||||
-- confgen.lua
|
||||
|
||||
cg.addPath(".config")
|
||||
|
||||
-- Evaluate the template `lib.cgt` and return it's output as a string,
|
||||
-- which we don't care about here.
|
||||
cg.doTemplateFile("lib.cgt")
|
||||
|
||||
cg.opt = {
|
||||
font = "MyFavoriteFont",
|
||||
font_size = "11",
|
||||
}
|
||||
```
|
||||
|
||||
Unlike the `cg.add*` family of functions, the `cg.doTemplate*` family immediately evaluates
|
||||
templates rather than adding them to the file list. They also return the resulting code, which is
|
||||
also handy. Let's get to the interesting file, though.
|
||||
|
||||
```
|
||||
<% oc %> -- lib.cgt <% cc %>
|
||||
|
||||
<% oc %> opt.greet = function(tmpl, name) <% cc %>
|
||||
Hello <% oe %> name <% ce %>!
|
||||
<% oc %> end <% cc %>
|
||||
```
|
||||
|
||||
Note the `tmpl` argument this function takes. This works similarly to subtemplates, except that we
|
||||
now append to another file's (the caller's) template rather than a subtemplate.
|
||||
We could now use this in another template:
|
||||
```
|
||||
<% oc %> -- message.txt.cgt <% cc %>
|
||||
|
||||
<% oc %> opt.greet(tmpl, "my Friend") <% cc %>
|
||||
```
|
||||
|
||||
> **Note**: We use a code block here rather than an expression block, because the `greet` function
|
||||
> pushes to our template, not returning any value.
|
||||
|
||||
Alright, I think that's probably enough insanity for one article, but there are more to come!
|
||||
This also isn't all that Confgen is packing, far from it. There are many API functions (such as
|
||||
`onDone` callbacks, file iterators and more), a whole assortment of handy CLI flags, and most
|
||||
importantly the previously hinted at **ConfgenFS** that didn't fit in here. Thank you for reading,
|
||||
and feel free to reach out if you have any questions, remarks or *concerns*.
|
||||
|
||||
## Found a bug?
|
||||
Sorry about that! Confgen is still a little rough around the edges sometimes. Please report it on the
|
||||
[issue tracker](https://git.mzte.de/LordMZTE/confgen/issues).
|
|
@ -51,3 +51,15 @@ code,
|
|||
color: #a6adc8;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0;
|
||||
padding-left: 12px;
|
||||
border-left: 8px solid;
|
||||
border-left-color: #b4befe;
|
||||
background-color: #b4befe30;
|
||||
}
|
||||
|
||||
blockquote p {
|
||||
padding: 4px;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue