This tutorial is all about rendering HTML docs utilizing a model new DSL library known as SwiftHtml and the Vapor internet framework.
Vapor
Introducing SwiftHtml
This time we will begin every thing from scratch. Within the first part of this text I’ll present you easy methods to setup the SwiftHtml as a package deal dependency and easy methods to generate HTML output based mostly on a template file. Let’s begin by making a model new executable Swift package deal.
mkdir Instance
cd "$_"
swift package deal init --type=executable
open Package deal.swift
You can too begin with a macOS Command Line Instrument from Xcode if you want, however these days I choose Swift Packages. Anyway, we should always add SwiftHtml as a dependency to our package deal straight away.
import PackageDescription
let package deal = Package deal(
identify: "Instance",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(url: "https://github.com/binarybirds/swift-html", from: "1.2.0"),
],
targets: [
.executableTarget(name: "Example", dependencies: [
.product(name: "SwiftHtml", package: "swift-html"),
]),
.testTarget(identify: "ExampleTests", dependencies: ["Example"]),
]
)
All proper, now we’re prepared to put in writing some Swift DSL code. We will begin with a extremely primary instance to get to know with SwiftHtml. Within the important.swift
file we should always create a brand new HTML doc, then we are able to use SwiftHtml’s built-in renderer to print the html supply. 🖨
import SwiftHtml
let doc = Doc(.html) {
Html {
Head {
Title("Hi there, World!")
Meta().charset("utf-8")
Meta().identify(.viewport).content material("width=device-width, initial-scale=1")
}
Physique {
Predominant {
Div {
H1("Hi there, World!")
P("This web page was generated by the SwiftHtml library.")
}
}
.class("container")
}
}
}
let html = DocumentRenderer(minify: false, indent: 2).render(doc)
print(html)
As you’ll be able to see the code is fairly easy, particularly if a bit about HTML. The SwiftHtml library tries to comply with the naming conventions as carefully as attainable, so for those who’ve written HTML earlier than this syntax needs to be very acquainted, besides that you do not have to put in writing opening and shutting tags, however we are able to make the most of the Swift compiler to do the boring repetative duties as an alternative of us.
Since we’re utilizing a website particular language in Swift, the compiler can type-check every thing at build-time, this manner it is 100% positive that our HTML code will not have syntax points. After all you’ll be able to nonetheless make semantic errors, however that is additionally attainable for those who’re not utilizing a DSL. 😅
The primary benefit right here is that you just will not be capable to mistype or misspell tags, and you do not even have to consider closing tags, however you need to use end result builders to assemble the HTML node tree. SwiftHtml makes use of tags and it will construct a tree from them, this manner it’s attainable to effectively render all the construction with correct indentation or minification whether it is wanted.
The DocumentRenderer
object can render a doc, it is usually attainable to create all kinds of SGML-based doc varieties, as a result of the SwiftHtml package deal comes with an abstraction layer. For those who check out the package deal construction you need to see that contained in the Sources listing there are a number of different directories, the core of the package deal is the SwiftSgml element, which permits builders to create different area particular languages on high of the bottom elements. 🤔
For instance, for those who check out the SwiftRss package deal you will notice that it is a easy extension over the SwiftSgml library. You’ll be able to subclass the Tag
object to create a brand new (area particular) tag with an underlying Node
object to symbolize a customized merchandise to your doc.
The SwiftSgml library could be very light-weight. The Node
struct is a illustration of a given SGML node with a customized sort, identify and attributes. The Tag
class is all about constructing a hierarchy in between the nodes. The Doc
struct is a particular object which is answerable for rendering the doctype declaration earlier than the basis tag if wanted, additionally in fact the doc accommodates the basis tag, which is the start of every thing. 😅
SwiftSgml additionally accommodates the DocumentRenderer
and a easy TagBuilder
enum, which is a end result builder and it permits us to outline our construction in a SwiftUI-like model.
So the SwiftHtml package deal is only a set of HTML guidelines on high of the SwiftSgml library and it follows the W3C HTML reference guides. You should use the output string to avoid wasting a HTML file, this manner you’ll be able to generate static web sites by utilizing the SwiftHtml library.
import Basis
import SwiftHtml
let doc = Doc(.html) {
Html {
Head {
Title("Hi there, World!")
Meta().charset("utf-8")
Meta().identify(.viewport).content material("width=device-width, initial-scale=1")
}
Physique {
Predominant {
Div {
H1("Hi there, World!")
P("This web page was generated by the SwiftHtml library.")
}
}
.class("container")
}
}
}
do {
let dir = FileManager.default.homeDirectoryForCurrentUser
let file = dir.appendingPathComponent("index.html")
let html = DocumentRenderer(minify: false, indent: 2).render(doc)
strive html.write(to: file, atomically: true, encoding: .utf8)
}
catch {
fatalError(error.localizedDescription)
}
This is only one means to make use of SwiftHtml, for my part static web site mills are high quality, however the true enjoyable begins when you’ll be able to render web sites based mostly on some sort of dynamic knowledge. 🙃
Utilizing SwiftHtml with Vapor
Vapor has an official template engine known as Leaf plus the group additionally created a type-safe HTML DSL library known as HTMLKit, so why create one thing very related?
Nicely, I attempted all of the accessible Swift HTML DSL libraries that I used to be capable of finding on GitHub, however I used to be not completely happy with the presently accessible options. A lot of them was outdated, incomplete or I merely did not like the flavour of the DSL. I wished to have a library which is freakin’ light-weight and follows the requirements, that is the explanation why I’ve constructed SwiftHtml. 🤐
How can we combine SwiftHtml with Vapor? Nicely, it is fairly easy, let’s add Vapor as a dependency to our venture first.
import PackageDescription
let package deal = Package deal(
identify: "Instance",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(url: "https://github.com/binarybirds/swift-html", from: "1.2.0"),
.package(url: "https://github.com/vapor/vapor", from: "4.54.0"),
],
targets: [
.executableTarget(name: "Example", dependencies: [
.product(name: "SwiftHtml", package: "swift-html"),
.product(name: "Vapor", package: "vapor"),
]),
.testTarget(identify: "ExampleTests", dependencies: ["Example"]),
]
)
We will want a brand new protocol, which we are able to use assemble a Tag
, that is going to symbolize a template file, so let’s name it TemplateRepresentable
.
import Vapor
import SwiftSgml
public protocol TemplateRepresentable {
@TagBuilder
func render(_ req: Request) -> Tag
}
Subsequent, we’d like one thing that may render a template file and return with a Response object, that we are able to use inside a request handler after we setup the route handlers in Vapor. Since we will return a HTML string, it’s essential to set the right response headers too.
import Vapor
import SwiftHtml
public struct TemplateRenderer {
var req: Request
init(_ req: Request) {
self.req = req
}
public func renderHtml(_ template: TemplateRepresentable, minify: Bool = false, indent: Int = 4) -> Response {
let doc = Doc(.html) { template.render(req) }
let physique = DocumentRenderer(minify: minify, indent: indent).render(doc)
return Response(standing: .okay, headers: ["content-type": "text/html"], physique: .init(string: physique))
}
}
Lastly we are able to prolong the built-in Request object to return a brand new template renderer if we’d like it.
import Vapor
public extension Request {
var templates: TemplateRenderer { .init(self) }
}
Now we simply must create a HTML template file. I am normally making a context object proper subsequent to the template this manner I am going to have the ability to cross round contextual variables for every template file. I am fairly pleased with this strategy up to now. ☺️
import Vapor
import SwiftHtml
struct IndexContext {
let title: String
let message: String
}
struct IndexTemplate: TemplateRepresentable {
let context: IndexContext
init(_ context: IndexContext) {
self.context = context
}
func render(_ req: Request) -> Tag {
Html {
Head {
Title(context.title)
Meta().charset("utf-8")
Meta().identify(.viewport).content material("width=device-width, initial-scale=1")
}
Physique {
Predominant {
Div {
H1(context.title)
P(context.message)
}
}
.class("container")
}
}
}
}
Lastly we simply have to put in writing some boilerplate code to begin up our Vapor internet server, we are able to use the app occasion and set a get request handler and render our template utilizing the newly created template renderer extension on the Request object.
import Vapor
import SwiftHtml
var env = strive Setting.detect()
strive LoggingSystem.bootstrap(from: &env)
let app = Utility(env)
defer { app.shutdown() }
app.get { req -> Response in
let template = IndexTemplate(.init(title: "Hi there, World!",
message: "This web page was generated by the SwiftHtml library."))
return req.templates.renderHtml(template)
}
strive app.run()
Roughly that is it, you need to be capable to run the server and hopefully you need to see the rendered HTML doc for those who open the localhost:8080 handle utilizing your browser.
Additionally it is attainable to make use of one template inside one other, since you’ll be able to name the render technique on a template and that template will return a Tag. The great thing about this strategy is which you can compose smaller templates collectively, this manner you’ll be able to provide you with a pleasant venture construction with reusable HTML templates written completely in Swift. I am more than pleased with this straightforward answer and looks like, for me, there isn’t a turning again to Leaf or Tau… 🤓