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) {1008BBEDBB}>
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}


&lt;h1&gt; Title &lt;/h1&gt;

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-11-21T11:25:29.237772+02:00
Nov 21, 2024 11:25am

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 }
&gt;1H/&lt;RAB OOF&gt;1H&lt;

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))}
&gt;1H/&lt;RAB OOF&gt;1H&lt;

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.

Fork me on Github