3bd43e39dd
Currently nix/html requires that the content of an element is either an HTML string (which may or may not be generated by the library) or a flat list of HTML strings (which may or may not be generated by the library). I've found that this requirement makes authoring more complex pages that have programmatically generated parts cumbersome since one needs to take care that returned lists are appended, not included as an element. This leads to confusing code and annoying errors. We don't really care about the nesting of a content list as long as the order is clear, so we can just flatten the list making life a little easier: (<main> { } [ (<section> { } (<h2> { } "static section")) listOfGeneratedSections (<section> { } (<h2> { } "another section")) ]) Change-Id: I06016a8eff01d34d7eaea7798a00ed191115f9c8 Reviewed-on: https://cl.tvl.fyi/c/depot/+/12908 Reviewed-by: sterni <sternenseemann@systemli.org> Tested-by: BuildkiteCI Autosubmit: sterni <sternenseemann@systemli.org> |
||
---|---|---|
.. | ||
tests | ||
default.nix | ||
README.md |
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.