feat(users/sterni/nix): cursed nix html DSL
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>
This commit is contained in:
parent
17d78867bb
commit
9ed439bfbd
3 changed files with 351 additions and 0 deletions
119
users/sterni/nix/html/default.nix
Normal file
119
users/sterni/nix/html/default.nix
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# 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>"
|
||||
=> "<hello>"
|
||||
*/
|
||||
escapeMinimal = builtins.replaceStrings
|
||||
[ "<" ">" "&" "\"" "'" ]
|
||||
[ "<" ">" "&" """ "'" ];
|
||||
|
||||
/* 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.
|
||||
|
||||
`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 "" 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;
|
||||
|
||||
in {
|
||||
inherit escapeMinimal renderTag withDoctype;
|
||||
|
||||
__findFile = _: renderTag;
|
||||
esc = escapeMinimal;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue