Swift-html 0.1.1: Type-safe, extensible, transformable HTML

We were excited about the recent announcement and look forward to the growing server-side Swift ecosystem, and today we'd like to propose a contribution to the space:

swift-html is a type-safe, transformable, extensible DSL for building and rendering HTML in Swift.

We think that Swift's type system gives us the opportunity to rethink today's more popular solution of templating languages. A Swift API can provide more compile-time guarantees and runtime flexibility. While HTML has only been brought up a couple times in the comments of the Server work group, new focus areas topic, we think it's an area worthy of consideration.

We also have a couple small libraries for integrating swift-html into Kitura and Vapor projects:

16 Likes

Really cool work :+1:

As a heavy user of Stencil, I see the possibilities of using a DSL instead. What interests me most is the power that comes from using Swift to defines views, the design possibilities that result from that and the prospect of being able to properly unit test views.

I do see a few potential issues however:

  1. With templates, I get pretty much immediate feedback when I change a template, as I don't have to recompile anything. How does the compilation delay added by your DSL scale to larger projects?
  2. How readable are large pieces of HTML defined using your DSL? At first glance, they look less readable than plain HTML + Stencil.

If you're interested in a challenge, my project is open source and sufficiently documented: GitHub - svanimpe/around-the-table: An open source platform that supports tabletop gaming communities, written in Swift.

I'd love to see a DSL version of this and compare the two.

We find the compilation of views themselves to be pretty quick, though it'd be good to gather some more data here. One thing to note is that the Swift Playgrounds provide a really nice feedback loop with a live WKWebView. You don't even need to hop over to your browser and refresh.

After working with DSLs for well over a year (in Swift, but also in Elm and other languages), we find them just as readable as raw HTML, and in Xcode they're definitely an improvement over templates since they're syntax-highlighted. (I understand that a lot of folks work with Stencil in other editors to get syntax highlighting.)

Haha, not sure how quickly we can attend to converting the whole project, but it would definitely be fun to explore and would help provide some data around your first concern.

While I personally do not like this specific thing for various reasons, this particular argument really doesn't fly in the Swift context. Why use Swift in the first place if you care about compilation time? Rather use a scripting language if you need super fast turnarounds.

Could you provide some constructive feedback here? It would be helpful to understand your reservations.

:wave: I'm Stephen's collaborator on this project...

Yeah this is exactly what motivated us! We both have years of experience with Ruby and JS templating languages in large codebases with many developers working on them. We got bitten many times by the fragility of stringly templates, and so when we started doing server-side Swift we looked for ways to fix that. Along the way we discovered that a lot of the niceties that templating languages give you can be expressed in much simpler ways using Swift.

Awesome! Glad to see some server-side Swift projects open sourced! Here's a few observations I have from browsing your repo:

  • I often found while working in templating languages that I let them get very large with lots of logic because I found it to be a pain to create lots of tiny templates since they each require a whole new file and probably some kind of directory structure to keep them organized.

    However, with Swift we can have a bunch of lil Swift view functions right next to each other, and decompose a large view into a bunch of small ones. Here's a small fragment I took from your base.stencil file (it builds up a nav unordered list):

    func navBarList(user: User?, unreadMessageCount: Int) -> Node {
      return ul(
        [`class`("navbar-nav")],
        [
          userListItems(user: user, unreadMessageCount: unreadMessageCount),
          li(
            [`class`("nav-item")],
            [a([`class`("nav-link"), href("/web/faq")],  [i([`class`("fa fa-question-circle fa-lg")], []))]
          )
        ]
      )
    }
    
    func userListItems(user: User?, unreadMessageCount count: Int) -> [Node] {
      guard let user = user else {
        return [
          li(
            [`class`("nav-item")], 
            [a([`class`("global-signin nav-link"), href("#")], ["Aanmelden"])]
          )
        ]
      }
    
      return unreadMessageCount(count) + [
        li(
          [`class`("nav-item dropdown")],
          [
            a([`class`("nav-link dropdown-toggle"), href("#"), data("toggle", "dropdown")], [.text(user.name)]),
            div([
              // More stuff here...
              ])
          ]
        )
      ]
    }
    
    func unreadMessageCount(_ count: Int) -> [Node] {
      guard count > 0 else { return [] }
      return [
        li(
          [`class`("nav-item")],
          [
            a(
              [`class`("nav-link"), href("/web/user/messages")], 
              [i([`class`("fa fa-envelope")], [])]
            )
          ]
        )
      ]
    }
    

    Couple of cool things about this:

    • It's not significantly longer than the template code (and I was generous with newlines), but it's broken into 3 easily digestible chunks that could even maybe be reused elsewhere some day.
    • We are using an optional User, which no templating language that I know of has implemented.
    • We are using a guard, which I also don't think any templating language has a concept of an early exit control flow.
    • We're using named arguments in functions to make it very clear what data the view requires.
  • You can easily build up some nice CSS helpers that aid your use of Bootstrap. Imagine a function like:

    func margin(top: Int? = nil, right: Int? = nil, bottom: Int? = nil, left: Int? = nil) -> String {
      return [
        top.map { "mt-\($0)" },
        right.map { "mr-\($0)" },
        bottom.map { "mb-\($0)" },
        left.map { "ml-\($0)" },
        ]
        .compactMap { $0 }
        .joined(separator: " ")
    }
    

    Then you could even do things like div([class(margin(top: 2, right: 1, bottom: 2, left: 1)))], ...). That could really help make some common stylings reusable. Also cool that we are making the margin API better by giving named arguments, and even default them to nil so that you can omit any sides you want.

  • In the swift-html DSL there is a raw HTML escape hatch, so you are always able to just copy-paste some stuff in just to get it working. Taking some of the code I wrote above, you could mix-and-match:

    li(
      [`class`("nav-item")],
      [
        .raw("""
          <a class="nav-link" href="/web/user/messages">
              <i class="fa fa-envelope"></i>
          </a>
          """)
      ]
    )
    

We also have a large site completely open source in Swift: GitHub - pointfreeco/pointfreeco: 🎬 The source for www.pointfree.co, a video series on functional programming and the Swift programming language.. It's built using this DSL and we've been working on it for the past year.

We even use the DSL to render our HTML emails, and this is where it really shines. Because we hold the document as a Swift data type, we were able to write a function that inlines our CSS stylesheet into a Node value, which is necessary since email clients don't allow external stylesheets or <style> tags. This is very difficult with templating languages because you essentially have to parse the output of the template, transform it, and then re-render it.

2 Likes

Your "cut-out" quote extract suggests that I didn't provide constructive feedback. That's not nice! ;-) My main point is/was in your favor actually:

this particular argument really doesn't fly in the Swift context. Why use Swift in the first place if you care about compilation time? Rather use a scripting language if you need super fast turnarounds.

While there are plenty of good reasons to use templates instead of code generation (and you name some on your projects' README), fast turnaround isn't a particularly convincing one in the Swift case. (if you think about it, a template is often just a specialized scripting language, so why not also use one for the invoking parts ...)

While I really don't want to go into details about all the stuff I dislike about that specific solution (that is going nowhere and just a waste of time, and also doesn't really belong here), I think the main takeaway is that this kind of stuff is a very opinionated thing.

P.S.: Actually I absolutely do think that there is a place for both (code generated and templated content).

That was my biggest issue as well. I rely on extends and include to add some structure and reuse, and split complex templates into separate versions. I'm happy with what I have now, although it's certainly not ideal.

It's not exactly the same, but Stencil does support checking for absent values, so I can use {% if user %} to check if user is not nil.

Yeah, that's caused me a lot of headaches. Without an early exit, control flow can be really complex, to the point where the template needs to be reworked (split) to keep it manageable.

Thanks for your feedback and examples. I really need to take a closer look at this!

1 Like

I think this is a very interesting (and different) approach to doing view generation w/ server-side Swift. Once we decide on how the proposal and incubation process for the SSWG works, I think you should definitely submit this package. :slight_smile:

1 Like