Link to the renderers guide from `README.md` (#251)

* Link to the renderers guide from `README.md`

The guide itself was merged into a single file for easier navigation.

* Update RenderersGuide.md
This commit is contained in:
Max Desiatov 2020-08-07 16:01:27 +01:00 committed by GitHub
parent e11effdd8c
commit 2a49b7808b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 410 additions and 394 deletions

View File

@ -4,22 +4,25 @@
![CI status](https://github.com/swiftwasm/Tokamak/workflows/CI/badge.svg?branch=main)
At the moment Tokamak implements a very basic subset of SwiftUI. Its DOM renderer supports
a few view types and modifiers (you can check the current list in [the progress document](docs/progress.md)),
and a new `HTML` view for constructing arbitrary HTML. The long-term goal of Tokamak is to implement
as much of SwiftUI API as possible and to provide a few more helpful additions that simplify HTML
and CSS interactions.
At the moment Tokamak implements a very basic subset of SwiftUI. Its DOM renderer supports a few
view types and modifiers (you can check the current list in [the progress
document](docs/progress.md)), and a new `HTML` view for constructing arbitrary HTML. The long-term
goal of Tokamak is to implement as much of SwiftUI API as possible and to provide a few more helpful
additions that simplify HTML and CSS interactions.
If there's some SwiftUI API that's missing but you'd like to use it, please review the existing
[issues](https://github.com/swiftwasm/Tokamak/issues) and [PRs](https://github.com/swiftwasm/Tokamak/pulls)
to get more details about the current status, or [create a new issue](https://github.com/swiftwasm/Tokamak/issues/new)
to let us prioritize the development based on the demand. We also try to make the development of
views and modifiers easier (with the help from the `HTML` view, see [the example
below](https://github.com/swiftwasm/Tokamak#arbitrary-html)), so pull requests are very welcome! Don't
forget to check [the "Contributing" section](https://github.com/swiftwasm/Tokamak#contributing) first.
[issues](https://github.com/swiftwasm/Tokamak/issues) and
[PRs](https://github.com/swiftwasm/Tokamak/pulls) to get more details about the current status, or
[create a new issue](https://github.com/swiftwasm/Tokamak/issues/new) to let us prioritize the
development based on the demand. We also try to make the development of views and modifiers easier
(with the help from the `HTML` view, see [the example
below](https://github.com/swiftwasm/Tokamak#arbitrary-html)), so pull requests are very welcome!
Don't forget to check [the "Contributing"
section](https://github.com/swiftwasm/Tokamak#contributing) first.
If you'd like to participate in the growing [SwiftWasm](https://swiftwasm.org) community, you're also very
welcome to join the `#webassembly` channel in [the SwiftPM Slack](https://swift-package-manager.herokuapp.com/).
If you'd like to participate in the growing [SwiftWasm](https://swiftwasm.org) community, you're
also very welcome to join the `#webassembly` channel in [the SwiftPM
Slack](https://swift-package-manager.herokuapp.com/).
### Example code
@ -169,34 +172,33 @@ will build the demo app that shows almost all of the currently implemented APIs.
### Modular structure
Tokamak is built with modularity in mind, providing a cross-platform `TokamakCore` module and
separate modules for platform-specific renderers. Currently, the only available renderer module
is `TokamakDOM`, but we intend to provide other renderers in the future, such as `TokamakHTML`
for static websites and server-side rendering. Tokamak users only need to import a renderer module
they would like to use, while `TokamakCore` is hidden as an "internal" `Tokamak` package target.
Unfortunately, Swift does not allow us to specify that certain symbols in `TokamakCore` are private
to a package, but they need to stay `public` for renderer modules to get access to them. Thus, the
current workaround is to mark those symbols with underscores in their names to indicate this. It
can be formulated as these "rules":
separate modules for platform-specific renderers. Currently, the only available renderer modules are
`TokamakDOM` and `TokamakStaticHTML`, the latter can be used for static websites and server-side
rendering. If you'd like to implement your own custom renderer, please refer to our [renderers
guide](docs/RenderersGuide.md) for more details.
1. If a symbol is restricted to a module and has no `public` access control, no need for an underscore.
2. If a symbol is part of a public renderer module API (e.g. `TokamakDOM`), no need for an underscore,
users may use those symbols directly, and it is re-exported from `TokamakCore` by the renderer module
via `public typealias`.
3. If a function or a type have `public` on them only by necessity to make them available in `TokamakDOM`,
but unavailable to users (or not intended for public use), underscore is needed to indicate that.
Tokamak users only need to import a renderer module they would like to use, while
`TokamakCore` is hidden as an "internal" `Tokamak` package target. Unfortunately, Swift does not
allow us to specify that certain symbols in `TokamakCore` are private to a package, but they need to
stay `public` for renderer modules to get access to them. Thus, the current workaround is to mark
those symbols with underscores in their names to indicate this. It can be formulated as these
"rules":
The benefit of separate modules is that they allow us to provide separate renderers for different platforms.
Users can pick and choose what they want to use, e.g. purely static websites would use only `TokamakHTML`,
single-page apps would use `TokamakDOM`, maybe in conjuction with `TokamakHTML` for pre-rendering. As we'd
like to try to implement a native renderer for Android at some point, probably in a separate `TokamakAndroid`
module, Android apps would use `TokamakAndroid` with no need to be aware of any of the web modules.
1. If a symbol is restricted to a module and has no `public` access control, no need for an
underscore.
2. If a symbol is part of a public renderer module API (e.g. `TokamakDOM`), no need for an
underscore, users may use those symbols directly, and it is re-exported from `TokamakCore` by the
renderer module via `public typealias`.
3. If a function or a type have `public` on them only by necessity to make them available in
`TokamakDOM`, but unavailable to users (or not intended for public use), underscore is needed to
indicate that.
### Sponsorship
If this library saved you any amount of time or money, please consider [sponsoring
the work of its maintainer](https://github.com/sponsors/MaxDesiatov). While some of the
sponsorship tiers give you priority support or even consulting time, any amount is
appreciated and helps in maintaining the project.
The benefit of separate modules is that they allow us to provide separate renderers for different
platforms. Users can pick and choose what they want to use, e.g. purely static websites would use
only `TokamakStaticHTML`, single-page apps would use `TokamakDOM`, maybe in conjuction with
`TokamakStaticHTML` for pre-rendering. As we'd like to try to implement a native renderer for
Android at some point, probably in a separate `TokamakAndroid` module, Android apps would use
`TokamakAndroid` with no need to be aware of any of the web modules.
### Coding Style
@ -232,6 +234,13 @@ Conduct](https://github.com/swiftwasm/Tokamak/blob/main/CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report
unacceptable behavior to conduct@tokamak.dev.
### Sponsorship
If this library saved you any amount of time or money, please consider [sponsoring
the work of its maintainer](https://github.com/sponsors/MaxDesiatov). While some of the
sponsorship tiers give you priority support or even consulting time, any amount is
appreciated and helps in maintaining the project.
## Maintainers
[Carson Katri](https://github.com/carson-katri),

View File

@ -1,19 +0,0 @@
# `Renderers` in Tokamak
Tokamak is a flexible library. `TokamakCore` provides the SwiftUI-API, which your `Renderer` can use
to construct a representation of `Views` that your platform understands.
To explain the creation of `Renderers`, well be creating a simple one: `TokamakStaticHTML` (which
you can find in the `Tokamak` repository).
Before we create the `Renderer`, we need to understand the requirements of our platform:
1. Stateful apps cannot be created This simplifies the scope of our project, as we only have to
render once. However, if you are building a `Renderer` that supports state changes, the process
is largely the same. `TokamakCore`s `StackReconciler` will let your `Renderer` know when a
`View` has to be redrawn.
2. HTML should be rendered `TokamakDOM` provides HTML representations of many `Views`, so we can
utilize it. However, we will cover how to provide custom `View` bodies your `Renderer` can
understand, and when you are required to do so.
And thats it! In the next part well go more in depth on `Renderers`.

View File

@ -1,17 +0,0 @@
# Understanding `Renderers`
So, what goes into a `Renderer`?
1. A `Target` - Targets are the destination for rendered `Views`. For instance, on iOS this is
`UIView`, on macOS an `NSView`, and on the web we render to DOM nodes.
2. A `StackReconciler` - The reconciler does all the heavy lifting to understand the view tree. It
notifies your `Renderer` of what views need to be mounted/unmounted.
3. `func mountTarget`- This function is called when a new target instance should be created and
added to the parent (either as a subview or some other way, e.g. installed if its a layout
constraint).
4. `func update` - This function is called when an existing target instance should be updated (e.g.
when `State` changes).
5. `func unmount` - This function is called when an existing target instance should be unmounted:
removed from the parent and most likely destroyed.
Thats it! Lets get our project setup.

View File

@ -1,80 +0,0 @@
# `TokamakStaticHTML` Setup
Every `Renderer` can choose what `Views`, `ViewModifiers`, property wrappers, etc. are available to
use. A `Core.swift` file is used to reexport these symbols. For `TokamakStaticHTML`, well use the
following `Core.swift` file:
```swift
import TokamakCore
// MARK: Environment & State
public typealias Environment = TokamakCore.Environment
// MARK: Modifiers & Styles
public typealias ViewModifier = TokamakCore.ViewModifier
public typealias ModifiedContent = TokamakCore.ModifiedContent
public typealias DefaultListStyle = TokamakCore.DefaultListStyle
public typealias PlainListStyle = TokamakCore.PlainListStyle
public typealias InsetListStyle = TokamakCore.InsetListStyle
public typealias GroupedListStyle = TokamakCore.GroupedListStyle
public typealias InsetGroupedListStyle = TokamakCore.InsetGroupedListStyle
// MARK: Shapes
public typealias Shape = TokamakCore.Shape
public typealias Capsule = TokamakCore.Capsule
public typealias Circle = TokamakCore.Circle
public typealias Ellipse = TokamakCore.Ellipse
public typealias Path = TokamakCore.Path
public typealias Rectangle = TokamakCore.Rectangle
public typealias RoundedRectangle = TokamakCore.RoundedRectangle
// MARK: Primitive values
public typealias Color = TokamakCore.Color
public typealias Font = TokamakCore.Font
public typealias CGAffineTransform = TokamakCore.CGAffineTransform
public typealias CGPoint = TokamakCore.CGPoint
public typealias CGRect = TokamakCore.CGRect
public typealias CGSize = TokamakCore.CGSize
// MARK: Views
public typealias Divider = TokamakCore.Divider
public typealias ForEach = TokamakCore.ForEach
public typealias GridItem = TokamakCore.GridItem
public typealias Group = TokamakCore.Group
public typealias HStack = TokamakCore.HStack
public typealias LazyHGrid = TokamakCore.LazyHGrid
public typealias LazyVGrid = TokamakCore.LazyVGrid
public typealias List = TokamakCore.List
public typealias ScrollView = TokamakCore.ScrollView
public typealias Section = TokamakCore.Section
public typealias Spacer = TokamakCore.Spacer
public typealias Text = TokamakCore.Text
public typealias VStack = TokamakCore.VStack
public typealias ZStack = TokamakCore.ZStack
// MARK: Special Views
public typealias View = TokamakCore.View
public typealias AnyView = TokamakCore.AnyView
public typealias EmptyView = TokamakCore.EmptyView
// MARK: Misc
// Note: This extension is required to support concatenation of `Text`.
extension Text {
public static func + (lhs: Self, rhs: Self) -> Self {
_concatenating(lhs: lhs, rhs: rhs)
}
}
```
Weve omitted any stateful `Views`, as well as property wrappers used to modify state.

View File

@ -1,45 +0,0 @@
# Building the `Target`
If you recall, we defined a `Target` as:
> the destination for rendered `Views`
In `TokamakStaticHTML`, this would be a tag in an `HTML` file. A tag has several properties,
although we dont need to worry about all of them. For now, we can consider a tag to have:
- The HTML for the tag itself (outer HTML)
- Child tags (inner HTML)
We can describe our target simply:
```swift
public final class HTMLTarget: Target {
var html: AnyHTML
var children: [HTMLTarget] = []
init<V: View>(_ view: V,
_ html: AnyHTML) {
self.html = html
super.init(view)
}
}
```
`AnyHTML` is from `TokamakDOM`, which you can declare as a dependency. The target stores the `View`
it hosts, the `HTML` that represents it, and its child elements.
Lastly, we can also provide an HTML string representation of the target:
```swift
extension HTMLTarget {
var outerHTML: String {
"""
<\(html.tag)\(html.attributes.isEmpty ? "" : " ")\
\(html.attributes.map { #"\#($0)="\#($1)""# }.joined(separator: " "))>\
\(html.innerHTML ?? "")\
\(children.map(\.outerHTML).joined(separator: "\n"))\
</\(html.tag)>
"""
}
}
```

View File

@ -1,134 +0,0 @@
# Building the `Renderer`
Now that we have a `Target`, we can start the `Renderer`:
```swift
public final class StaticHTMLRenderer: Renderer {
public private(set) var reconciler: StackReconciler<StaticHTMLRenderer>?
var rootTarget: HTMLTarget
public var html: String {
"""
<html>
\(rootTarget.outerHTML)
</html>
"""
}
}
```
We start by declaring the `StackReconciler`. It will handle the app, while our `Renderer` can focus
on mounting and un-mounting `Views`.
```swift
...
public init<V: View>(_ view: V) {
rootTarget = HTMLTarget(view, HTMLBody())
reconciler = StackReconciler(
view: view,
target: rootTarget,
renderer: self,
environment: EnvironmentValues()
) { closure in
fatalError("Stateful apps cannot be created with TokamakStaticHTML")
}
}
```
Next we declare an initializer that takes a `View` and builds a reconciler. The reconciler takes the
`View`, our root `Target` (in this case, `HTMLBody`), the renderer (`self`), and any default
`EnvironmentValues` we may need to setup. The closure at the end is the scheduler. It tells the
reconciler when it can update. In this case, we wont need to update, so we can crash.
`HTMLBody` is declared like so:
```swift
struct HTMLBody: AnyHTML {
let tag: String = "body"
let innerHTML: String? = nil
let attributes: [String : String] = [:]
let listeners: [String : Listener] = [:]
}
```
## Mounting
Now that we have a reconciler, we need to be able to mount the `HTMLTargets` it asks for.
```swift
public func mountTarget(to parent: HTMLTarget, with host: MountedHost) -> HTMLTarget? {
// 1.
guard let html = mapAnyView(
host.view,
transform: { (html: AnyHTML) in html }
) else {
// 2.
if mapAnyView(host.view, transform: { (view: ParentView) in view }) != nil {
return parent
}
return nil
}
// 3.
let node = HTMLTarget(host.view, html)
parent.children.append(node)
return node
}}
```
1. We use the `mapAnyView` function to convert the `AnyView` passed in to `AnyHTML`, which can be
used with our `HTMLTarget`.
2. `ParentView` is a special type of `View` in Tokamak. It indicates that the view has no
representation itself, and is purely a container for children (e.g. `ForEach` or `Group`).
3. We create a new `HTMLTarget` for the view, assign it as a child of the parent, and return it.
The other two functions required by the `Renderer` protocol can crash, as `TokamakStaticHTML`
doesnt support state changes:
```swift
public func update(target: HTMLTarget, with host: MountedHost) {
fatalError("Stateful apps cannot be created with TokamakStaticHTML")
}
public func unmount(
target: HTMLTarget,
from parent: HTMLTarget,
with host: MountedHost,
completion: @escaping () -> ()
) {
fatalError("Stateful apps cannot be created with TokamakStaticHTML")
}
```
If you are creating a `Renderer` that supports state changes, heres a quick synopsis:
- `func update` - Mutate the `target` to match the `host`.
- `func unmount` - Remove the `target` from the `parent`, and call `completion` once it has been
removed.
Now that we can mount, lets give it a try:
```swift
struct ContentView : View {
var body: some View {
Text("Hello, world!")
}
}
let renderer = StaticHTMLRenderer(ContentView())
print(renderer.html)
```
This spits out:
```html
<html>
<body>
<span style="...">Hello, world!</span>
</body>
</html>
```
Congratulations 🎉 You successfully wrote a `Renderer`. We cant wait to see what platforms youll
bring Tokamak to.

View File

@ -1,61 +0,0 @@
# Providing platform-specific primitives
Primitive `Views`, such as `Text`, `Button`, `HStack`, etc. have a body type of `Never`. When the
`StackReconciler` goes to render these `Views`, it expects your `Renderer` to provide a body.
This is done via the `ViewDeferredToRenderer` protocol. There we can provide a `View` that our
`Renderer` understands. For instance, `TokamakDOM` (and `TokamakStaticHTML` by extension) use the
`HTML` view. Lets look at a simpler version of this view:
```swift
protocol AnyHTML {
let tag: String
let attributes: [String:String]
let innerHTML: String
}
struct HTML: View, AnyHTML {
let tag: String
let attributes: [String:String]
let innerHTML: String
var body: Never {
neverBody("HTML")
}
}
```
Here we define an `HTML` view to have a body type of `Never`, like other primitive `Views`. It also
conforms to `AnyHTML`, which allows our `Renderer` to access the attributes of the `HTML` without
worrying about the `associatedtypes` involved with `View`.
## `ViewDeferredToRenderer`
Now we can use `HTML` to override the body of the primitive `Views` provided by `TokamakCore`:
```swift
extension Text: ViewDeferredToRenderer {
var deferredBody: AnyView {
AnyView(HTML("span", [:], _TextProxy(self).rawText))
}
}
```
If you recall, our `Renderer` mapped the `AnyView` received from the reconciler to `AnyHTML`:
```swift
// 1.
guard let html = mapAnyView(
host.view,
transform: { (html: AnyHTML) in html }
) else { ... }
```
Then we were able to access the properties of the HTML.
## Proxies
Proxies allow access to internal properties of views implemented by `TokamakCore`. For instance, to
access the storage of the `Text` view, we were required to use a `_TextProxy`.
Proxies contain all of the properties of the primitive necessary to build your platform-specific
implementation.

363
docs/RenderersGuide.md Normal file
View File

@ -0,0 +1,363 @@
# `Renderers` in Tokamak
**Author: [@carson-katri](https://github.com/carson-katri)**
Tokamak is a flexible library. `TokamakCore` provides the SwiftUI API, which your `Renderer` can use
to construct a representation of `Views` that your platform understands.
To explain the creation of `Renderers`, well be creating a simple one: `TokamakStaticHTML` (which
you can find in the `Tokamak` repository).
Before we create the `Renderer`, we need to understand the requirements of our platform:
1. Stateful apps cannot be created. This simplifies the scope of our project, as we only have to
render once. However, if you are building a `Renderer` that supports state changes, the process
is largely the same. `TokamakCore`s `StackReconciler` will let your `Renderer` know when a
`View` has to be redrawn.
2. HTML should be rendered. `TokamakDOM` provides HTML representations of many `Views`, so we can
utilize it. However, we will cover how to provide custom `View` bodies your `Renderer` can
understand, and when you are required to do so.
And thats it! In the next part well go more in depth on `Renderers`.
## Understanding `Renderers`
So, what goes into a `Renderer`?
1. A `Target` - Targets are the destination for rendered `Views`. For instance, on iOS this is
`UIView`, on macOS an `NSView`, and on the web we render to DOM nodes.
2. A `StackReconciler` - The reconciler does all the heavy lifting to understand the view tree. It
notifies your `Renderer` of what views need to be mounted/unmounted.
3. `func mountTarget`- This function is called when a new target instance should be created and
added to the parent (either as a subview or some other way, e.g. installed if its a layout
constraint).
4. `func update` - This function is called when an existing target instance should be updated (e.g.
when `State` changes).
5. `func unmount` - This function is called when an existing target instance should be unmounted:
removed from the parent and most likely destroyed.
Thats it! Lets get our project set up.
## `TokamakStaticHTML` Setup
Every `Renderer` can choose what `Views`, `ViewModifiers`, property wrappers, etc. are available to
use. A `Core.swift` file is used to re-export these symbols. For `TokamakStaticHTML`, well use the
following `Core.swift` file:
```swift
import TokamakCore
// MARK: Environment & State
public typealias Environment = TokamakCore.Environment
// MARK: Modifiers & Styles
public typealias ViewModifier = TokamakCore.ViewModifier
public typealias ModifiedContent = TokamakCore.ModifiedContent
public typealias DefaultListStyle = TokamakCore.DefaultListStyle
public typealias PlainListStyle = TokamakCore.PlainListStyle
public typealias InsetListStyle = TokamakCore.InsetListStyle
public typealias GroupedListStyle = TokamakCore.GroupedListStyle
public typealias InsetGroupedListStyle = TokamakCore.InsetGroupedListStyle
// MARK: Shapes
public typealias Shape = TokamakCore.Shape
public typealias Capsule = TokamakCore.Capsule
public typealias Circle = TokamakCore.Circle
public typealias Ellipse = TokamakCore.Ellipse
public typealias Path = TokamakCore.Path
public typealias Rectangle = TokamakCore.Rectangle
public typealias RoundedRectangle = TokamakCore.RoundedRectangle
// MARK: Primitive values
public typealias Color = TokamakCore.Color
public typealias Font = TokamakCore.Font
public typealias CGAffineTransform = TokamakCore.CGAffineTransform
public typealias CGPoint = TokamakCore.CGPoint
public typealias CGRect = TokamakCore.CGRect
public typealias CGSize = TokamakCore.CGSize
// MARK: Views
public typealias Divider = TokamakCore.Divider
public typealias ForEach = TokamakCore.ForEach
public typealias GridItem = TokamakCore.GridItem
public typealias Group = TokamakCore.Group
public typealias HStack = TokamakCore.HStack
public typealias LazyHGrid = TokamakCore.LazyHGrid
public typealias LazyVGrid = TokamakCore.LazyVGrid
public typealias List = TokamakCore.List
public typealias ScrollView = TokamakCore.ScrollView
public typealias Section = TokamakCore.Section
public typealias Spacer = TokamakCore.Spacer
public typealias Text = TokamakCore.Text
public typealias VStack = TokamakCore.VStack
public typealias ZStack = TokamakCore.ZStack
// MARK: Special Views
public typealias View = TokamakCore.View
public typealias AnyView = TokamakCore.AnyView
public typealias EmptyView = TokamakCore.EmptyView
// MARK: Misc
// Note: This extension is required to support concatenation of `Text`.
extension Text {
public static func + (lhs: Self, rhs: Self) -> Self {
_concatenating(lhs: lhs, rhs: rhs)
}
}
```
Weve omitted any stateful `Views`, as well as property wrappers used to modify state.
## Building the `Target`
If you recall, we defined a `Target` as:
> the destination for rendered `Views`
In `TokamakStaticHTML`, this would be a tag in an `HTML` file. A tag has several properties,
although we dont need to worry about all of them. For now, we can consider a tag to have:
- The HTML for the tag itself (outer HTML)
- Child tags (inner HTML)
We can describe our target simply:
```swift
public final class HTMLTarget: Target {
var html: AnyHTML
var children: [HTMLTarget] = []
init<V: View>(_ view: V,
_ html: AnyHTML) {
self.html = html
super.init(view)
}
}
```
`AnyHTML` type is coming from `TokamakDOM`, which you can declare as a dependency. The target stores
the `View` it hosts, the `HTML` that represents it, and its child elements.
Lastly, we can also provide an HTML string representation of the target:
```swift
extension HTMLTarget {
var outerHTML: String {
"""
<\(html.tag)\(html.attributes.isEmpty ? "" : " ")\
\(html.attributes.map { #"\#($0)="\#($1)""# }.joined(separator: " "))>\
\(html.innerHTML ?? "")\
\(children.map(\.outerHTML).joined(separator: "\n"))\
</\(html.tag)>
"""
}
}
```
## Building the `Renderer`
Now that we have a `Target`, we can start the `Renderer`:
```swift
public final class StaticHTMLRenderer: Renderer {
public private(set) var reconciler: StackReconciler<StaticHTMLRenderer>?
var rootTarget: HTMLTarget
public var html: String {
"""
<html>
\(rootTarget.outerHTML)
</html>
"""
}
}
```
We start by declaring the `StackReconciler`. It will handle the app, while our `Renderer` can focus
on mounting and un-mounting `Views`.
```swift
...
public init<V: View>(_ view: V) {
rootTarget = HTMLTarget(view, HTMLBody())
reconciler = StackReconciler(
view: view,
target: rootTarget,
renderer: self,
environment: EnvironmentValues()
) { closure in
fatalError("Stateful apps cannot be created with TokamakStaticHTML")
}
}
```
Next we declare an initializer that takes a `View` and builds a reconciler. The reconciler takes the
`View`, our root `Target` (in this case, `HTMLBody`), the renderer (`self`), and any default
`EnvironmentValues` we may need to setup. The closure at the end is the scheduler. It tells the
reconciler when it can update. In this case, we wont need to update, so we can crash.
`HTMLBody` is declared like so:
```swift
struct HTMLBody: AnyHTML {
let tag: String = "body"
let innerHTML: String? = nil
let attributes: [String : String] = [:]
let listeners: [String : Listener] = [:]
}
```
### Mounting
Now that we have a reconciler, we need to be able to mount the `HTMLTargets` it asks for.
```swift
public func mountTarget(to parent: HTMLTarget, with host: MountedHost) -> HTMLTarget? {
// 1.
guard let html = mapAnyView(
host.view,
transform: { (html: AnyHTML) in html }
) else {
// 2.
if mapAnyView(host.view, transform: { (view: ParentView) in view }) != nil {
return parent
}
return nil
}
// 3.
let node = HTMLTarget(host.view, html)
parent.children.append(node)
return node
}}
```
1. We use the `mapAnyView` function to convert the `AnyView` passed in to `AnyHTML`, which can be
used with our `HTMLTarget`.
2. `ParentView` is a special type of `View` in Tokamak. It indicates that the view has no
representation itself, and is purely a container for children (e.g. `ForEach` or `Group`).
3. We create a new `HTMLTarget` for the view, assign it as a child of the parent, and return it.
The other two functions required by the `Renderer` protocol can crash, as `TokamakStaticHTML`
doesnt support state changes:
```swift
public func update(target: HTMLTarget, with host: MountedHost) {
fatalError("Stateful apps cannot be created with TokamakStaticHTML")
}
public func unmount(
target: HTMLTarget,
from parent: HTMLTarget,
with host: MountedHost,
completion: @escaping () -> ()
) {
fatalError("Stateful apps cannot be created with TokamakStaticHTML")
}
```
If you are creating a `Renderer` that supports state changes, heres a quick synopsis:
- `func update` - Mutate the `target` to match the `host`.
- `func unmount` - Remove the `target` from the `parent`, and call `completion` once it has been
removed.
Now that we can mount, lets give it a try:
```swift
struct ContentView : View {
var body: some View {
Text("Hello, world!")
}
}
let renderer = StaticHTMLRenderer(ContentView())
print(renderer.html)
```
This spits out:
```html
<html>
<body>
<span style="...">Hello, world!</span>
</body>
</html>
```
Congratulations 🎉 You successfully wrote a `Renderer`. We cant wait to see what platforms youll
bring Tokamak to.
## Providing platform-specific primitives
Primitive `Views`, such as `Text`, `Button`, `HStack`, etc. have a body type of `Never`. When the
`StackReconciler` goes to render these `Views`, it expects your `Renderer` to provide a body.
This is done via the `ViewDeferredToRenderer` protocol. There we can provide a `View` that our
`Renderer` understands. For instance, `TokamakDOM` (and `TokamakStaticHTML` by extension) use the
`HTML` view. Lets look at a simpler version of this view:
```swift
protocol AnyHTML {
let tag: String
let attributes: [String:String]
let innerHTML: String
}
struct HTML: View, AnyHTML {
let tag: String
let attributes: [String:String]
let innerHTML: String
var body: Never {
neverBody("HTML")
}
}
```
Here we define an `HTML` view to have a body type of `Never`, like other primitive `Views`. It also
conforms to `AnyHTML`, which allows our `Renderer` to access the attributes of the `HTML` without
worrying about the `associatedtypes` involved with `View`.
### `ViewDeferredToRenderer`
Now we can use `HTML` to override the body of the primitive `Views` provided by `TokamakCore`:
```swift
extension Text: ViewDeferredToRenderer {
var deferredBody: AnyView {
AnyView(HTML("span", [:], _TextProxy(self).rawText))
}
}
```
If you recall, our `Renderer` mapped the `AnyView` received from the reconciler to `AnyHTML`:
```swift
// 1.
guard let html = mapAnyView(
host.view,
transform: { (html: AnyHTML) in html }
) else { ... }
```
Then we were able to access the properties of the HTML.
### Proxies
Proxies allow access to internal properties of views implemented by `TokamakCore`. For instance, to
access the storage of the `Text` view, we were required to use a `_TextProxy`.
Proxies contain all of the properties of the primitive necessary to build your platform-specific
implementation.