Template syntax

The template syntax got a bit complicated (at least it might seem so, at first sight)—it works for me though. I'm very open to suggestions about it.

The parser operates in two modes:

Switching between them happens when the parser encounters the special characters { and }. The parser is recursive—if, for example, it is in Lisp mode and the { is encountered, it does not terminate the current Lisp expression—instead it just reads a chunk of raw text that will be inserted in the current place of the lisp expression as a string. Therefore you can write for example:

;; produces “Hello World!” (and some whitespace)
{[define x "World"]}
{[define y {Hello {x}!}]}
{y}

Here is how the parser transforms the above code:

(strcat "
"
 (progn (define x "World") NIL) "
"
 (progn (define y (strcat "Hello " x "!")) NIL) "
"
 y)

The following notes should explain how it works:

  1. The parser starts in “raw text” mode.
  2. It reads text until it encounters the open bracket.
  3. At that point it starts reading one Lisp expression, after which it expects the closing bracket.
  4. If, while reading Lisp tokens, it encounters the open bracket, it switches to raw text mode and reads until the closing bracket is encountered.
  5. These parser modes are recursive—that is, if in raw text mode we encounter an open bracket, we enter Lisp mode to read one expression, then we return to the text mode; similarly, if in Lisp mode we encounter the open bracket we enter raw mode, and a strcat expression will be returned in the AST.
  6. A “raw text” could simply be a string, but more generally it'll contain multiple chunks (strings mixed with Lisp expressions), therefore the parser passes all those chunks to “strcat”.
  7. “strcat” joins all non-NIL arguments that it receives and returns a string. The arguments are forced to string, if needed, via Common Lisp's "~A" format argument (except lists, which are simply joined).
  8. Whitespace is kept faithfully; usually in HTML it won't be a problem, but there are various tricks you can use to control whitespace.
  9. By default the result of each Lisp expression is inserted into the output, unless it's NIL. Syntactic sugar is provided to call functions without including the result (using [square brackets] instead of regular parens).

Executing a template is a matter of executing this big “strcat” expression.

Let's see a more complicated example. Input template:

;; a semicolon in column zero is a comment, discarded from the output
<h1>{page.title}</h1>

;; show links
<ul class="links">
  {(foreach link '(("http://foo.com" . "The Foo")
                   ("http://bar.com" . "The Bar")
                   ("http://baz.com" . "The Baz"))
     (let ((url (car link))
           (name (cdr link)))
       {<li><a href="{\url}">{\name}</a></li>}))}
</ul>

Parser output:

(strcat "<h1>" (&dot-lookup page (quote title)) "</h1>

<ul class=\"links\">
  "
 (foreach link
  (quote
   (("http://foo.com" . "The Foo") ("http://bar.com" . "The Bar")
    ("http://baz.com" . "The Baz")))
  (let ((url (car link)) (name (cdr link)))
   (strcat "<li><a href=\"" (esc url) "\">" (esc name) "</a></li>")))
 "
</ul>
")

By default the result of Lisp expressions is inserted literally, rather than escaped. I've experimented a lot with both default ways (also with trying to make the parser read my mind, which I failed)—in short, my conclusion is that auto-escaping by default is more of an annoyance than a benefit. Note, therefore, that the last example includes a backslash before expressions that I wanted to escape, like {\name}—that's just syntactic sugar equivalent to {(esc name)}.

Fork me on Github