Git Product home page Git Product logo

leaf-kit's Introduction

leaf-kit's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

leaf-kit's Issues

Underscore character not valid in attribute name

The underscore character is not being treated as a valid character in a attribute name.

To Reproduce

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)
    }
}

Expected result

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.

Actual result

[ 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

view extensions / pre-compile templates

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

`#extend` not rendered when wrapped by `#with`

Describe the bug

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.)

To Reproduce

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>")

Expected behavior

The #with call should provide its parameter as context to the #extend.

Environment

  • Vapor Framework version: 4.85.1
  • Vapor Toolbox version: 18.7.4
  • OS version: macOS 14.1.1 (23B81)

Additional context

I think I see what the problem is. I’ll submit a PR shortly.

#count tag not working

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.

#if statement with `not` (!) fails to render

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)
    }

#if(variable) fails check if variable exists in context

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.

Hashtag character is not allowed to start the value of a data attribute

The # character is not allowed to start the value of a data-* attribute. For instance data-foo="#bar" renders as data-foo="".

To Reproduce

Simply add a data-* attribute that has the # as the first character.

Expected behavior

If I add data-foo="#bar" I expect data-foo="#bar" to be rendered in the final HTML.

Environment

  • Vapor Framework version: 4.39.2
  • Vapor Toolbox version: 18.3.0
  • OS version: Big Sur 11.2.1

Leaf removes non-keywords that follow `#`

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">

Request for Feedback: Leaf Syntax & Design

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.

LeafKit Mission

As I understand it, anyway

  • Performant presentation layer templating of normalized data input
  • Coherent & legible template code that co-exists with final presentation language peacefully
  • Swift-friendly concepts that feel natural as an extension of both Swift & Vapor
  • Lightweight template design and minimal overhead in interpreting provided data
  • Presentation format agnosticism*
    (*While HTML/CSS/JS is the overwhelming use for Leaf, the ability to auto-generate other formats should not be disregarded as the ability to autogenerate things like PDFs could push significant interest in full-stack use of Vapor/Leaf in deeper business chains)

Precis

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:

  • Clearly delineated from presentation language both programmatically and visually
  • Semantically straightforward as mixing two languages in a file is inherently confusing
  • As generic and yet as unique as possible to avoid collisions with other languages

Language as a Pipeline

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.

BIGGEST PROPOSED CHANGE

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)

Nominal BNF Description

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 #

Current Format Issues

Character Escaping

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

  • General syntax proposed solves basically all collision chances with the major use cases for Leaf and is unlikely to prove problematic with other potential presentation language uses.
  • Pre-flighting available tag identifiers is something Leaf needs anyway and that can easily solve
    the potential for accidentally consuming raw hashtags
  • Making Syntax items capable of self-reporting their expectations can prevent consuming colon body indicators - a specific tag, once found, can be told how many parameters it will be receiving, and it can identify whether it would also expect a body (and if not, colons can be left raw). This solves issues like the example before, as the parser is naive and consumes a colon regardless of whether the tag actually WILL take a body. Similar happens with something like #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.

Lack of Commenting

No way of commenting the Leaf file is feasible currently.

SOLUTION

  • Proposed syntax easily allows defining a Leaf Comment as an expression... For example, define a comment expression as anything bounded by two hashtags within the identity function.
  • Ex: #(# THIS IS A LEAF COMMENT #)

Lack of Scoping

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.

  • Some of this can be solved with, as mentioned before, a robust symbol mapping in the parse stages so that ASTs can expose to the serializer what keys they make use of so internal template variables are scoped correctly to prevent overlap - eg, more robust data access like #(context.key) vs #(exports.key).
  • Ideally, I would like to move LeafData towards as much conformance as possible with Vapor/Fluent so it can take advantage of Model protocols to allow direct key access within Leaf so that no additional steps need to be taken to mangle data to fit into a context for the template.
  • Clear scoping rules to establish data stores on the programmatic side can clear up inconsistent naming as well, so that:
    • Functions like #export can use key names without having to use string literals.
    • Templates can access runtime or static variables, eg - request data or global server data

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)

Ambiguous Names

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.

Varied And Robust Language & File Support

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 DSL
  • LeafRenderer.render("template", as: .pdf) to enable a PDF DSL
  • LeafRenderer.render("template", as: .nyancat) to enable a prank DSL

Where 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 embeded references to JPEG files or fonts could be correctly read as raw bytes rather than running through Strings.

loop over custom tag result

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:

  1. #for(language in #config("app", "languages")):
    Throws exception:
    LeafKit.LexerError.Reason.invalidParameterToken(\"#\")

  2. #for(language in config("app", "languages")):
    Throws:
    "for loops expect single expression, 'name in names'"

Consider supporting ranges in the template loop tag

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)

Add modulo (%) infix operator

Description

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

Solution

Implement function which handles modulo operation for Int as well as Double input values.
Already working on pull-request.

Allow `#import` to be empty or not declared

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 👍

free parsing of `#`

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.

Reversing Tau reintroduced issue compiling on Arm64

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.

Add ability to loop though dictionaries

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 morekey` 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.

Leaf-kit cannot render the page with AppCode from JetBrains

Describe the bug

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]

To Reproduce

Steps to reproduce the behavior:

  1. Create Vapor project with leaf template.
  2. Open project in AppCode from JetBrains (version used by me: 2021.1)
  3. Run project

Expected behavior

Webpage render correctly.

Environment

  • Vapor Framework version: 4.41.0
  • Vapor Toolbox version: 18.3.0
  • Leaf version: 4.0.1

Public visibility for `LeafData.lazy(...)` to build LeafData lazily from a closure

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))
    }

Suggestion: increased granular control over template caching

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:

  • nil - no cached document existed for the path
  • true - cached document was purged
  • false - not currently returned but would in future mean that a document is cached for that path, but could not be purged because other templates have dependencies on it

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.

Fork Comparison Detail

Subclassing for context is not working

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)
    }
}

Provide a way to dump the current context as JSON via a tag

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.

CustomTag does not work within #embed/#set/#get

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:

Example repo: https://github.com/cweinberger/LeafClientIssue
Examples deployed on vapor.cloud:

elseif continue testing to next elseif on match

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.

`requireBody()` and `requireNoBody()` not working as expected

Describe the bug

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

To Reproduce

  1. Create a simple project just using Leaf 4
  2. Create
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")
    }
}
  1. Add the tags to the test project
app.leaf.tags["bodytag"] = BodyRequiringTag()
app.leaf.tags["nobodytag"] = NoBodyRequiringTag()
  1. Adding the first tag to the project
#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"

  1. Adding the other tag
#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

Environment

  • Vapor Framework version: 4.76.0
  • Vapor Toolbox version: 18.6.0
  • OS version: MacOS Ventura 13.0
  • Xcode version: 14.1 (14B47b)

Custom tag accessing an index of an array for 4.0.0-tau.1

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).

index not working inside for loop

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.

utf8 support

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.

Add support for minification or prettification of Leaf's final output

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.

Xcode Warnings on Xcode 13.3

Describe the bug

'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

To Reproduce

Build a Vapor project with leaf-kit in it on Xcode 13.3

Expected behavior

No Xcode warnings should exist

Environment

vapor: stable 18.3.3

FYI: the issue template says to use vapor --version but this command doesn't exist any longer.

Getting a 'should have been resolved BEFORE serialization' error

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

Custom tags with body

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
    }
}

pre-render phase

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.

Special AST Case

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.

Separate Renderer

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 #.

/cc @mcdappdev @BrettRToomey

Loops doesn’t like nested arrays

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)" }
             

Add a `#routes` or `#link_to` Tag

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>

LeafCache is public but not implementable

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.

Leaf-kit 1.0.0-rc.1.17 no longer compiling on raspberry pi

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

Allow to add quotes into stringLiteral parameters

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.

comment syntax

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

adopt extended backus-naur form

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:

  • String escaping
  • Supported identifier syntax
  • White space
  • Declarations of basic types like character, digit, ascii, etc

Conditional operands do not seem to evaluate lazily

Describe the bug

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.

To Reproduce

Steps to reproduce the behavior:

  1. Create a leaf file (e.g. 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>
  1. Render the page with the following context:
struct Context: Encodable { let ctx: [String]? }
func configure(app: Application) {
    app.get(use: { $0.view.render("page", Context(ctx: nil)) })
}
  1. Rendering fails with "Unable to convert count parameter to LeafData collection" as thrown by the 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

Expected behavior

The page renders fine, showing "No children." with the following rendered HTML:

<!DOCTYPE html>
<html>
<body>
<p>No children.</p>
</body>
</html>

Environment

  • Vapor Framework version: 4.36.0
  • Leaf Framework version: 4.0.1
  • LeafKit Framework version: 1.0.0
  • Vapor Toolbox version: n/a
  • OS version: macOS 11.0.1 as well as Ubuntu 18.04
  • Swift version: 5.3.1

Additional context

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.

Nested-scope (e.g. conditional) imports in files extended from loop are not resolved

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 extends 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 exports of the first extend. This is covered by the second test-case, though.

Error including partials at top level via #extend

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>

`#extend` with two parameters doesn’t pass second parameter as context when nested

Description

When nesting an #extend with two parameters, the second parameter seems to be ignored instead of being provided as context to the template.

To Reproduce

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>")

Expected behavior

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>

Environment

  • Vapor Framework version: 4.85.1
  • Vapor Toolbox version: 18.7.4
  • OS version: macOS 14.1.1 (23B81)

Additional context

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!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.