From 57ec6135253289bcc6773d8edc0a9bdefd81c508 Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Tue, 20 Feb 2024 21:54:48 +0100 Subject: [PATCH] Prepare for release (#145) * Make the features flag available to vapor * Tidy up a bit * Silence deprecation warning of nsdictionary * Update readme * Silence warning of shadowing type parameters --- README.md | 2 +- .../Environment/EnvironmentObject.swift | 14 +-- .../Framework/Localization/Localization.swift | 15 ++- .../Framework/Rendering/Features.swift | 11 ++ .../Rendering/Renderer+FeatureSet.swift | 14 --- .../Framework/Rendering/Renderer.swift | 100 +++++++++--------- .../HTMLKit/Framework/Security/Security.swift | 2 +- Sources/HTMLKitVapor/Configuration.swift | 26 +++++ .../Extensions/Vapor+HTMLKit.swift | 75 ++++++------- Sources/HTMLKitVapor/ViewRenderer.swift | 11 +- Tests/HTMLKitTests/PerformanceTests.swift | 3 +- Tests/HTMLKitTests/RenderingTests.swift | 18 +--- Tests/HTMLKitTests/SecurityTests.swift | 4 +- Tests/HTMLKitVaporTests/ProviderTests.swift | 44 +++++++- 14 files changed, 195 insertions(+), 144 deletions(-) create mode 100644 Sources/HTMLKit/Framework/Rendering/Features.swift delete mode 100644 Sources/HTMLKit/Framework/Rendering/Renderer+FeatureSet.swift create mode 100644 Sources/HTMLKitVapor/Configuration.swift diff --git a/README.md b/README.md index 2a4cec96..80e8e24d 100644 --- a/README.md +++ b/README.md @@ -23,4 +23,4 @@ The HTMLKit framework consists of a list of features to create HTML templates in - **UI components** - Construct user interfaces with the components library. - **Symbol bundle** - Use the symbols which come within the framework. -The framework comes with an integration for the web framework [Vapor](https://swiftpackageindex.com/vapor/vapor). Interested? Lets get started: [Tutorials](https://swiftpackageindex.com/vapor-community/HTMLKit/3.0.0-alpha.8/documentation/htmlkit) +The framework comes with an integration for the web framework [Vapor](https://github.com/vapor/vapor). Interested? Lets get started: [Tutorials](https://swiftpackageindex.com/vapor-community/htmlkit/main/tutorials/introduction) diff --git a/Sources/HTMLKit/Framework/Environment/EnvironmentObject.swift b/Sources/HTMLKit/Framework/Environment/EnvironmentObject.swift index 6d4a858c..d030f9ce 100644 --- a/Sources/HTMLKit/Framework/Environment/EnvironmentObject.swift +++ b/Sources/HTMLKit/Framework/Environment/EnvironmentObject.swift @@ -1,19 +1,19 @@ import Foundation /// A property wrapper type to initate an environment object -@frozen @propertyWrapper public struct EnvironmentObject { +@frozen @propertyWrapper public struct EnvironmentObject { /// The wrapped value - public var wrappedValue: Wrapper + public var wrappedValue: Wrapper /// Converts the type into the wrapped value - public init(_ type: Value.Type) { + public init(_ type: ObjectType.Type) { self.wrappedValue = .init() } /// A type, that holds the environment object informationen - @dynamicMemberLookup public struct Wrapper { + @dynamicMemberLookup public struct Wrapper { /// The path of the parent internal var parent: AnyKeyPath? @@ -24,7 +24,7 @@ import Foundation /// Initiates a wrapper public init() { - self.path = \Value.self + self.path = \WrapperType.self } /// Initiates a wrapper with the necessary information for the environment object @@ -35,7 +35,7 @@ import Foundation } /// Looks up for a containing property - public subscript(dynamicMember member: KeyPath) -> EnvironmentValue { + public subscript(dynamicMember member: KeyPath) -> EnvironmentValue { guard let newPath = self.path.appending(path: member) else { fatalError() @@ -49,7 +49,7 @@ import Foundation } /// Looks up for a containing model - public subscript(dynamicMember member: KeyPath) -> Wrapper where T: ViewModel { + public subscript(dynamicMember member: KeyPath) -> Wrapper where T: ViewModel { guard let newPath = self.path.appending(path: member) else { fatalError() diff --git a/Sources/HTMLKit/Framework/Localization/Localization.swift b/Sources/HTMLKit/Framework/Localization/Localization.swift index 9b9ed897..172d53b4 100644 --- a/Sources/HTMLKit/Framework/Localization/Localization.swift +++ b/Sources/HTMLKit/Framework/Localization/Localization.swift @@ -12,6 +12,7 @@ public class Localization { case missingTables case unkownTable case noFallback + case loadingDataFailed public var description: String { @@ -30,6 +31,9 @@ public class Localization { case .noFallback: return "The fallback needs to be set up first." + + case .loadingDataFailed: + return "Unable to load data." } } } @@ -81,11 +85,14 @@ public class Localization { if var translationTables = localizationTables[locale] { - if let translations = NSDictionary(contentsOf: path) as? [String: String] { - translationTables.append(TranslationTable(name: path.deletingPathExtension().lastPathComponent, translations: translations)) + if let data = try? Foundation.Data(contentsOf: path) { + + if let translations = try? PropertyListSerialization.propertyList(from: data, options: .mutableContainers, format: nil) as? [String: String] { + translationTables.append(TranslationTable(name: path.deletingPathExtension().lastPathComponent, translations: translations)) + } + + localizationTables[locale] = translationTables } - - localizationTables[locale] = translationTables } } diff --git a/Sources/HTMLKit/Framework/Rendering/Features.swift b/Sources/HTMLKit/Framework/Rendering/Features.swift new file mode 100644 index 00000000..780ea9e2 --- /dev/null +++ b/Sources/HTMLKit/Framework/Rendering/Features.swift @@ -0,0 +1,11 @@ +/// An option set of features. +public struct Features: OptionSet { + + public var rawValue: Int + + public static let markdown = Features(rawValue: 1 << 0) + + public init(rawValue: Int) { + self.rawValue = rawValue + } +} diff --git a/Sources/HTMLKit/Framework/Rendering/Renderer+FeatureSet.swift b/Sources/HTMLKit/Framework/Rendering/Renderer+FeatureSet.swift deleted file mode 100644 index e3cce561..00000000 --- a/Sources/HTMLKit/Framework/Rendering/Renderer+FeatureSet.swift +++ /dev/null @@ -1,14 +0,0 @@ -extension Renderer { - - public struct FeatureSet: OptionSet { - - public var rawValue: Int - - public static let markdown = FeatureSet(rawValue: 1 << 0) - - public init(rawValue: Int) { - self.rawValue = rawValue - } - } - -} diff --git a/Sources/HTMLKit/Framework/Rendering/Renderer.swift b/Sources/HTMLKit/Framework/Rendering/Renderer.swift index b357c35a..2be7f67e 100644 --- a/Sources/HTMLKit/Framework/Rendering/Renderer.swift +++ b/Sources/HTMLKit/Framework/Rendering/Renderer.swift @@ -6,19 +6,28 @@ import Foundation import OrderedCollections -/// A struct containing the different formulas for the different views. @_documentation(visibility: internal) -public class Renderer { +public final class Renderer { - /// A enumeration of possible render errors + /// An enumeration of possible rendering errors. public enum Errors: Error { + /// Indicates a casting error. case unableToCastEnvironmentValue + + /// Indicates a wrong environment key. case unindendedEnvironmentKey + + /// Indicates a missing environment object. case environmentObjectNotFound + + /// Indicates a missing environment value. case environmentValueNotFound + + /// Indicates a missing localization configuration. case missingLocalization + /// A brief error description. public var description: String { switch self { @@ -40,47 +49,35 @@ public class Renderer { } } + /// The context environment private var environment: Environment + /// The localization configuration private var localization: Localization? + /// The security configuration private var security: Security + /// The markdown parser private var markdown: Markdown - public var features: FeatureSet + /// The feature flag used to manage the visibility of new and untested features. + private var features: Features /// Initiates the renderer. - public init(localization: Localization? = nil) { - - self.localization = localization - self.environment = Environment() - self.security = Security() - self.markdown = Markdown() - self.features = [] - } - - /// Initiates the renderer. - public init(localization: Localization? = nil, security: Security) { - - self.localization = localization - self.environment = Environment() - self.security = security - self.markdown = Markdown() - self.features = [] - } - - /// Initiates the renderer. - public init(localization: Localization? = nil, environment: Environment, security: Security) { + public init(localization: Localization? = nil, + environment: Environment = Environment(), + security: Security = Security(), + features: Features = []) { self.localization = localization self.environment = environment self.security = security self.markdown = Markdown() - self.features = [] + self.features = features } - /// Renders a view + /// Renders a view and transforms it into a string representation. public func render(view: some View) throws -> String { var result = "" @@ -92,7 +89,8 @@ public class Renderer { return result } - internal func render(contents: [Content]) throws -> String { + /// Reads the view content and transforms it. + private func render(contents: [Content]) throws -> String { var result = "" @@ -156,8 +154,8 @@ public class Renderer { return result } - /// Renders a content element - internal func render(element: some ContentNode) throws -> String { + /// Renders a content element. A content element usually has descendants, which need to be rendered as well. + private func render(element: some ContentNode) throws -> String { var result = "" @@ -234,8 +232,8 @@ public class Renderer { return result } - /// Renders a empty element - internal func render(element: some EmptyNode) throws -> String { + /// Renders a empty element. An empty element has no descendants. + private func render(element: some EmptyNode) throws -> String { var result = "" @@ -250,11 +248,11 @@ public class Renderer { return result } - /// Renders a document element - internal func render(element: some DocumentNode) -> String { + /// Renders a document element. The document element holds the metadata. + private func render(element: some DocumentNode) -> String { var result = "" - + result += " String { + /// Renders a comment element. + private func render(element: some CommentNode) -> String { var result = "" @@ -278,8 +276,8 @@ public class Renderer { return result } - /// Renders a content element - internal func render(element: some CustomNode) throws -> String { + /// Renders a custom element. + private func render(element: some CustomNode) throws -> String { var result = "" @@ -352,8 +350,8 @@ public class Renderer { return result } - /// Renders a localized string key - internal func render(stringkey: LocalizedStringKey) throws -> String { + /// Renders a localized string key. + private func render(stringkey: LocalizedStringKey) throws -> String { guard let localization = self.localization else { throw Errors.missingLocalization @@ -366,8 +364,8 @@ public class Renderer { return try localization.localize(key: stringkey.key, locale: environment.locale, interpolation: stringkey.interpolation) } - /// Renders a environment modifier - internal func render(modifier: EnvironmentModifier) throws -> String { + /// Renders a environment modifier. + private func render(modifier: EnvironmentModifier) throws -> String { if let value = modifier.value { self.environment.upsert(value, for: modifier.key) @@ -401,8 +399,8 @@ public class Renderer { return try render(contents: modifier.content) } - /// Renders a environment value - internal func render(value: EnvironmentValue) throws -> String { + /// Renders a environment value. + private func render(value: EnvironmentValue) throws -> String { guard let parent = self.environment.retrieve(for: value.parentPath) else { throw Errors.environmentObjectNotFound @@ -439,8 +437,8 @@ public class Renderer { } } - /// Renders the node attributes - internal func render(attributes: OrderedDictionary) throws -> String { + /// Renders the node attributes. + private func render(attributes: OrderedDictionary) throws -> String { var result = "" @@ -461,13 +459,13 @@ public class Renderer { return result } - /// Renders the markdown content - internal func render(markdown: MarkdownString) throws -> String { + /// Renders the markdown content. + private func render(markdown: MarkdownString) throws -> String { return self.markdown.render(string: escape(content: markdown.raw)) } /// Converts specific charaters into encoded values. - internal func escape(attribute value: String) -> String { + private func escape(attribute value: String) -> String { if security.autoEscaping { return value.replacingOccurrences(of: "&", with: "&") @@ -479,7 +477,7 @@ public class Renderer { } /// Converts specific charaters into encoded values. - internal func escape(content value: String) -> String { + private func escape(content value: String) -> String { if security.autoEscaping { return value.replacingOccurrences(of: "<", with: "<") diff --git a/Sources/HTMLKit/Framework/Security/Security.swift b/Sources/HTMLKit/Framework/Security/Security.swift index 0c4c7bbf..b3319770 100644 --- a/Sources/HTMLKit/Framework/Security/Security.swift +++ b/Sources/HTMLKit/Framework/Security/Security.swift @@ -1,4 +1,4 @@ -public class Security { +public final class Security { public var autoEscaping: Bool diff --git a/Sources/HTMLKitVapor/Configuration.swift b/Sources/HTMLKitVapor/Configuration.swift new file mode 100644 index 00000000..a0525177 --- /dev/null +++ b/Sources/HTMLKitVapor/Configuration.swift @@ -0,0 +1,26 @@ +import HTMLKit + +/// Holds the renderer configuration +public final class Configuration { + + /// Holds the localization configuration + internal var localization: HTMLKit.Localization + + /// Holds the environment configuration + internal var environment: HTMLKit.Environment + + /// Holds the security configuration + internal var security: HTMLKit.Security + + /// Holds the enabled features + internal var features: HTMLKit.Features + + /// Creates a configuration + public init() { + + self.localization = Localization() + self.environment = Environment() + self.security = Security() + self.features = [] + } +} diff --git a/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift b/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift index fa208d33..e3dfc8bf 100644 --- a/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift +++ b/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift @@ -16,58 +16,65 @@ extension Application { /// The vapor provider public struct HtmlKit { - internal struct LocalizationStorageKey: StorageKey { + public var security: HTMLKit.Security { - public typealias Value = HTMLKit.Localization - } - - internal struct EnvironmentStorageKey: StorageKey { + get { + self.configuration.security + } - public typealias Value = HTMLKit.Environment + nonmutating set { + self.configuration.security = newValue + } } - internal struct SecurityStorageKey: StorageKey { + public var environment: HTMLKit.Environment { - public typealias Value = HTMLKit.Security + get { + self.configuration.environment + } + + nonmutating set { + self.configuration.environment = newValue + } } - public var security: HTMLKit.Security { + public var localization: HTMLKit.Localization { - if let configuration = self.application.storage[SecurityStorageKey.self] { - return configuration + get { + self.configuration.localization } - let configuration = Security() - - self.application.storage[SecurityStorageKey.self] = configuration - - return configuration + nonmutating set { + self.configuration.localization = newValue + } } - /// The view localization - public var localization: HTMLKit.Localization { + public var features: HTMLKit.Features { - if let configuration = self.application.storage[LocalizationStorageKey.self] { - return configuration + get { + self.configuration.features } - let configuration = Localization() - - self.application.storage[LocalizationStorageKey.self] = configuration + nonmutating set { + self.configuration.features = newValue + } + } + + internal struct ConfigurationKey: StorageKey { - return configuration + public typealias Value = Configuration } - /// The view environment - public var environment: HTMLKit.Environment { + /// The configuration storage + internal var configuration: Configuration { - if let configuration = self.application.storage[EnvironmentStorageKey.self] { + if let configuration = self.application.storage[ConfigurationKey.self] { return configuration } - let configuration = Environment() + let configuration = Configuration() - self.application.storage[EnvironmentStorageKey.self] = configuration + self.application.storage[ConfigurationKey.self] = configuration return configuration } @@ -75,10 +82,7 @@ extension Application { /// The view renderer internal var renderer: ViewRenderer { - return .init(eventLoop: application.eventLoopGroup.next(), - localization: localization, - environment: environment, - security: security) + return .init(eventLoop: application.eventLoopGroup.next(), configuration: configuration) } /// The application dependency @@ -111,9 +115,6 @@ extension Request { self.application.htmlkit.localization.set(locale: acceptLanguage) } - return .init(eventLoop: self.eventLoop, - localization: self.application.htmlkit.localization, - environment: self.application.htmlkit.environment, - security: self.application.htmlkit.security) + return .init(eventLoop: self.eventLoop, configuration: self.application.htmlkit.configuration) } } diff --git a/Sources/HTMLKitVapor/ViewRenderer.swift b/Sources/HTMLKitVapor/ViewRenderer.swift index 9c08ae2d..933dc63a 100644 --- a/Sources/HTMLKitVapor/ViewRenderer.swift +++ b/Sources/HTMLKitVapor/ViewRenderer.swift @@ -16,13 +16,16 @@ public class ViewRenderer { internal var renderer: Renderer /// Creates the view renderer - public init(eventLoop: EventLoop, localization: Localization, environment: HTMLKit.Environment, security: Security) { + public init(eventLoop: EventLoop, configuration: Configuration) { self.eventLoop = eventLoop - self.renderer = Renderer(localization: localization, environment: environment, security: security) + self.renderer = Renderer(localization: configuration.localization, + environment: configuration.environment, + security: configuration.security, + features: configuration.features) } - /// Renders a layout and its context + /// Renders a view and transforms it into a view response. public func render(_ view: some HTMLKit.View) -> EventLoopFuture { do { @@ -37,7 +40,7 @@ public class ViewRenderer { } } - /// Renders a layout and its context + /// Renders a view and transforms it into a view response. public func render(_ view: some HTMLKit.View) async throws -> Vapor.View { return try await render(view).get() } diff --git a/Tests/HTMLKitTests/PerformanceTests.swift b/Tests/HTMLKitTests/PerformanceTests.swift index 1efbf26f..5c0bc4bd 100644 --- a/Tests/HTMLKitTests/PerformanceTests.swift +++ b/Tests/HTMLKitTests/PerformanceTests.swift @@ -49,8 +49,7 @@ final class PerformanceTests: XCTestCase { let security = Security() security.autoEscaping = true - let renderer = Renderer(security: security) - renderer.features = [.markdown] + let renderer = Renderer(security: security, features: [.markdown]) measure { diff --git a/Tests/HTMLKitTests/RenderingTests.swift b/Tests/HTMLKitTests/RenderingTests.swift index 3a904493..86f4d44b 100644 --- a/Tests/HTMLKitTests/RenderingTests.swift +++ b/Tests/HTMLKitTests/RenderingTests.swift @@ -13,7 +13,7 @@ final class RenderingTests: XCTestCase { @ContentBuilder var body: Content } - var renderer = Renderer() + var renderer = Renderer(features: [.markdown]) func testRenderingDocument() throws { @@ -234,8 +234,6 @@ final class RenderingTests: XCTestCase { func testRenderingItalicMarkdown() throws { - renderer.features = [.markdown] - let view = TestView { Paragraph { MarkdownString("*italic*") @@ -255,8 +253,6 @@ final class RenderingTests: XCTestCase { func testRenderingBoldMarkdown() throws { - renderer.features = [.markdown] - let view = TestView { Paragraph { MarkdownString("**bold**") @@ -276,8 +272,6 @@ final class RenderingTests: XCTestCase { func testRenderingBoldItalicMarkdown() throws { - renderer.features = [.markdown] - let view = TestView { Paragraph { MarkdownString("***bold and italic***") @@ -297,8 +291,6 @@ final class RenderingTests: XCTestCase { func testRenderingMonospaceMarkdown() throws { - renderer.features = [.markdown] - let view = TestView { Paragraph { MarkdownString("`code`") @@ -314,8 +306,6 @@ final class RenderingTests: XCTestCase { func testRenderingStrikeThroughMarkdown() throws { - renderer.features = [.markdown] - let view = TestView { Paragraph { MarkdownString("~strikethrough~") @@ -335,8 +325,6 @@ final class RenderingTests: XCTestCase { func testRenderingLinkMarkdown() throws { - renderer.features = [.markdown] - let view = TestView { Paragraph { MarkdownString("[Link](https://www.google.de)") @@ -352,8 +340,6 @@ final class RenderingTests: XCTestCase { func testRenderingMarkdownParagraph() throws { - renderer.features = [.markdown] - let view = TestView { Paragraph { MarkdownString("This *substring* is **important**.") @@ -369,8 +355,6 @@ final class RenderingTests: XCTestCase { func testRenderingNestedMarkdown() throws { - renderer.features = [.markdown] - let view = TestView { Paragraph { MarkdownString { diff --git a/Tests/HTMLKitTests/SecurityTests.swift b/Tests/HTMLKitTests/SecurityTests.swift index 3916fc2d..b598aa0d 100644 --- a/Tests/HTMLKitTests/SecurityTests.swift +++ b/Tests/HTMLKitTests/SecurityTests.swift @@ -13,7 +13,7 @@ final class SecurityTests: XCTestCase { @ContentBuilder var body: Content } - var renderer = Renderer() + var renderer = Renderer(features: [.markdown]) func testEncodingAttributeContext() throws { @@ -105,8 +105,6 @@ final class SecurityTests: XCTestCase { } } - renderer.features = [.markdown] - XCTAssertEqual(try renderer.render(view: view), """

lt;script></script>

diff --git a/Tests/HTMLKitVaporTests/ProviderTests.swift b/Tests/HTMLKitVaporTests/ProviderTests.swift index f2c88f72..135ec129 100644 --- a/Tests/HTMLKitVaporTests/ProviderTests.swift +++ b/Tests/HTMLKitVaporTests/ProviderTests.swift @@ -81,6 +81,17 @@ final class ProviderTests: XCTestCase { } } } + + struct FriendView: HTMLKit.View { + + var body: HTMLKit.Content { + MainView { + Paragraph { + MarkdownString("This *substring* is **important**.") + } + } + } + } } func testEventLoopIntegration() throws { @@ -114,7 +125,6 @@ final class ProviderTests: XCTestCase { } } - @available(macOS 12, *) func testConcurrencyIntegration() throws { let app = Application(.testing) @@ -146,7 +156,6 @@ final class ProviderTests: XCTestCase { } } - @available(macOS 12, *) func testLocalizationIntegration() throws { let currentFile = URL(fileURLWithPath: #file).deletingLastPathComponent() @@ -183,7 +192,6 @@ final class ProviderTests: XCTestCase { } } - @available(macOS 12, *) func testEnvironmentIntegration() throws { let app = Application(.testing) @@ -211,4 +219,34 @@ final class ProviderTests: XCTestCase { ) } } + + func testMarkdownSupport() throws { + + let app = Application(.testing) + + defer { app.shutdown() } + + app.htmlkit.features = [.markdown] + + app.get("test") { request async throws -> Vapor.View in + return try await request.htmlkit.render(TestPage.FriendView()) + } + + try app.test(.GET, "test") { response in + XCTAssertEqual(response.status, .ok) + XCTAssertEqual(response.body.string, + """ + \ + \ + \ + TestPage\ + \ + \ +

This substring is important.

\ + \ + + """ + ) + } + } }