Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support meta tags in StaticHTMLRenderer #483

Merged
merged 19 commits into from
May 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions Sources/TokamakCore/MountedViews/MountedCompositeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,6 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {

if let preferenceModifier = self.view.view as? _PreferenceWritingViewProtocol {
self.view = preferenceModifier.modifyPreferenceStore(&self.preferenceStore)
if let parent = parent {
parent.preferenceStore.merge(with: self.preferenceStore)
}
}

if let preferenceReader = self.view.view as? _PreferenceReadingViewProtocol {
Expand Down
17 changes: 10 additions & 7 deletions Sources/TokamakCore/MountedViews/MountedElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,9 @@ public class MountedElement<R: Renderer> {

public internal(set) var environmentValues: EnvironmentValues

weak var parent: MountedElement<R>?
/// `didSet` on this field propagates the preference changes up the view tree.
var preferenceStore: _PreferenceStore = .init() {
didSet {
parent?.preferenceStore.merge(with: preferenceStore)
}
}
private(set) weak var parent: MountedElement<R>?

var preferenceStore: _PreferenceStore = .init()

public internal(set) var viewTraits: _ViewTraitStore

Expand All @@ -110,6 +106,7 @@ public class MountedElement<R: Renderer> {
self.environmentValues = environmentValues
viewTraits = .init()
updateEnvironment()
connectParentPreferenceStore()
}

init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
Expand All @@ -118,6 +115,7 @@ public class MountedElement<R: Renderer> {
self.environmentValues = environmentValues
viewTraits = .init()
updateEnvironment()
connectParentPreferenceStore()
}

init(
Expand All @@ -131,6 +129,7 @@ public class MountedElement<R: Renderer> {
self.environmentValues = environmentValues
self.viewTraits = viewTraits
updateEnvironment()
connectParentPreferenceStore()
}

func updateEnvironment() {
Expand All @@ -145,6 +144,10 @@ public class MountedElement<R: Renderer> {
}
}

func connectParentPreferenceStore() {
preferenceStore.parent = parent?.preferenceStore
}

/// You must call `super.prepareForMount` before all other mounting work.
func prepareForMount(with transaction: Transaction) {
// `GroupView`'s don't really mount, so let their children transition if the group can.
Expand Down
19 changes: 5 additions & 14 deletions Sources/TokamakCore/Preferences/PreferenceKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ public extension _PreferenceValue {
}
}

public struct _PreferenceStore {
public final class _PreferenceStore {
/// The backing values of the `_PreferenceStore`.
private var values: [String: Any]

weak var parent: _PreferenceStore?

public init(values: [String: Any] = [:]) {
self.values = values
}
Expand All @@ -63,23 +65,12 @@ public struct _PreferenceStore {
?? _PreferenceValue(valueList: [Key.defaultValue])
}

public mutating func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
public func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
where Key: PreferenceKey
{
let previousValues = self.value(forKey: key).valueList
values[String(reflecting: key)] = _PreferenceValue<Key>(valueList: previousValues + [value])
}

public mutating func merge(with other: Self) {
self = merging(with: other)
}

public func merging(with other: Self) -> Self {
var result = values
for (key, value) in other.values {
result[key] = value
}
return .init(values: result)
parent?.insert(value, forKey: key)
}
}

Expand Down
6 changes: 6 additions & 0 deletions Sources/TokamakCore/StackReconciler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ public final class StackReconciler<R: Renderer> {
*/
public let rootTarget: R.TargetType

/** A root renderer's main preference store.
*/
public var preferenceStore: _PreferenceStore {
rootElement.preferenceStore
}

/** A root of the mounted elements tree to which all other mounted elements are attached to.
*/
private let rootElement: MountedElement<R>
Expand Down
2 changes: 1 addition & 1 deletion Sources/TokamakCore/Views/Containers/Group.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public struct Group<Content> {
}
}

extension Group: _PrimitiveView & View where Content: View {}
extension Group: _PrimitiveView, View where Content: View {}

extension Group: ParentView where Content: View {
@_spi(TokamakCore)
Expand Down
2 changes: 1 addition & 1 deletion Sources/TokamakStaticHTML/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public extension App {
}

static func _setTitle(_ title: String) {
StaticHTMLRenderer.title = title
// no-op: use Title view
}

var _phasePublisher: AnyPublisher<ScenePhase, Never> {
Expand Down
37 changes: 37 additions & 0 deletions Sources/TokamakStaticHTML/Preferences/Preferences.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2022 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 Andrew Barba on 5/20/22.
//

import TokamakCore

public struct HTMLTitlePreferenceKey: PreferenceKey {
public static var defaultValue: String = ""

public static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}

public struct HTMLMetaPreferenceKey: PreferenceKey {
public static var defaultValue: [HTMLMeta.MetaTag] = []

public static func reduce(
value: inout [HTMLMeta.MetaTag],
nextValue: () -> [HTMLMeta.MetaTag]
) {
value += nextValue()
}
}
26 changes: 24 additions & 2 deletions Sources/TokamakStaticHTML/StaticHTMLRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,40 @@ struct HTMLBody: AnyHTML {
]
}

extension HTMLMeta.MetaTag {
func outerHTML() -> String {
switch self {
case let .charset(charset):
return #"<meta charset="\#(charset)">"#
case let .name(name, content):
return #"<meta name="\#(name)" content="\#(content)">"#
case let .property(property, content):
return #"<meta property="\#(property)" content="\#(content)">"#
case let .httpEquiv(httpEquiv, content):
return #"<meta http-equiv="\#(httpEquiv)" content="\#(content)">"#
}
}
}

public final class StaticHTMLRenderer: Renderer {
private var reconciler: StackReconciler<StaticHTMLRenderer>?

var rootTarget: HTMLTarget

static var title: String = ""
var title: String {
reconciler?.preferenceStore.value(forKey: HTMLTitlePreferenceKey.self).value ?? ""
}

var meta: [HTMLMeta.MetaTag] {
reconciler?.preferenceStore.value(forKey: HTMLMetaPreferenceKey.self).value ?? []
}

public func render(shouldSortAttributes: Bool = false) -> String {
"""
<html>
<head>
<title>\(Self.title)</title>
<title>\(title)</title>
\(meta.map { $0.outerHTML() }.joined(separator: "\n "))
<style>
\(tokamakStyles)
</style>
Expand Down
83 changes: 83 additions & 0 deletions Sources/TokamakStaticHTML/Views/Head/Meta.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2022 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 Andrew Barba on 5/20/22.
//

import TokamakCore

public struct HTMLMeta: View {
public enum MetaTag: Equatable, Hashable {
case charset(_ charset: String)
case name(_ name: String, content: String)
case property(_ property: String, content: String)
case httpEquiv(_ httpEquiv: String, content: String)
}

var meta: MetaTag

public init(_ value: MetaTag) {
meta = value
}

public init(charset: String) {
meta = .charset(charset)
}

public init(name: String, content: String) {
meta = .name(name, content: content)
}

public init(property: String, content: String) {
meta = .property(property, content: content)
}

public init(httpEquiv: String, content: String) {
meta = .httpEquiv(httpEquiv, content: content)
}

public var body: some View {
EmptyView()
.preference(key: HTMLMetaPreferenceKey.self, value: [meta])
}
}

public extension View {
func htmlMeta(_ value: HTMLMeta.MetaTag) -> some View {
htmlMeta(.init(value))
}

func htmlMeta(charset: String) -> some View {
htmlMeta(.init(charset: charset))
}

func htmlMeta(name: String, content: String) -> some View {
htmlMeta(.init(name: name, content: content))
}

func htmlMeta(property: String, content: String) -> some View {
htmlMeta(.init(property: property, content: content))
}

func htmlMeta(httpEquiv: String, content: String) -> some View {
htmlMeta(.init(httpEquiv: httpEquiv, content: content))
}

func htmlMeta(_ meta: HTMLMeta) -> some View {
Group {
self
meta
}
}
}
44 changes: 44 additions & 0 deletions Sources/TokamakStaticHTML/Views/Head/Title.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2022 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 Andrew Barba on 5/20/22.
//

import TokamakCore

public struct HTMLTitle: View {
var title: String

public init(_ title: String) {
self.title = title
}

public var body: some View {
EmptyView()
.preference(key: HTMLTitlePreferenceKey.self, value: title)
}
}

public extension View {
func htmlTitle(_ title: String) -> some View {
htmlTitle(.init(title))
}

func htmlTitle(_ title: HTMLTitle) -> some View {
Group {
self
title
}
}
}
Loading