tvl-depot/users/sterni/nix/html/default.nix
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

128 lines
3.4 KiB
Nix

# Copyright © 2021 sterni
# SPDX-License-Identifier: MIT
#
# This file provides a cursed HTML DSL for nix which works by overloading
# the NIX_PATH lookup operation via angle bracket operations, e. g. `<nixpkgs>`.
{ ... }:
let
/* Escape everything we have to escape in an HTML document if either
in a normal context or an attribute string (`<>&"'`).
A shorthand for this function called `esc` is also provided.
Type: string -> string
Example:
escapeMinimal "<hello>"
=> "&lt;hello&gt;"
*/
escapeMinimal = builtins.replaceStrings
[ "<" ">" "&" "\"" "'" ]
[ "&lt;" "&gt;" "&amp;" "&quot;" "&#039;" ];
/* Return a string with a correctly rendered tag of the given name,
with the given attributes which are automatically escaped.
If the content argument is `null`, the tag will have no children nor a
closing element. If the content argument is a string it is used as the
content as is (unescaped). If the content argument is a list, its
elements are concatenated (recursively if necessary).
`renderTag` is only an internal function which is reexposed as `__findFile`
to allow for much neater syntax than calling `renderTag` everywhere:
```nix
{ depot, ... }:
let
inherit (depot.users.sterni.nix.html) __findFile esc;
in
<html> {} [
(<head> {} (<title> {} (esc "hello world")))
(<body> {} [
(<h1> {} (esc "hello world"))
(<p> {} (esc "foo bar"))
])
]
```
As you can see, the need to call a function disappears, instead the
`NIX_PATH` lookup operation via `<foo>` is overloaded, so it becomes
`renderTag "foo"` automatically.
Since the content argument may contain the result of other `renderTag`
calls, we can't escape it automatically. Instead this must be done manually
using `esc`.
Type: string -> attrs<string> -> (list<string> | string | null) -> string
Example:
<link> {
rel = "stylesheet";
href = "/css/main.css";
type = "text/css";
} null
renderTag "link" {
rel = "stylesheet";
href = "/css/main.css";
type = "text/css";
} null
=> "<link href=\"/css/main.css\" rel=\"stylesheet\" type=\"text/css\"/>"
<p> {} [
"foo "
(<strong> {} "bar")
]
renderTag "p" {} "foo <strong>bar</strong>"
=> "<p>foo <strong>bar</strong></p>"
*/
renderTag = tag: attrs: content:
let
attrs' = builtins.concatStringsSep "" (
builtins.map
(n:
" ${escapeMinimal n}=\"${escapeMinimal (toString attrs.${n})}\""
)
(builtins.attrNames attrs)
);
content' =
if builtins.isList content
then builtins.concatStringsSep "" (flatten content)
else content;
in
if content == null
then "<${tag}${attrs'}/>"
else "<${tag}${attrs'}>${content'}</${tag}>";
/* Prepend "<!DOCTYPE html>" to a string.
Type: string -> string
Example:
withDoctype (<body> {} (esc "hello"))
=> "<!DOCTYPE html><body>hello</body>"
*/
withDoctype = doc: "<!DOCTYPE html>" + doc;
/* Taken from <nixpkgs/lib/lists.nix>. */
flatten = x:
if builtins.isList x
then builtins.concatMap (y: flatten y) x
else [ x ];
in
{
inherit escapeMinimal renderTag withDoctype;
__findFile = _: renderTag;
esc = escapeMinimal;
}