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>
		
	
			
		
			
				
	
	
		
			148 lines
		
	
	
	
		
			3.3 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			148 lines
		
	
	
	
		
			3.3 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # html.nix — _the_ most cursed Nix HTML DSL
 | |
| 
 | |
| A quick example to show you what it looks like:
 | |
| 
 | |
| ```nix
 | |
| # 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:
 | |
| 
 | |
| ```console
 | |
| $ $BROWSER $(nix-build example.nix)
 | |
| ```
 | |
| 
 | |
| Alternatively, in depot:
 | |
| 
 | |
| ```console
 | |
| $ $BROWSER $(nix-build -A users.sterni.nix.html.tests)
 | |
| ```
 | |
| 
 | |
| ## Creating tags
 | |
| 
 | |
| An empty tag is passed `null` as its content argument:
 | |
| 
 | |
| ```nix
 | |
| <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:
 | |
| 
 | |
| ```nix
 | |
| <div> { class = "foo"; } "<strong>hi</strong>"
 | |
| 
 | |
| # => "<div class=\"foo\"><strong>hi</strong></div>"
 | |
| ```
 | |
| 
 | |
| If it's not, be sure to escape it:
 | |
| 
 | |
| ```nix
 | |
| <p> {} (esc "A => B")
 | |
| 
 | |
| # => "<p>A => B</p>"
 | |
| ```
 | |
| 
 | |
| Nesting tags works of course:
 | |
| 
 | |
| ```nix
 | |
| <div> {} (<strong> {} (<em> {} "hi"))
 | |
| 
 | |
| # => "<div><strong><em>hi</em></strong></div>"
 | |
| ```
 | |
| 
 | |
| If the content of a tag is a list, it's concatenated:
 | |
| 
 | |
| ```nix
 | |
| <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][spath-parsing]
 | |
| 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.
 | |
| 
 | |
| [spath-parsing]: https://github.com/NixOS/nix/blob/293220bed5a75efc963e33c183787e87e55e28d9/src/libexpr/parser.y#L410-L416
 |