tvl-depot/users/sterni/nix/html
sterni 3bd43e39dd feat(sterni/nix/html): flatten lists enclosed by an element
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>
2024-12-25 00:36:36 +00:00
..
tests feat(sterni/nix/html): flatten lists enclosed by an element 2024-12-25 00:36:36 +00:00
default.nix feat(sterni/nix/html): flatten lists enclosed by an element 2024-12-25 00:36:36 +00:00
README.md feat(users/sterni/nix): cursed nix html DSL 2021-08-26 15:34:58 +00:00

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 =&gt; 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.