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.