vapor / leaf-kit Goto Github PK
View Code? Open in Web Editor NEW🍃 An expressive, performant, and extensible templating language built for Swift.
Home Page: https://docs.vapor.codes/4.0/leaf/getting-started
License: MIT License
🍃 An expressive, performant, and extensible templating language built for Swift.
Home Page: https://docs.vapor.codes/4.0/leaf/getting-started
License: MIT License
Vapor cannot render a webpage templated in leaf, if AppCode from JetBrains is used. JetBrains by default use a .build directory in project root directory to store dependencies and build files. Because of that, LeafKit returns error:
[ ERROR ] LeafError(file: "<absolute path to my project root>/.build/checkouts/leaf-kit/Sources/LeafKit/LeafSource/NIOLeafFiles.swift", function: "file(template:escape:on:)", line: 89, column: 57, reason: LeafKit.LeafError.Reason.illegalAccess("Attempted to access .build")) [request-id: 6468838E-F42F-4252-8AC7-04BEBE2485AF]
Steps to reproduce the behavior:
Webpage render correctly.
With LeafKit 1.0.0-alpha.1,
It seems this
#if(test1):
Val1
#elseif(test2):
Val2
#elseif(test3):
Val3
#else:
Val4
#endif
will output
Val1
Val3
Val4
If test1
and test3
are true
and test2
is false
.
The elseif
does not seem to care about the previous tests in the if.
Allowing some control over LeafCache beyond isEnabled
would be great for certain use cases where templates themselves are dynamically controlled in use (in my case I want to be able to generate additional, user defined templates live in production, for example with email templates), but as it stands there's no way to clear cached, resolved documents while the app is running.
My naive solution is just to add a purge function to LeafCache protocol - in the current implementation, this is pretty basic but gets 95% of what's necessary done.
@discardableResult func purge(
_ name: String,
on loop: EventLoop
) -> EventLoopFuture<Bool?>
It returns an optional Bool meaning:
Since extended templates resolve inline when they're processed, this does mean that cached templates dependent on purged documents would still continue to use the previously loaded template unless they themselves are purged, which.... is of dubious desirability, but any more complex behavior would require maintaining a dependency graph alongside, and I realize that may be outside the scope of what you'd like LeafKit to handle.
When a #with
wraps an #extend
, the #extend
doesn’t seem to render. (I mentioned this issue when I reported #126 but they appear to be separate issues.)
Here’s a test that shows the issue and currently fails:
func testWithWrappingExtend() throws {
let header = """
<h1>#(child)</h1>
"""
let base = """
<body>#with(parent):<main>#extend("header")</main>#endwith</body>
"""
let expected = """
<body><main><h1>Elizabeth</h1></main></body>
"""
let headerAST = try LeafAST(name: "header", ast: parse(header))
let baseAST = try LeafAST(name: "base", ast: parse(base))
let baseResolved = LeafAST(from: baseAST, referencing: ["header": headerAST])
var serializer = LeafSerializer(
ast: baseResolved.ast,
ignoreUnfoundImports: false
)
let view = try serializer.serialize(context: ["parent": ["child": "Elizabeth"]])
let str = view.getString(at: view.readerIndex, length: view.readableBytes) ?? ""
XCTAssertEqual(str, expected)
}
This test fails with:
XCTAssertEqual failed: ("<body><main></body>") is not equal to ("<body><main><h1>Elizabeth</h1></main></body>")
The #with
call should provide its parameter as context to the #extend
.
I think I see what the problem is. I’ll submit a PR shortly.
A set of conditional operands as for example if (a || b)
seems to evaluate all its operands (a
and b
) upfront.
This means that b
will be evaluated even if a
was already true.
Steps to reproduce the behavior:
page.leaf
) with the following contents:<!DOCTYPE html>
<html>
<body>
#if(!ctx.children || count(ctx.children) <= 0):
<ul>#for(child in ctx.children):<li>#(child)</li>#endfor</ul>
#else:
<p>No children.</p>
#endif
</body>
</html>
struct Context: Encodable { let ctx: [String]? }
func configure(app: Application) {
app.get(use: { $0.view.render("page", Context(ctx: nil)) })
}
Count
tag if the parameter is no collection. In this case, a breakpoint in Count
prints the following for the first parameter (given that ctx.children
is nil
):(lldb) po ctx.parameters
▿ 1 element
▿ 0 : void()?
▿ storage : void()?
▿ optional : 2 elements
- .0 : nil
- .1 : LeafKit.LeafData.NaturalType.void
The page renders fine, showing "No children." with the following rendered HTML:
<!DOCTYPE html>
<html>
<body>
<p>No children.</p>
</body>
</html>
I tried to dig a bit into LeafKit
's source, and am pretty sure that the evaluation of all conditional operands is the problem. However, I could also be missing something, in which case my "attempted diagnosis" might be incorrect and the issue is actually something different.
Original comment syntax of:
#("foo")
#// this is a comment!
bar
and multi-line of:
#("foo")
#/*
this is a comment!
*/
bar
not currently supported, and conflicts with our new preference of :
to open a body and subsequent #end<tag>
syntax.
I propose moving to:
#com:
this is a comment here :)
#endcom
When trying to use #count
tag in an #if
condition else
branch always gets executed whether the condition is satisfied or not. Also, when trying to display number of elements somewhere on the page, nothing renders. For example <p>Total number of articles is #count(articles)</p>
renders Total number of articles is
.
I'm finding that the default output of a readable Leaf template includes quite a lot of unnecessary whitespace. To combat this, I'm reducing the "real" whitespace in my Leaf templates, but it's making them unreadable.
It would be great if Leaf could be told to ignore whitespace in certain contexts, or better yet, if Leaf supported minification/prettification of it's output.
As I dig further in to LeafKit, I'd like to summarize some general thoughts I've had toward attempting to future-proof LeafKit's syntax and structural plannings in order make it much more capable, usable, and extensible. I've reviewed the various open issues and contemplated for myself what I'd like to see it be capable of that I think it isn't now, and thus - my assumptions and conclusions are below, and I welcome any feedback or thoughts.
With Vapor 4 getting close to release, and the already-significant changes to syntax from Leaf 3 in the current builds being breaking anyway for anyone moving from that version, I'd like to take the opportunity to immediately lock down general syntax rules for Leaf going forward, and I believe this provides a good basis for that - while I'm suggesting a few syntactical changes that I don't personally think are very different from the current state, I think they very clearly mark a safe boundary for expected behaviors.
As I understand it, anyway
At its core, a mixed-language file like a Leaf template faces some difficulty in parsing since you're interpolating the templating language with presentation language, so Leaf syntax must be:
A core concept I believe that needs to be emphasized is that LeafKit, while innately needing a language representation, is not PER SE a language in the sense of doing anything meaningful on its own; rather, it serves as a stored representation of a pipeline. Intermingled processing and data languages in one file on its face is reminiscent of things like PHP, but Leaf, though having a fully defined syntax, should always attempt to be nothing more than a black box that bridges data in from Swift code to an output format. While this might seem self obvious, this informs some conclusions I've come to as outlined below.
All Leaf directives should be considered to be, essentially, a contained block and their Syntax definition would enforce (and report) expectations in terms of what capabilities they have. All statements would, in essence, be either an anonymous function or a named function, so parenthesis would be non-optional. Additionally, Body-bearing tags would have a trailing # after the end tag.
Description | Current | Suggested |
---|---|---|
Context (Data In) | #(key) | No change |
Tag with no Parameters | #now | #now() |
Tag with "Body" (as Parameter) | #export("key", "value") | No Change |
Tag with "Body" (as Parameter)* | #export("key", "value"): | #export("key","value") |
Tag with "Body" (as Body) | #for(i in a):#(i)#endfor | #for(i in a):#(i)#endfor# |
(* See Character Escaping)
TagIdentifier :== #
TagName :== TagIdentifier
[a-zA-z_]*
(opt)
EndTagName :== TagIdentifier
end
TagName
TagIdentifier
Parameters :== (
Expression
* )
BodyIdentifier :== :
IdentityTag :== TagName
Paramaters
BodyTag :== TagName
Paramaters
BodyIdentifier
TagStatement
|RawStatement
* EndTagName
TagStatement :== IdentityTag
| BodyTag
(* where an optional tag name on an IdentityTag represents an identity evaluation of the expression)
While this is EVER so slightly more verbose in requiring parans, it removes nearly any ambiguity with any languages I can readily think of, and IMO makes things more blatantly obvious in representing ALL actions Leaf takes as a function - either named or unnamed. While a trailing hashtag on EndTagName is not strictly necessary, I believe it's visually clarifying and semantically meaningful when considering a body-bearing tag as a block, matching the initial hashtag. This reads as an identity tag taking the hashtag as a prefix unary - # TAG - and a body block as # TAG BODY ENDTAG #
Ideally, escaping anything would never need to occur to presentation layer code, as templating should be atomically separable in the file. Currently, changes to the syntax from Leaf 3 to #tag(parameter):Raw Body#endTag
completely removes bracket collision chances with CSS/JS, but introduces problems when anchor tags are used in HTML or colons immediately follow a tag.
SOLUTIONS
#index:
where an atomic value can inherently have no body, but the parser will consume it anyway. This is a bug but easiest to solve by making Syntax objects more self-reflective.No way of commenting the Leaf file is feasible currently.
SOLUTION
#(# THIS IS A LEAF COMMENT #)
Inability to easily (or at all) scope data access. This prevents things like dictionary access by key, avoiding collisions or ambiguity between context data keys vs tags, and the like. It also increases confusion where values are not consistently addressed - #(key)
and #export("key")
both are referring to a key mapping to a value, but one requires string literals to name them.
#(context.key)
vs #(exports.key)
.#export
can use key names without having to use string literals.Examples:
Scoping | Meaning |
---|---|
#(key) |
Default scoping to context data |
#(dict[key]) |
Context scoping dictionary accessors within loops |
#(key.uuid) |
Context/Model scoped property key accessors |
#( $exports[key] = value ) |
Export scoped values being set |
#( $exports[key] ) |
Export scoped values being read |
#( $request[client_ip] ) |
Request userInfo scoped values |
#( $static[server_name] ) |
Runtime scoped static values |
(* No fixed thoughts on prefixing or specifics for scoping rules as of yet but alignment towards Swift norms is obviously preferable)
Personal opinion - I find extend
, export
, and import
of unclear definition, as they are contrary to the actual way in which Leaf handles non-flat syntaxes. Extend implies that the file in which the statement is made is extending the referenced file, and export/import, while serving a sort of limited scoping between the two, are also not entirely clear - the 1:1 of export/import makes it seem like there's protection in only mapping between the two, but this isn't the case as a template 2 layers removed would still have access to the export (as it will have already been inlined to the first referenced extension. Extend makes more sense when you think of the referenced file as being placed into the current one. Thus, I think it should be broken out into two separate and clearer meanings.
SOLUTION
Current Tag | Proposed | Function |
---|---|---|
extend |
inline |
Actively inlines the syntax of the referenced file natively to the current template when parsed |
extend |
embed |
Passively embeds the referenced file as an isolated, unparsed raw object |
export |
removed | Would be replaced by natural value setting to the specified "export" scope |
import |
removed | Would be replaced by accessing the specified "export" scope |
inline
(extend) would additionally no longer accept a body for exports. Instead, it would take only an optional passed parameter to a specific scope if desirable for security reasons - presumably a global export scope could be provided. In cases where multiple external files all might need the same exports, this would considerably cut down on repetition and allow consolidating whatever limited actual processing the template needs to do into one area of the file.
Currently, and understandably, LeafKit is almost entirely focused on HTML/CSS/JS processing. Understanding that it's been somewhat refocused onto that from TemplateKit perhaps trying to be too general purpose, I'd suggest that even so, LeafKit is very capable of handling other domains as well. Looking forward, I think a very broad capability is possible by simply providing appropriately designed DSL packages as packages of tag extensions, and configuring LeafKit to declaratively hint at complied presentation file types; ex:
LeafRenderer.render("template", as: .html)
to enable an HTML DSLLeafRenderer.render("template", as: .pdf)
to enable a PDF DSLLeafRenderer.render("template", as: .nyancat)
to enable a prank DSLWhere said DSLs would provide the requisite Tag definitions to map data appropriately. Ex, an html DSL would add the appropriate tags for any html elements as direct mappings to tags:
#_img(src: item.url, aria-label: item.name)
or the like
And could additionally provide pre/post-processing handlers, so that things like minification/whitespace stripping could be done to the parsed ASTs ahead of time. With an appropriate symbol tree assembled during the lex/parse of the AST, this may potentially provide increased performance benefits, especially when conditional blocks are needed routinely for frequently alternating state (as one would have for html tags which might have 10+ attributes that need to be inserted - 1 Syntax element instead of 21.)
As a further step, the aforementioned inline
/embed
functions would equally allow hinting so that an initial template run with the HTML DSL could then inline a templated run with the JS DSL.
Additionally this would allow directive control over input/output file encoding. While the initial referenced Leaf template would need to be in UTF8, in the example of generating PDFs, such a DSL could automatically specify that the output stream serialize to ASCII7, and likewise embed
ed references to JPEG files or fonts could be correctly read as raw bytes rather than running through Strings.
If I use a subclass ContextGameWithPlayer from ContextGame for context rendering the base properties from ContextGame are not available.
Example:
class ContextGame: Codable {
enum CodingKeys: CodingKey {
case game, status, players
}
let game: GameModel
let status: String
let players: [Player]
init(game: GameModel, status: String, players: [Player]) {
self.game = game
self.status = status
self.players = players
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
game = try container.decode(GameModel.self, forKey: .game)
status = try container.decode(String.self, forKey: .status)
players = try container.decode([Player].self, forKey: .players)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(game, forKey: .game)
try container.encode(status, forKey: .status)
try container.encode(players, forKey: .players)
}
}
class ContextGameWithPlayer: ContextGame {
let player: Player
enum CodingKeys: CodingKey {
case player
}
init(game: GameModel, status: String, players: [Player], player: Player) {
self.player = player
super.init(game: game, status: status, players: players)
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
player = try container.decode(Player.self, forKey: .player)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(player, forKey: .player)
try super.encode(to: encoder)
}
}
Error:
{"error":true,"reason":"LeafRenderer.swift.syncSerialize(::):370\n[player] variable(s) missing"}
Same content (not subclassing) is working:
class ContextGameWithPlayer: Codable {
let player: Player
let game: GameModel
let status: String
let players: [Player]
enum CodingKeys: CodingKey {
case player, game, status, players
}
init(game: GameModel, status: String, players: [Player], player: Player) {
self.player = player
self.game = game
self.status = status
self.players = players
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
player = try values.decode(Player.self, forKey: .player)
game = try values.decode(GameModel.self, forKey: .game)
status = try values.decode(String.self, forKey: .status)
players = try values.decode([Player].self, forKey: .players)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(player, forKey: .player)
try container.encode(game, forKey: .game)
try container.encode(status, forKey: .status)
try container.encode(players, forKey: .players)
}
}
Quoting Leaf documentation about #if
:
if you provide a variable it will check that variable exists in its context
The latest Leaf engine doesn't follow this convention: it only evaluates Bool true
as true, all other values including nil
and strings evaluated as false
.
In other words, no matter what you provide it'll always be treated as false
unless Bool true
provided.
Small experiment:
<p>Title = #(title)<br>
#if(title):
The 'title' is #(title)
#else:
No 'title' was provided.
#endif
</p>
<p>Nonexistent = #(nonexistent)<br>
#if(nonexistent):
The 'nonexistent' is #(nonexistent)
#else:
No 'nonexistent' was provided.
#endif
</p>
Results in:
Title = Home
No 'title' was provided.
Nonexistent =
No 'nonexistent' was provided.
Whilst creating a custom UnsafeUnescapedLeafTag
, context.requireNoBody()
always throws even if no body is provided, while context.requireBody()
never throws even when no body is provided, rather returning an empty [Syntax]
.
While looking through the tests I noticed these methods are not tested anywhere and I think simply checking for the array being empty in addition to checking for it being != nil
could solve the issue. The other option would be searching where the body
of the tag is passed in and setting it to nil
if it's an empty array
struct BodyRequiringTag: UnsafeUnescapedLeafTag {
func render(_ ctx: LeafContext) throws -> LeafData {
_ = try ctx.requireBody()
return .string("Hello there")
}
}
and
struct NoBodyRequiringTag: UnsafeUnescapedLeafTag {
func render(_ ctx: LeafContext) throws -> LeafData {
try ctx.requireNoBody()
return .string("General Kenobi")
}
}
app.leaf.tags["bodytag"] = BodyRequiringTag()
app.leaf.tags["nobodytag"] = NoBodyRequiringTag()
#bodytag:#endbodytag
or
#bodytag()
This returns Hello there
, which is just the return type of the tag, while it should be returning {error: true, reason: "Missing body"
#nobodytag
This returns {error: true, reason: "Extraneous body"}
The only way to make this error go away is to set the template as
#nobodytag:#endnobodytag
Which is, I think, not that intuitive
4.76.0
18.6.0
MacOS Ventura 13.0
14.1 (14B47b)
Currently view templating is done by interacting with the context and embedding base views. In Leaf, this usually looks something like:
base.leaf
<title>#get(title)</title>
#get(body)
home.leaf
#set("title") { Welcome }
#set("body") { Hello, #(name)! }
#embed("base")
While this system is really flexible, that flexibility comes at the cost of some performance. Because the #get
tags can be used for importing anything from static code to dynamic variables, TemplateKit can't do anything to optimize or "pre-compile" the views.
To elaborate this, let's look at the cached AST for home.leaf
:
[
set("title", [raw("Welcome")]),
set("body", [raw("Hello, "), get(name), raw("!")]),
embed("base")
]
I propose a new AST case that represents specifically this concept of view extension. In Leaf, the new AST type could be utilized like so:
base.leaf
<title>#import(title)</title>
#import(body)
home.leaf
#extend("base") {
#export("title") { Welcome }
#export("body") { Hello, #(name)! }
}
Because this syntax is more specialized, we can be more aggressive about resolving and caching view data. Let's take a look at the cached AST that could be created for home.leaf
after some resolution steps.
[
raw("<title>"),
raw("Welcome"),
raw("</title>"),
raw("Hello, "),
get(name),
raw("!")
]
With some further optimization, we could reduce this to:
[
raw("<title>Welcome</title>\nHello, "),
get(name),
raw("!")
]
This AST would be dramatically faster to serialize than the earlier one that still needed multiple calls to #set
, #get
, and #embed
to be resolved during the serialization step.
/cc @mcdappdev
Expression like this works:
#if(foo || zoo):
some stuff
#endif
But if !
is added then an error is thrown:
#if(foo || !zoo):
some stuff
#endif
It passes the lexer fine but fails to render with error:
unsupported expression, expected 2 or 3 components: [variable(foo), operator(||), operator(!), variable(zoo)]
Here's the test:
func testLeafConditional3() throws {
let template = """
#if(foo || !zoo):
some stuff
#endif
"""
let v = parse(template).first!
print(v)
let s = try render(template, ["foo": "some"])
print(s)
}
The issue is in LeafKit/LeafData/LeafDataRepresentable.swift
. The offending line is
extension Float80: LeafDataRepresentable {}
Which needs to be changed to
#if arch(i386) || arch(x86_64)
extension Float80: LeafDataRepresentable {}
#endif
in order to compile on a raspberry pi.
I'm using Leaf 4.0.0-rc1.2 and Leaf-Kit 1.0.0-rc1.2. I've tried a minimal route to test index in a for loop but get an error:
/Users/npr/Dropbox/Apps/rv4/Resources/Views/for.leaf: unable to match TagDeclaration(name: "", parameters: Optional([variable(name)]), expectsBody: true) with TagDeclaration(name: "endfor", parameters: nil, expectsBody: false)
The .leaf file is:
#for(name in names):
#(name):#(index)
#endfor
If I take the reference to index out, then it works as expected.
Include some math presentation examples for testing: https://forums.swift.org/t/pitch-new-leaf-body-syntax/18188/14?u=tanner0101
'self' refers to the method 'LeafKeyword.self', which may be unexpected
SourcePackages/checkouts/leaf-kit/Sources/LeafKit/LeafLexer/LeafParameterTypes.swift:67:44: warning: 'self' refers to the method 'LeafKeyword.self', which may be unexpected
internal var identity: Bool { self == .`self` }
^
SourcePackages/checkouts/leaf-kit/Sources/LeafKit/LeafLexer/LeafParameterTypes.swift:67:44: note: use 'LeafKeyword.self' to silence this warning
internal var identity: Bool { self == .`self` }
^
LeafKeyword.
<unknown>:0: warning: 'self' refers to the method 'LeafKeyword.self', which may be unexpected
<unknown>:0: note: use 'LeafKeyword.self' to silence this warning
Build a Vapor project with leaf-kit
in it on Xcode 13.3
No Xcode warnings should exist
vapor: stable 18.3.3
FYI: the issue template says to use vapor --version
but this command doesn't exist any longer.
I tried to define a custom tag that accepts a body, but the body is provided to the tag as [Syntax]
, which is not something that converts to the required return type of LeafData
.
How can I capture the body, and return if necessary?
For context, I'm trying to create requireRole(role)
tag, that returns the body if the role matches the role of the current user;
#requireRole("ADMIN"):
<a href="/projects/#(project.id)/delete">Delete</a>
#endrequireRole
In the tag class I retrieve the body using ctx.requireBody()
, which, as mentioned, returns an array of Syntax
elements.
final class RequireRoleTag<A>: LeafTag where A: RoleAuthenticatable {
init(userType: A.Type) {}
func render(_ ctx: LeafContext) throws -> LeafData {
try ctx.requireParameterCount(1)
let body = try ctx.requireBody()
guard let requiredRole = ctx.parameters[0].string else {
throw "role is not a string"
}
guard let req = ctx.request,
let role = getRole(req: req)
else {
return .trueNil
}
if role == requiredRole {
// This doesn't work
return body
}
return .trueNil
}
private func getRole(req: Request) -> String? {
let a = req.auth.get(A.self)
return a?.role.description
}
}
The #
character is not allowed to start the value of a data-*
attribute. For instance data-foo="#bar"
renders as data-foo=""
.
Simply add a data-*
attribute that has the #
as the first character.
If I add data-foo="#bar"
I expect data-foo="#bar"
to be rendered in the final HTML.
When embedding content using #get/set
that contains a custom tag which uses a Client
to fetch information, the content is not displayed / empty.
I have added a couple of examples to visualize the issue:
SimpleTag
ClientTag
.ClientTag
outside of body.Example repo: https://github.com/cweinberger/LeafClientIssue
Examples deployed on vapor.cloud:
Is your feature request related to a problem? Please describe.
I'd love to pull some LeafData in lazily with a closure. It appears to be implemented but disabled. Is this feature hidden due to an incomplete implementation or is it's internal visibility an error?
Describe the solution you'd like
I'd love to see closures for lazy LeafData to become public API.
Additional context
The API I'm referring to is in LeafData.swift:
/// Creates a new `LeafData` from `() -> LeafData` if possible or `nil` if not possible.
/// `returns` must specify a `NaturalType` that the function will return
internal static func lazy(_ lambda: @escaping () -> LeafData,
returns type: LeafData.NaturalType,
invariant sideEffects: Bool) throws -> LeafData {
LeafData(.lazy(f: lambda, returns: type, invariant: sideEffects))
}
The underscore character is not being treated as a valid character in a attribute name.
Include the following code snippets in a Leaf project:
Foo.swift
struct Foo: Content {
enum CodingKeys: String, CodingKey {
case fooBar = "foo_bar"
}
let fooBar: String
}
test.leaf
#(foo_bar)
Route handler:
func routes(_ app: Application) throws {
app.get { req -> EventLoopFuture<View> in
let foo = Foo(fooBar: "foobar")
return req.view.render("test", foo)
}
}
Leaf should be able to handle underscores in attribute names. This is particularly relevant when using snake case in models being shared across clients, ie restful api responses.
[ ERROR ] LexerError(line: 0, column: 5, name:
"/Users/jeremy/Projects/underscore/Resources/Views/test.leaf", reason:
LeafKit.LexerError.Reason.invalidParameterToken("_"), lexed: [tagIndicator, tag(name: ""),
parametersStart, param(variable(foo))])
Example demo project -> underscore
Getting an "unresolved ast" error when trying to #extend
an HTML snippet into the top-level template.
But have no problems using #extend
of the same snippet at lower levels.
The #embed
tag seems to have been deprecated... yet it was so handy for inclusions of simple snippets.
Is there a workaround for this?
base.leaf
<!doctype html>
<html lang="en">
<head>
</head>
<body>
✅This works here:
#extend("partials/picture.svg"):#endextend
#import("body")
</body>
</html>
page.leaf
#extend("base"):
#export("title", "Some website")
#export("description", "Some page")
#export("body"):
🛑 This doesn't work here: [ ERROR ] unresolved ast
#extend("partials/picture.svg"):#endextend
#endexport
#endextend
picture.svg
<svg>
<path d="M0...">
</svg>
In Leaf 3 we could loop not just over arrays from context, but we also could loop over custom tag result
Now in Leaf 4:
#for(language in #config("app", "languages")):
Throws exception:
LeafKit.LexerError.Reason.invalidParameterToken(\"#\")
#for(language in config("app", "languages")):
Throws:
"for loops expect single expression, 'name in names'"
Is your feature request related to a problem? Please describe.
I'd like to implement a custom LeafCache
Describe the solution you'd like
Have read access to LeafAST
fields that need to be cached
Describe alternatives you've considered
Have LeafAST
conform to Codable
and have the field name
public.
I suppose the effort to have LeafAST
conforms Codable
to not be too much problematic thanks to the introduction of the Codable
enums in swift 5.5
Additional context
LeafCache
is not implementable because LeafAST
doesn't even have the field name
public which is how LeafCache requires to store the leafAST
.
It won't be enough to have it public because we don't have access to the others fields.
Consider porting this Leaf 3 enhancement: vapor/template-kit#45
Constructing names on the fly can be quite useful, e.g.:
#for(cloud in clouds):
#extend("cloud/" + #(cloud) + "_logo")
#extend("cloud/" + #(cloud) + "_button")
#endfor
Is your feature request related to a problem? Please describe.
I'm currently looking for a way to add my routes in my leaf templates
<a href="#routes(home)">Home</a>
# => <a href="/home">Home</a>
Describe the solution you'd like
I would like for a way to add routes to leaf templates
Describe alternatives you've considered
thought about hardcoding them but that is too tedious and it doesn't scale very well
Additional context
As an Example here is a Rails
<%= link_to "Home", @home %>
# => <a href="/home">Home</a>
Compile generates an error due to line 45 of file LeafKit/LeafData/LeafDataRepresentable.swift. The offending line is:
extension Float80: LeafDataRepresentable {}
Enclosing it in compiler directives fixes the problem:
#if arch(i386) || arch(x86_64)
extension Float80: LeafDataRepresentable {}
#endif
Is your feature request related to a problem? Please describe.
When debugging Leaf templates in complex applications, I often find it extremely difficult to work out what is being provided as context to the current rendering.
Describe the solution you'd like
I'd love to be able to quickly dump out the complete context to my work-in-progress.
Describe alternatives you've considered
I'm not sure there are alternatives? I can keep poking around and manually looking for what I've missed.
I would like to be able to do this when paginating:
#for(i in 1...page_count):
<li class="active"><a href="/items/list/?page=#(i)">#(i)</a></li>
...
My current workaround is to create a page count array inside the application:
let page_count = Array(1...items.pageCount)
With Leaf 3 I could use tag.source.file
and tag.source.line
.
source
appears to be missing from LeafContext
altogether?
Templates will often contain references to static variables that only need to be serialized once. For example:
<link href="{{baseURL}}/style.css">
Currently TemplateKit offers no distinction between dynamic and static variables. There's potential for huge performance gains if TemplateKit could support a "pre-render phase" where variables known to be static are converted to raw data.
There are a couple of ways this could be implemented.
A new syntax type could be created that represents a "static" variable (as opposed to the normal variable type). When rendering an uncached view, TemplateKit could first serialize all instances of this static variable.
In Leaf, this might look something like:
<link href="#static(baseURL)/style.css">
Hello, #(name)!
Some caveats with this method are that the data would still need to be supplied in the render context even after it has been cached. This could also require some complex logic to "simplify" ASTs after components have been replaced.
Another option is that we create a new type of "pre-renderer". This would take a different parser than the main renderer and would be run as a step before the main renderer.
The pipeline for this would look like:
ViewData + StaticContext -> Pre-Renderer = ProcessedViewData
ProcessedViewData + Context -> Renderer = FinalViewData
This has the benefit that we can use an entirely different parser for the pre-process phase.
In Leaf, this might look something like:
<link href="$(baseURL)/style.css">
Hello, #(name)!
The pre-processing parser would use $
as it's special token, where as the normal parser would continue to use #
.
Currently Leaf parses templates using ByteBuffer
(byte-by-byte). We should consider moving to String
for UTF-8 support. Especially now that String is natively UTF-8 in Swift 5.
Consider you have two structs:
struct Test1 {
let a: String
let b: Test2
}
struct Test2 {
let c: [String]
}
If you have Test1 as Data for a leaf template, iterating over b.c is not possible.
Consider the following workaround:
diff --git a/Sources/LeafKit/LeafSerializer.swift b/Sources/LeafKit/LeafSerializer.swift
index 9fa7a2b..7ee3a2f 100644
--- a/Sources/LeafKit/LeafSerializer.swift
+++ b/Sources/LeafKit/LeafSerializer.swift
@@ -100,7 +100,21 @@ struct LeafSerializer {
}
mutating func serialize(_ loop: Syntax.Loop) throws {
- guard let array = data[loop.array]?.array else { throw "expected array at key: \(loop.array)" }
+ let arrayPath = loop.array.split(separator: ".")
+ var data = self.data
+ for component in arrayPath.dropLast() {
+ guard let subData = data[String(component)] else {
+ throw "did not found component \(component) in path \(loop.array)"
+ }
+ if let dictionary = subData.dictionary {
+ data = dictionary
+ }
+ else {
+ throw "expected dictionary for component \(component) in path \(loop.array)"
+ }
+ }
+ guard let array = data[String(arrayPath.last!)]?.array else { throw "expected array at key: \(loop.array)" }
templates with extend not rendering
4.102.0
MacOS14.5
Swift5.10
/Views/Layouts/base.leaf
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
#if(meta):
#(meta)
#endif
<title>#(title)</title>
</head>
<body>
#(body)
</body>
</html>
/Views/Public/about.leaf
#extend("Layouts/base"):
#export("title"):
About Seald CMS
#endexport
#export("meta"):
<meta name="description" content="This is the home page of My Vapor App">
<meta name="keywords" content="vapor, swift, web, app">
#endexport
#export("body"):
<h1>Hello Seald CMS</h1>
#endexport
#endextend
No response
Documentation about syntax for extend and export is not up to date imo
Is your feature request related to a problem? Please describe.
I'm using a custom tag that has a string parameter. If this parameter includes a quote, this is interpreted as final quote for the parameter, although it's escaped. For example:
#tag("value", "this is a \"custom\" parameter")
for the second parameter, LeafLexer parses it as this is a \
instead of this is "custom" parameter
Describe the solution you'd like
I would like to allow quotes inside parameters using escape characters or other option.
Looping through arrays is already possible via the #for( something in array ):
syntax. But I often need to loop through dictionaries. In my projects I often have [UUID: Codable]
or [String: Codable]
structures.
I would like to be able to write
struct Person : Codable {
var name: String
// ...more props
}
let persons : [String: Person] = [
"a": Person(name: "tanner"),
"b": Person(name: "ziz"),
"c": Person(name: "vapor")
]
let template = """
#for(person in persons):
#(key): #(person.name) index=#(index) last=#(isLast) first=#(isFirst)
#endfor
"""
render(template, ["persons": persons])
Should render
a: tanner index=0 last=false first=true
b: ziz index=1 last=false first=false
c: vapor index=2 last=true first=false
Leaf's current #for
implementation already exposes additional local in-loop variables index
, isFirst
and 'isLastand my proposal adds just one more
key` variable.
Alternatively extending the loop syntax to #for( (key, person) in persons )
would have provided control over the name of the in-loop variables, and this feels more "Swift-like" yet breaks with Leaf-kit tradition.
Another alternative was to loop through the keys instead of the values, e.g. #for( key in persons ):
but this will require repeated dictionary lookups when interpolating values, e.g.
#for(key in persons):
#(key): #(persons[key].name) #(persons[key].address) #(persons[key].tel)
#endfor
This option feels more like Javascript or Python but the repeated dictionary looks are tedious to write and has a runtime performance penalty.
When nesting an #extend
with two parameters, the second parameter seems to be ignored instead of being provided as context to the template.
Here’s a test (based on the existing testExtendWithSugar
) that shows the issue and fails:
func testNestedExtendWithSugar() throws {
let layout = """
<body>#import("main")</body>
"""
let header = """
<h1>#(child)</h1>
"""
let base = """
#extend("layout"):#export("main"):#extend("header", parent)#endexport#endextend
"""
let expected = """
<body><h1>Elizabeth</h1></body>
"""
let layoutAST = try LeafAST(name: "layout", ast: parse(layout))
let headerAST = try LeafAST(name: "header", ast: parse(header))
let baseAST = try LeafAST(name: "base", ast: parse(base))
let baseResolved = LeafAST(from: baseAST, referencing: ["layout": layoutAST, "header": headerAST])
var serializer = LeafSerializer(
ast: baseResolved.ast,
ignoreUnfoundImports: false
)
let view = try serializer.serialize(context: ["parent": ["child": "Elizabeth"]])
let str = view.getString(at: view.readerIndex, length: view.readableBytes) ?? ""
XCTAssertEqual(str, expected)
}
The test fails with:
XCTAssertEqual failed: ("<body><h1></h1></body>") is not equal to ("<body><h1>Elizabeth</h1></body>")
My understanding from #111 is that when #extend
is given two parameters, the second parameter should be passed as context to the template (which sounds like a very useful feature). That behavior seems to happen correctly when the #extend
isn’t nested, but doesn’t work when nested.
My hope is to use this feature to pass data around like the example below:
items/list.leaf:
#extend("layouts/main"):
#export("content"):
<h1>Items</h1>
#for(item in items):
#extend("items/item", item)
#endfor
#endexport
#endextend
items/item.leaf:
<div>
<h2>#(name)</h2>
…
</div>
I’ve tried to figure out how to fix the issue but haven’t been able to yet.
I also tried to workaround the issue by wrapping my nested #extend
in a #with
. From the example above, it would look like this:
items/list.leaf:
#extend("layouts/main"):
#export("content"):
<h1>Items</h1>
#for(item in items):
#with(item):
#extend("items/item")
#endwith
#endfor
#endexport
#endextend
But that seems to cause the entire items/item
template to not be rendered. I’m not sure if that’s intentional or not. If not, I can file a separate issue for that. Thanks!
LeafKit should consider officially adopting an EBNF describing its syntax. This could help new users understand the language and help contributors write tooling.
I've taken a first stab at writing the EBNF, but it's not perfect yet.
template = { text | tag };
tag = tag start, [ identifier ], [ params ], has body;
has body = ":";
func = identifier, params;
number = { digit };
string = '"', { text - '"' }, '"';
identifier = character, { character | digit | "_" };
params = params start, [ param, { param delim, [ space ], param } ], params end;
param = identifier | func | string | number;
param delim = ",";
params end = ")";
params start = "(";
text = { ascii | tag escape } - tag start;
tag escape = "\#";
tag start = "#";
character = ? a...z | A...Z ?;
digit = ? 0...9 ?;
ascii = ? any byte ?;
space = " "
A couple of things left to nail down:
Is your feature request related to a problem? Please describe.
I have a layout.leaf
looking like this
<doctype html>
<html>
<head>
<link rel="stylesheet" href="mystyles.css"/>
#import("otherStyles")
</head
<body>
#import("content")
</body>
</html>
But if I don't export it, I'm having the following error:
"import("styles") should have been resolved BEFORE serialization".
What I have to do now to have it working is adding #export("otherStyles"): #endexport
to all of my views, even if they don't have otherStyles.
Describe the solution you'd like
I would like that import to be empty, I don't always want to export additional styles, possible solutions are
a) #import("otherStyles") // Modify default behaviour to be optional or allow empty imports
b) #import("otherStyles", allowEmpty: true)
c) #import("otherStyles", "default")
a
or c
would be perfect 👍
As requested in #56, this is the issue tracking the bug, that imports aren't resolved when they happen in certain nested scopes inside the imported file.
The two test cases I've written in #56 are here:
func testLoopedConditionalImport() throws {
var test = TestFiles()
test.files["/base.leaf"] = """
#for(x in list):
#extend("entry"):#export("something", "Whatever")#endextend
#endfor
"""
test.files["/entry.leaf"] = """
#(x): #if(isFirst):#import("something")#else:Not First#endif
"""
let expected = """
A: Whatever
B: Not First
C: Not First
"""
let renderer = LeafRenderer(
configuration: .init(rootDirectory: "/"),
cache: DefaultLeafCache(),
files: test,
eventLoop: EmbeddedEventLoop()
)
let page = try renderer.render(path: "base", context: ["list": ["A", "B", "C"]]).wait()
XCTAssertEqual(page.string, expected)
}
func testMultipleLoopedConditionalImports() throws {
var test = TestFiles()
test.files["/base.leaf"] = """
#for(x in list1):
#extend("entry"):#export("something", "Whatever")#endextend
#endfor
#for(x in list2):
#extend("entry"):#export("something", "Something Else")#endextend
#endfor
"""
test.files["/entry.leaf"] = """
#(x): #if(isFirst):#import("something")#else:Not First#endif
"""
let expected = """
A: Whatever
B: Not First
C: Not First
A: Something Else
B: Not First
C: Not First
"""
let renderer = LeafRenderer(
configuration: .init(rootDirectory: "/"),
cache: DefaultLeafCache(),
files: test,
eventLoop: EmbeddedEventLoop()
)
let page = try renderer.render(path: "base", context: [
"list1": ["A", "B", "C"],
"list2": ["A", "B", "C"],
]).wait()
XCTAssertEqual(page.string, expected)
}
Furthermore, imports
might appear in all kinds of different places (e.g. inside parameters of another tag).
One thing I noticed in my approach to it, is that care has to be taken with Conditional
since it's a class. My first approach was not resolving imports in repetitive extend
s correctly anymore, because the Conditional
in the included file was only created once and thus only resolved in the first pass. The second embed
would show the export
s of the first extend
. This is covered by the second test-case, though.
Perhaps somewhat related to #16 ... Parser removes all words that begin with #
even if no matching Leaf keyword exists.
This leads to subtle bugs, for example Bootstrap mobile menus stop working. Such a problem isn't immediately obvious and as a first time "gotcha" takes quite some time to figure out why and implement escaping. Too easy to miss.
Leaf silently transforms data-target="#navbarCollapse"
into data-target=""
This markup breaks:
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse">
Escaped version obviously works:
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="\#navbarCollapse">
I'm getting an error that prevents me from using reusable components with leaf.
The code is the following:
#extend("Templates/base"):
#export("body"):
#for(challenge in challenges):
#extend("Components/single-challenge-total"):#endextend
#endfor
#endexport
#endextend
It results in the following error:
{"error":true,"reason":"syntax extend(\"Components\/single-challenge-total\") should have been resolved BEFORE serialization"}
I've tried this with leaf-kit 1.0.0-rc.1.6 and leaf 4.0.0-rc.1.2
Consider porting this Leaf 3 performance enhancement to LeafKit: vapor/template-kit#56
in prior versions of leaf, the following was supported:
#("hi") #thisIsNotATag...
currently tho, there's no way to distinguish that it is not a tag given new parsing rules, particularly around end tags.
we could propose to use the original #("#")
but is maybe not so nice long term. alternatives could be found to support this type of syntax more intelligently if we can design rules to do so.
Modulo (%) operation is not supported at the moment. It can be used e.g. for calculation even/odd rows. Zebra striped tables or grids are pretty common design requirement.
example psudo-code:
#for(row in rows)
#if(index % 2 == 0):
// use row backgroud color
#else:
// use row alternate backgroud color
#endif
#endfor
Implement function which handles modulo operation for Int
as well as Double
input values.
Already working on pull-request.
Hey, just looking for a possibility to use two variables together to access an index of an array(Strings) like this:
Example:
#for(index in intValue5):
#(colors)[#(index)]
#endfor
Swift example:
let index: Int = 2
let colours: [String] = ["#000000", "#0c02ff", "#008000"]
let colour: String = colours[index]
I tried a custom function #ValueFromArray(#(colors), #(index)) but got:
No exact match for function ValueFromArray(_ : variable($:index)); 1 possible matches: ValueFromArray(_: Array, _: Int)"}
public struct ValueFromArray: LeafFunction, StringReturn, Invariant {
public static var callSignature: [LeafCallParameter] { [.array, .int] }
public func evaluate(_ params: LeafCallValues) -> LeafData {
guard let array = params[0].array else {
return .error("`ValueFromArray` must be called with an array parameter.")
}
guard let index = params[1].int else {
return .error("`ValueFromArray` must be called with an int parameter.")
}
return array[index]
}
}
Thx for your Help, great work for the 4.0.0-tau.1 release, only documentation is hard (to find and also to create i know).
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.