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
				
			
		
							
								
								
									
										148
									
								
								users/sterni/nix/html/README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								users/sterni/nix/html/README.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,148 @@
 | 
				
			||||||
 | 
					# 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
 | 
				
			||||||
							
								
								
									
										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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										84
									
								
								users/sterni/nix/html/tests/default.nix
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								users/sterni/nix/html/tests/default.nix
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,84 @@
 | 
				
			||||||
 | 
					{ depot, pkgs, ... }:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let
 | 
				
			||||||
 | 
					  inherit (depot.users.sterni.nix.html)
 | 
				
			||||||
 | 
					    __findFile
 | 
				
			||||||
 | 
					    esc
 | 
				
			||||||
 | 
					    withDoctype
 | 
				
			||||||
 | 
					    ;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  exampleDocument = withDoctype (<html> { lang = "en"; } [
 | 
				
			||||||
 | 
					    (<head> {} [
 | 
				
			||||||
 | 
					      (<meta> { charset = "utf-8"; } null)
 | 
				
			||||||
 | 
					      (<title> {} "html.nix example document")
 | 
				
			||||||
 | 
					      (<link> {
 | 
				
			||||||
 | 
					        rel = "license";
 | 
				
			||||||
 | 
					        href = "https://code.tvl.fyi/about/LICENSE";
 | 
				
			||||||
 | 
					        type = "text/html";
 | 
				
			||||||
 | 
					      } null)
 | 
				
			||||||
 | 
					      (<style> {}  (esc ''
 | 
				
			||||||
 | 
					        hgroup h2 {
 | 
				
			||||||
 | 
					          font-weight: normal;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        dd {
 | 
				
			||||||
 | 
					          margin: 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ''))
 | 
				
			||||||
 | 
					    ])
 | 
				
			||||||
 | 
					    (<body> {} [
 | 
				
			||||||
 | 
					      (<main> {} [
 | 
				
			||||||
 | 
					        (<hgroup> {} [
 | 
				
			||||||
 | 
					          (<h1> {} (esc "html.nix"))
 | 
				
			||||||
 | 
					          (<h2> {} [
 | 
				
			||||||
 | 
					            (<em> {} "the")
 | 
				
			||||||
 | 
					            (esc " most cursed HTML DSL ever!")
 | 
				
			||||||
 | 
					          ])
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					        (<dl> {} [
 | 
				
			||||||
 | 
					          (<dt> {} [
 | 
				
			||||||
 | 
					            (esc "Q: Wait, it's all ")
 | 
				
			||||||
 | 
					            (<a> {
 | 
				
			||||||
 | 
					              href = "https://cl.tvl.fyi/q/hashtag:cursed";
 | 
				
			||||||
 | 
					            } (esc "cursed"))
 | 
				
			||||||
 | 
					            (esc " nix hacks?")
 | 
				
			||||||
 | 
					          ])
 | 
				
			||||||
 | 
					          (<dd> {} (esc "A: Always has been. 🔫"))
 | 
				
			||||||
 | 
					          (<dt> {} (esc "Q: Why does this work?"))
 | 
				
			||||||
 | 
					          (<dd> {} [
 | 
				
			||||||
 | 
					            (esc "Because nix ")
 | 
				
			||||||
 | 
					            (<a> {
 | 
				
			||||||
 | 
					              href = "https://github.com/NixOS/nix/blob/293220bed5a75efc963e33c183787e87e55e28d9/src/libexpr/parser.y#L410-L416";
 | 
				
			||||||
 | 
					            } (esc "translates "))
 | 
				
			||||||
 | 
					            (<a> {
 | 
				
			||||||
 | 
					              href = "https://github.com/NixOS/nix/blob/293220bed5a75efc963e33c183787e87e55e28d9/src/libexpr/lexer.l#L100";
 | 
				
			||||||
 | 
					            } (esc "SPATH tokens"))
 | 
				
			||||||
 | 
					            (esc " like ")
 | 
				
			||||||
 | 
					            (<code> {} (esc "<nixpkgs>"))
 | 
				
			||||||
 | 
					            (esc " into calls to ")
 | 
				
			||||||
 | 
					            (<code> {} (esc "__findFile"))
 | 
				
			||||||
 | 
					            (esc " in the ")
 | 
				
			||||||
 | 
					            (<em> {} (esc "current"))
 | 
				
			||||||
 | 
					            (esc " scope.")
 | 
				
			||||||
 | 
					          ])
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					      ])
 | 
				
			||||||
 | 
					    ])
 | 
				
			||||||
 | 
					  ]);
 | 
				
			||||||
 | 
					in
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pkgs.runCommandNoCC "html.nix.html" {
 | 
				
			||||||
 | 
					  passAsFile = [ "exampleDocument" ];
 | 
				
			||||||
 | 
					  inherit exampleDocument;
 | 
				
			||||||
 | 
					  nativeBuildInputs = [ pkgs.html5validator ];
 | 
				
			||||||
 | 
					} ''
 | 
				
			||||||
 | 
					  set -x
 | 
				
			||||||
 | 
					  test "${esc "<> && \" \'"}" = "<> && " '"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # slow as hell unfortunately
 | 
				
			||||||
 | 
					  html5validator "$exampleDocumentPath"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  mv "$exampleDocumentPath" "$out"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  set +x
 | 
				
			||||||
 | 
					''
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue