Pitch: Leaf View Extensions

Pitch: Leaf View Extensions

Leaf is a templating language built for Swift. This pitch proposes re-introducing syntax for declaring view extensions, a common template-language pattern for building complex, hierarchical views in a modular way.

Support for this syntax was available in previous versions of Leaf, but was not carried over when Leaf was re-written for 3.0. See vapor/leaf#131 for more discussion.

Current Solution

Leaf 3.0 recommends using a combination of #get, #set, and #embed for generating complex view hierarchies.

  • #get: prints a value from the context without escaping.
  • #set: puts a value in the context without escaping.
  • #embed: serializes the referenced view, passing current context.

The example below demonstrates common usage:

base.leaf

<html>
    <head>
        <title>#(title)</title>
    </head>
    <body>#get(body)</body>
</html>

welcome.leaf

#set("title", "Welcome")
#set("body") { Hello, #(name)! }
#embed("base")

get/set/embed Caveats

One of the main caveats of the current syntax is that it relies heavily on the shared context for constructing the template's abstract syntax tree (AST). The context, which contains data to serialize into the template at runtime, is a black box to Leaf's parser and serializer. Neither the parser nor serializer can make optimizations to the cached, in-memory representation of hierarchical templates using this syntax.

To better understand this issue, let's take a look at the AST generated for the previous example:

[
    set("title", [string("Welcome")]),
    set("body", [string("Hello, "), get(name), string("!")]),
    embed("base")
]

Because the template's hierarchy relies on context, much of the serialization work must happen at runtime (when the template is serialized given a concrete context). This is not ideal, since the more work Leaf must do at runtime, the slower it will be to fulfill requests under high load.

Extend Tag

This pitch introduces three tags: #import, #export, and #extend for creating template hiearachies in a more well-defined and optmiziable way.

  • #import: Includes an exported section from a template extending it.
  • #export: Exports a section to the template being extended.
  • #extend: Indicates the template is being extended.

To better understand this new syntax, let's take a look at how the previous example using get/set/embed could be updated:

base.leaf

<html>
    <head>
        <title>#import("title")</title>
    </head>
    <body>#import("body")</body>
</html>

welcome.leaf

#extend("base") {
	#export("title", "Welcome")
	#export("body") { Hello, #(name)! }
}

Because this new syntax does not rely on the context, the Leaf renderer can generate and cache a more complete AST:

[
    string("<html><head><title>"),
    string("Welcome"),
    string("</title></head><body>"),
    string("Hello, "), 
    get(name),
    string("!"),
    string("</body></html>")
]

Which can be further simplified to:

[
    string("<html><head><title>Welcome</title></head><body>Hello, "), 
    get(name), 
    string("!</body></html>")
]

This AST would be much faster to serialize than the AST generated using get/set/embed syntax. This syntax has the added benefit of being able to emit warnings or errors for extraneous #import or #export statements.

Nesting Extend

#export tags can only exist within the body of an #extend tag. This design allows for more than one #extend tag to appear in a given template. It also allows for them to be nested (an #extend to appear in an #export).

This could be a useful pattern for building re-usable components in Leaf:

base.leaf

<html>
    <head>
        <title>#import("title")</title>
    </head>
    <body>#import("body")</body>
</html>

alert.leaf

<alert style=#import("class")>
	<p>#import("message")</p>
</alert>

welcome.leaf

#extend("base") {
	#export("title", "Welcome")
	#export("body") { 
		#extend("alert") {
			#export("class", "warning")
			#export("message", alert.message)
		}
		Hello, #(name)! 
	}
}

Even this complex hierarchy could resolve to a relatively simple AST with great performance characteristics:

[
    string("<html><body><title>Welcome</title></head><body><alert style=warning><p>")
    get(alert.message),
    string("</p></alert>Hello, "), 
    get(name), 
    string("!</body></html>")
]

Deprecating Set

While #get and #embed could continue to be useful even with these new tags, there isn't really a reason to continue supporting #set beyond backward compatibility.

It may be worth considering deprecating #set to push users toward adopting the new, more performant tags.

Alternatives Considered

Deprecating Embed

Unlike #extend, #embed is able to import templates dynamically. In other words, you can pass a variable name to extend which will resolve to a file name at runtime.

// will always embed base file
#embed("base")

// will resolve file to embed at runtime
#embed(variable)

I'm interested to know whether anyone finds that functionality useful. If not, it might be better to update #embed with the newly described #import / #export syntax instead of offering two similar and possibly confusing methods for embedding Leaf templates.

Optimizing get/set/embed

Given certain limitations are met, get/set/embed could be optimized internally by the Leaf parser and serializer. Although it would be nice to not require any syntax changes, I think the implementation for this would have to be quite hacky and difficult to maintain.

1 Like