Tokamak/Sources/TokamakStaticHTML/StaticRenderer.swift

146 lines
3.6 KiB
Swift

// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/20/20.
//
import TokamakCore
extension EnvironmentValues {
/// Returns default settings for the static HTML environment
static var defaultEnvironment: Self {
var environment = EnvironmentValues()
environment[_ColorSchemeKey] = .light
return environment
}
}
public final class HTMLTarget: Target {
var html: AnyHTML
var children: [HTMLTarget] = []
public var view: AnyView
init<V: View>(_ view: V, _ html: AnyHTML) {
self.html = html
self.view = AnyView(view)
}
init(_ html: AnyHTML) {
self.html = html
view = AnyView(EmptyView())
}
}
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)>
"""
}
}
struct HTMLBody: AnyHTML {
let tag: String = "body"
let innerHTML: String? = nil
let attributes: [String: String] = [
"style": "margin: 0;" + rootNodeStyles,
]
}
public final class StaticHTMLRenderer: Renderer {
public private(set) var reconciler: StackReconciler<StaticHTMLRenderer>?
var rootTarget: HTMLTarget
static var title: String = ""
public var html: String {
"""
<html>
<head>
<title>\(Self.title)</title>
<style>
\(tokamakStyles)
</style>
</head>
\(rootTarget.outerHTML)
</html>
"""
}
public init<V: View>(_ view: V, _ rootEnvironment: EnvironmentValues? = nil) {
rootTarget = HTMLTarget(view, HTMLBody())
reconciler = StackReconciler(
view: view,
target: rootTarget,
environment: .defaultEnvironment,
renderer: self,
scheduler: { _ in
fatalError("Stateful apps cannot be created with TokamakStaticHTML")
}
)
}
public init<A: App>(_ app: A, _ rootEnvironment: EnvironmentValues? = nil) {
rootTarget = HTMLTarget(HTMLBody())
reconciler = StackReconciler(
app: app,
target: rootTarget,
environment: .defaultEnvironment,
renderer: self,
scheduler: { _ in
fatalError("Stateful apps cannot be created with TokamakStaticHTML")
}
)
}
public func mountTarget(to parent: HTMLTarget, with host: MountedHost) -> HTMLTarget? {
guard let html = mapAnyView(
host.view,
transform: { (html: AnyHTML) in html }
) else {
// handle cases like `TupleView`
if mapAnyView(host.view, transform: { (view: ParentView) in view }) != nil {
return parent
}
return nil
}
let node = HTMLTarget(host.view, html)
parent.children.append(node)
return node
}
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")
}
}