Couldn't sleep, so I made a surprisingly neat way to render HTML documents in Nix using our favorite feature __findFile: let inherit (depot.users.sterni.nix.html) __findFile esc; in <html> {} [ (<head> {} [ (<meta> { charset = "utf-8"; } null) (<title> {} (esc "hello")) ]) (<body> {} [ (<h1> {} (esc "hello world")) ]) ] => "<html><head><meta charset=\"utf-8\"/><title>hello</title></head><body><h1>hello world</h1></body></html>" Change-Id: Id36808a56ae3da3b5263c06f29342fc22d105c21 Reviewed-on: https://cl.tvl.fyi/c/depot/+/3410 Tested-by: BuildkiteCI Reviewed-by: tazjin <mail@tazj.in>
3.3 KiB
html.nix — the most cursed Nix HTML DSL
A quick example to show you what it looks like:
# Note: this example is for standalone usage out of depot
{ pkgs ? import <nixpkgs> {} }:
let
# zero dependency, one file implementation
htmlNix = import ./path/to/html.nix { };
# make the magic work
inherit (htmlNix) __findFile esc withDoctype;
in
pkgs.writeText "example.html" (withDoctype (<html> {} [
(<head> {} [
(<meta> { charset = "utf-8"; } null)
(<title> {} (esc "hello world"))
])
(<body> {} [
(<h1> {} (esc "hello world"))
(<p> { class = "intro"; } (esc ''
welcome to the land of sillyness!
''))
(<ul> {} [
(<li> {} [
(esc "check out ")
(<a> { href = "https://code.tvl.fyi"; } "depot")
])
(<li> {} [
(esc "find ")
(<a> { href = "https://cl.tvl.fyi/q/hashtag:cursed"; } "cursed things")
])
])
])
]))
Convince yourself it works:
$ $BROWSER $(nix-build example.nix)
Alternatively, in depot:
$ $BROWSER $(nix-build -A users.sterni.nix.html.tests)
Creating tags
An empty tag is passed null
as its content argument:
<link> {
rel = "stylesheet";
href = "/main.css";
type = "text/css";
} null
# => "<link href=\"/main.css\" rel=\"stylesheet\" type=\"text/css\"/>"
Content is expected to be HTML:
<div> { class = "foo"; } "<strong>hi</strong>"
# => "<div class=\"foo\"><strong>hi</strong></div>"
If it's not, be sure to escape it:
<p> {} (esc "A => B")
# => "<p>A => B</p>"
Nesting tags works of course:
<div> {} (<strong> {} (<em> {} "hi"))
# => "<div><strong><em>hi</em></strong></div>"
If the content of a tag is a list, it's concatenated:
<h1> {} [
(esc "The ")
(<strong> {} "Nix")
(esc " ")
(<em> {} "Expression")
(esc " Language")
]
# => "<h1>The <strong>Nix</strong> <em>Expression</em> Language</h1>"
More detailed documentation can be found in nixdoc
-compatible
comments in the source file (default.nix
in this directory).
How does this work?
Theoretically expressions like <nixpkgs>
are just ordinary paths —
their actual value is determined from NIX_PATH
. html.nix
works
because of how this is actually implemented: At parse time
Nix transparently translates an expression like <foo>
into
__findFile __nixPath "foo"
:
nix-repl> <nixpkgs>
/nix/var/nix/profiles/per-user/root/channels/vuizvui/nixpkgs
nix-repl> __findFile __nixPath "nixpkgs"
/nix/var/nix/profiles/per-user/root/channels/vuizvui/nixpkgs
This translation doesn't take any scoping issues into account --
so we can just shadow __findFile
and make it return anything,
even a function:
nix-repl> __findFile = nixPath: str:
/**/ if str == "double" then x: x * 2
else if str == "triple" then x: x * 3
else throw "what?"
nix-repl> <double> 2
4
nix-repl> <triple> 3
9
nix-repl> <quadruple> 4
error: what?
Exactly this is what we are doing in html.nix
:
Using let inherit (htmlNix) __findFile; in
we shadow the builtin __findFile
with a function which returns a function rendering a particular HTML tag.