Syntactic Sugar
The syntax as described so far is relatively simple. The parser
accumulates text until one of the special tokens is encountered. By
default the start token is the bracket {. When a special
block is entered, the parser proceeds to read a s-expression, then it
returns back to plain text when the closing bracket } is
found. The s-expression might be a variable, or a list. While reading
it, the brackets are still special characters—when encountered, the parser
goes recursively into text mode and inserts at that point an s-expression
that will evaluate into that text. If the result of a s-expression is not
NIL
, it is coerced into a string and inserted into the
output.
That's pretty simple and probably would have been sufficient, but I couldn't resist adding some sugar. I'll describe it below.
Avoiding output
As mentioned, when the result of a Lisp expression is not NIL
it will be inserted into the output. Sometimes we need to avoid
this, such as when defining a function:
foo
{(defun sqr (x) (* x x))}
bar
The above does define the function but the output will look like this:
foo
#<FUNCTION (COMMON-LISP:LAMBDA (COMMON-LISP:&REST COMMON-LISP:VALUES)
:IN
SYTES.TEMPLATE::COMP-LAMBDA) {1006D07CBB}>
bar
This happens because (defun)
returns the newly created
function, which is then coerced into a string. One way around it would
be:
foo
{(progn
(defun sqr (x) (* x x))
nil)}
bar
|
→ |
foo
bar
|
Because it's awkward to type that manually, I thought about a different syntax:
foo
{[defun sqr (x) (* x x)]}
bar
|
→ |
foo
bar
|
By using a square bracket instead of a regular paren we tell the parser
that we don't want anything into the output at that point. It's
effectively equivalent to using (progn ... nil)
but easier
to type.
In Lisp mode, square brackets can be used to the same effect as regular
parens (that is, starting/ending a list); the only special place is when
they occur as first character of the expression, as above; they still
delimit a list, but additionally they give a hint to the parser to force
it return NIL
so that nothing goes into the output.
Escaping output
The value of a Lisp expression is inserted into the output literally, for example:
{[define start "<h1>"]}
{[define end "</h1>"]}
{start} Title {end}
|
→ |
<h1> Title </h1>
|
The value of start
and end
variables are
unescaped, therefore the output will be interpreted as HTML by the
browser, which might sometimes be exactly what you need. Other times you
might want to escape values and you can do that by prepending a backslash
to the expressions. Compare the above example and output to:
{[define start "<h1>"]}
{[define end "</h1>"]}
{\start} Title {\end}
|
→ |
<h1> Title </h1>
|
The backslash in this situation is syntactic sugar for i.e. {(esc start)}
.
Filters
Filters are a feature borrowed from Template::Toolkit. For example:
{ "foo bar" | upcase }
|
→ |
FOO BAR
|
The upcase filter is defined like this:
{[define-filter upcase (str) (string-upcase str)]}
Filters are just syntactic sugar for regular functions, but they do have a separate namespace, that is, if you have a variable named upcase it won't be clobbered by the filter definition above. That's convenient because you probably want to keep short names for filters.
Filters can be chained, that is, you can apply multiple filters to the same expression. Simply separate them with whitespace:
{ "foo bar" | upcase reverse }
|
→ |
RAB OOF
|
They are applied to the expression in sequence, in the order they've been passed; each filter operates on the result of the previous one. Note that no filters are defined by default in Sytes — the two above are defined by this website.
Filters can receive arguments; in this website for example there is a date filter that can format dates:
;; default output for a timestamp is RFC3339:
{(date/now)}
;; here we pass it through the date filter
;; with a custom format:
{(date/now) | (date "%mmm %d, %yyyy %h:%MM%tt")}
|
→ |
2024-12-14T19:08:26.875965+02:00
Dec 14, 2024 7:08pm
|
In fact the filter takes two arguments, but the first one is implicitly
passed (the date; (date/now)
returns the current date as a
local-time:timestamp
object. As you can see, when applying
the filter the syntax is to write it like a function call and pass only
the other arguments (in this case, the template format).
Too many parens?
Another syntax extension that I thought that can be done unambiguously was to not require parens to call a function. I said above that when the special token { is encountered, a single Lisp expression is read, but I lied. In fact, the parser will look at what's coming after the expression; if it's the pipe symbol | then it starts reading filters; if it's the end token } then it ends the expression. If none of the above, it continues reading a list. For example the following two are equivalent:
{(+ 1 2 3 4)}
{+ 1 2 3 4}
|
→ |
10
10
|
Filters work in this situation too:
{[define-filter sqr (x)
(* x x)]}
{ + 1 2 3 4 | sqr sqr }
|
→ |
10000
|
and so does the backslash:
{\ fmt "<h1>~A</h1>" {foo bar} | upcase reverse }
|
→ |
>1H/<RAB OOF>1H<
|
The order in which the above are applied is explained by the following equivalent expression:
{(esc (&apply-filter
(&apply-filter
(fmt "<h1>~A</h1>" "foo bar")
upcase)
reverse))}
|
→ |
>1H/<RAB OOF>1H<
|
So the backslash escaping applies to the result of the whole expression, after filters have been applied.
Other special characters
As noted before, the default special characters are the brackets. They trigger special behavior in the parser, which begs the question, how would you insert a literal bracket into the output when you need it? Well, prefix it with a backslash:
This is evaluated: {(+ 1 2)}
And this is not: \{(+ 1 2)\}
|
→ |
This is evaluated: 3
And this is not: {(+ 1 2)}
|
As you can see my syntax highlighting script colors backslashes like comments above—thought that would make sense, because the backslash itself won't be a part of the output, its only role being to tell the parser that the next bracket isn't special.
Other special characters are the semicolon (; — but only when it's the first character in a line) and the tilde (~ — but only when it's the last character in a line). A semicolon in the first column starts a comment, while a tilde in the last column suppresses the following newline. The backslash, again, can be used to escape them in the said situations.
;; This is a comment.
\;; But not this one.
Tilde ~
suppresses ~
the following ~
newline.
Unless \~
prefixed with \~
a backslash.
|
→ |
;; But not this one.
Tilde suppresses the following newline.
Unless ~
prefixed with ~
a backslash.
|
The backslash is only special in one of the situations above—therefore, when the parser encounters a backslash not followed by a special character, it's output literally. Also note that as I said, the semicolon and tilde are special only in certain positions, which should explain the following:
These will be output literally: \; \~.
|
→ |
These will be output literally: \; \~.
|
Changing the special characters
The brackets are convenient in most cases I had, but sometimes it's useful to be able to insert brackets unescaped, like for example when you display program samples. A special directive which is interpreted by the parser allows you to change the brackets to some other characters:
{.SYNTAX "“" "”"}
{no longer special}
But check this: “(+ 2 3 4)”
;; to change it again now I need to
;; use the new characters
“.SYNTAX "{" "}"”
Now this is special: {+ 2 3 4}
|
→ |
{no longer special}
But check this: 9
Now this is special: 9
|
Note that the .SYNTAX directive applies only to the current template.
Sytes
A Common Lisp library for building websites the easy way.