00:06 A while ago, we wanted to change something on our website, but we
found the project would no longer compile or run. Because it's a static website,
we didn't bother maintaining our dependencies, and as a result, they'd become
outdated.
00:45 So, at some point, we decided to bite the bullet and rewrite the
website in Swift. This gave us the opportunity to write our own static site
generator. It worked out very well, and it only took us around two weeks from
start to finish.
01:10 We're going to rebuild the basic skeleton of our static site
generator. Since it borrows a lot of ideas from SwiftUI, it could be interesting
for others who are used to working with SwiftUI and who need a website for an
app or a personal project.
Defining Rules
01:29 Let's get started by writing the simplest example of what a static
site generator should be able to do, which is writing a string to a file:
struct MySite: Rule {
var body: some Rule {
Write(contents: "Hello world", to: "index.html")
}
}
02:09 The Rule
protocol works a lot like SwiftUI's View
. It has a
body
property that returns some other Rule
. This makes it possible to
compose rules:
protocol Rule {
associatedtype Body: Rule
var body: Body { get }
}
02:44 Then, we need to create the Write
struct:
struct Write {
var contents: String
var to: String }
03:12 The question is, how do we conform Write
to Rule
?
Executing Rules
03:25 Right now, we've only defined Rule
as something that returns
another Rule
. But at some point, we need a way to execute a rule in such a way
that it produces a result:
extension Rule {
func execute() {
}
}
04:11 We need to distinguish between rules like MySite
, which are
defined by users of the library, and built-in rules, which are the building
blocks defined by the library.
This same idea lies at the heart of SwiftUI's View
. Built-in views like
HStack
and Text
are fundamentally different from user-defined views, because
they aren't defined in terms of other views. We saw this when we reimplemented
parts of SwiftUI.
05:20 The BuiltinRule
protocol requires a run
function to be
implemented:
protocol BuiltinRule {
func run()
}
05:41 The Write
rule is a BuiltinRule
; it does its work in a custom
run
function — for now, we just print the contents to the console. And to be
incorporated into other rules, Write
also needs to be a Rule
. Its body
property should never be executed, so we throw a fatal error in it:
struct Write: BuiltinRule, Rule {
var contents: String
var to: String
func run() {
print("\(contents) — filename: \(to)")
}
var body: Never {
fatalError()
}
}
06:53 If we want to use Never
as a body type, we have to conform
Never
to Rule
:
extension Never: Rule {
var body: Never {
fatalError()
}
}
Every built-in rule will have this same error-throwing body
property, so we
can provide a default implementation and remove it from Write
:
extension BuiltinRule {
var body: Never {
fatalError()
}
}
struct Write: BuiltinRule, Rule {
var contents: String
var to: String
func run() {
print("\(contents) — filename: \(to)")
}
}
08:07 Next, we need to implement the Rule.execute
function. If the
rule we call this on conforms to BuiltinRule
, we call its run
method.
Otherwise, we have to somehow execute the body
property. We can achieve this
by creating an AnyBuiltinRule
.
08:26 We initialize AnyBuiltinRule
with a Rule
. If this rule is a
BuiltinRule
, we store its run
method. Otherwise, we create an
AnyBuiltinRule
from the rule's body
, and we store its run
method:
struct AnyBuiltinRule: BuiltinRule {
let _run: () -> ()
init<R: Rule>(_ rule: R) {
if let builtin = rule as? BuiltinRule {
self._run = builtin.run
} else {
self._run = { AnyBuiltinRule(rule.body).run() }
}
}
func run() {
_run()
}
}
09:46 To execute
a rule, we wrap it in an AnyBuiltinRule
, and we
call run
on it:
extension Rule {
func execute() {
AnyBuiltinRule(self).run()
}
}
09:57 We'll need to define more built-in rules. And to have multiple
rules in a rule, we'll need to write a RuleBuilder
. But we already have the
basic infrastructure in place: we can define custom rules using a mix of rules
and built-in rules, just like how we create SwiftUI views.
HTML
10:20 One thing a static site generator obviously needs is the ability
to produce HTML. We imported Robb Böhnke's Swim
library, and we'll use it to write our templates.
10:46 Instead of the "Hello world" string, we want to pass a Node
to
the Write
rule:
import Swim
struct Write: BuiltinRule, Rule {
var contents: Node
var to: String
func run() {
print("\(contents) — filename: \(to)")
}
}
11:12 In our example site, we can now construct an HTML node using Swim:
struct MySite: Rule {
var body: some Rule {
Write(contents: html {
HTML.body {
h1 { "Hello, World!" }
}
}, to: "index.html")
}
}
Coming Up
11:56 When we run our example, formatted HTML output is printed to the
console. We'll need to write this output to an actual file:
MySite().execute()
But we only specify a relative path for the Write
rule, e.g. index.html
. So,
when we execute the rule, we need to know the base path.
12:20 We want to define this base path — and other configuration details
— in an environment. Like in SwiftUI, we want to be able to access and override
the environment, but we don't want to manually propagate it down to each rule.
Let's tackle this in the next episode.