Pitch: New Leaf Body Syntax
Leaf is a templating language built for Swift. This pitch proposes introducing a new syntax for declaring tag bodies that would simplify the parser and make using Leaf easier.
Current Syntax
Leaf has supported the following syntax for declaring tag bodies since version 1.0.
<h1>Users</html
<ul>
#for(user in users) {
<li>#(user.name)</li>
}
</ul>
This syntax, which we'll refer to as curly-brace bodies, was inspired by Swift's closure syntax. Much of Leaf's original design was copied directly from Swift in an effort to make the new templating language easy to learn.
While this syntax is easy to learn and great for example code, it suffers from some issues in real world use cases--especially when Leaf is used alongside JavaScript.
Problems with Curly-Brace Bodies
The main problem with curly-brace bodies is the prevelance of }
in normal template code. Because }
denotes the end of a body in this syntax, any appearances of }
in the template's text must be escaped.
let xs = [-5, 3, 42, -3.14]
#if(count(xs) != 0) {
|x| = { x, if x >= 0; -x, otherwise; }
Solve equations:
#for(x in xs) {
|#(x)| = ?
}
}
In the above example, Leaf would (perhaps unexpectedly) assume the }
in the definition of |x|
signals the end of the #if
tag's body.
The solution to this problem is to escape any }
that do not actually intend to close a tag body using \
.
#if(count(xs) != 0) {
|x| = { x, if x >= 0; -x, otherwise; \}
Solve equations:
#for(x in xs) {
|#(x)| = ?
}
}
While escaping is fairly trivial for this example, one can imagine that it might get out of hand when dealing with large amounts of embedded JavaScript.
Proposed Solution: Colon Syntax
This pitch proposes introducing the following syntax to make parsing tag bodies more predictable and resilient:
- Tags intending to include a body are immediately trailed by a
:
-
#:<tagname>
token is used to close a body.
Take a look at the following example:
#if(count(xs) != 0):
|x| = { x, if x >= 0; -x, otherwise; }
Solve equations:
#for(x in xs):
|#(x)| = ?
#:for
#:if
This new syntax has a number of key benefits over the existing syntax.
- Tags that do not declare a body are easier to parse (no need to check for unescaped
{
) - Body closing syntax is less likely to appear in template text (
#:for
is less common than}
) - Body closing tag matches opening tag (
#:for
ends#for
)- This makes it easier for humans to read templates
- It also allows the Leaf compiler to better catch copy/paste errors
Edge Cases
Escaping Trailing Colon
It's likely that a colon could appear directly after a tag in cases when the developer is not declaring a body.
#for(user in users):
- #(user.name): #(user.email)
#:for
In these cases, the trailing colon can be escaped or added via string concatenation.
#(user.name)\: #(user.email)
#(user.name + ":") #(user.email)
#(user.name)#(":") #(user.email)
Escaping Closing Tag
Although unlikely, it's possible that a closing body tag such as #:if
could appear in the template text. In this case, the closing tag could be escaped like any normal tag using \
before #
.
#if(showLink):
The link is: http://foo.co/index.html\#:ifasdf
#else:
No link.
#:if
Single Line Bodies
One of the nice advantages of the curly-brace body syntax is that declaring single line bodies is concise. Take the following example of setting a page title.
#set("title") { Home }
This is considerably less concise with the proposed colon syntax.
#set("title"): Home #:set
One solution that is already available for some Leaf tags is passing a string as the second parameter. This could be expanded to all tags that accept bodies as a general pattern in Leaf.
#set("title", "Home")
This has the added advantage of not introducing extraneous white space around the content.
Deprecated Support for Curly-Brace Syntax
It should be possible to continue supporting the existing curly-brace syntax for declaring tag bodies alongside the new colon syntax. This usage would be considered deprecated and would emit a warning in debug build modes. Support for the syntax would likely be removed in the next version of Leaf.
It's not possible to 100% guarantee support for both syntaxes will be reasonable to implement in the parser, but if it is, it would likely be greatly appreciated by developers with large, existing codebases using Leaf.
Alternatives Considered
End-Syntax
If Leaf had a way to statically determine whether or not a given tag usage required a body, it could be possible to omit requirement of the trailing :
. This could allow for the following syntax:
#for(user in users)
Hello #(user.name)!
#endfor
This syntax does not suffer from the Escaping Trailing Colon issue. However, statically determining whether a tag requires a body could prove difficult to implement and likely restrict custom Leaf tags.
One difficult case is the #set
tag.
#set("title", "Home")
#set("content")
<sidebar>...</sidebar>
#endset
This use-case needs support for dynamically requiring a tag body based on the number of arguments supplied.
Here's an extend example using #if
#if(a)
<p>#(a)</p>
#elseif(b)
<h1>#(b)</h1>
#else
<strong>Nothing</strong>
#endif
Curly-Brace Counting
vapor/leaf#119 proposed counting opening curly braces to more intelligently detect closing braces. While this works well for detecting curly-braces in typical JavaScript usage, it fails to account for some seemingly feasible cases, such as using this smiley face.
#for(user in users) {
Hello #(user.name)! :}
}