diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 00000000..f9b21b8d --- /dev/null +++ b/Package.resolved @@ -0,0 +1,160 @@ +{ + "object": { + "pins": [ + { + "package": "Console", + "repositoryURL": "https://github.com/vapor/console.git", + "state": { + "branch": null, + "revision": "d6cf07af59ae63cd95c4b5f98cf1f25627750fd1", + "version": "3.1.0" + } + }, + { + "package": "Core", + "repositoryURL": "https://github.com/vapor/core.git", + "state": { + "branch": null, + "revision": "439d6dcd6c520451ae30d39b2ca9f2aba96c22f4", + "version": "3.7.0" + } + }, + { + "package": "Crypto", + "repositoryURL": "https://github.com/vapor/crypto.git", + "state": { + "branch": null, + "revision": "45bb12d13cdec80dbd1cc0685ea002e51ab83439", + "version": "3.3.2" + } + }, + { + "package": "DatabaseKit", + "repositoryURL": "https://github.com/vapor/database-kit.git", + "state": { + "branch": null, + "revision": "8f352c8e66dab301ab9bfef912a01ce1361ba1e4", + "version": "1.3.3" + } + }, + { + "package": "HTTP", + "repositoryURL": "https://github.com/vapor/http.git", + "state": { + "branch": null, + "revision": "b57005e0de30ba36372ac41bfce1ac12b2bc3272", + "version": "3.1.8" + } + }, + { + "package": "Multipart", + "repositoryURL": "https://github.com/vapor/multipart.git", + "state": { + "branch": null, + "revision": "bd7736c5f28e48ed8b683dcc9df3dcd346064c2b", + "version": "3.0.3" + } + }, + { + "package": "Routing", + "repositoryURL": "https://github.com/vapor/routing.git", + "state": { + "branch": null, + "revision": "626190ddd2bd9f967743b60ba6adaf90bbd2651c", + "version": "3.0.2" + } + }, + { + "package": "Service", + "repositoryURL": "https://github.com/vapor/service.git", + "state": { + "branch": null, + "revision": "4907311d7d7f609365982fa302b8b17ffdeb46da", + "version": "1.0.1" + } + }, + { + "package": "swift-nio", + "repositoryURL": "https://github.com/apple/swift-nio.git", + "state": { + "branch": null, + "revision": "87dbd0216c47ea2e7ddb1b545271b716e03b943e", + "version": "1.13.1" + } + }, + { + "package": "swift-nio-ssl", + "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", + "state": { + "branch": null, + "revision": "0f3999f3e3c359cc74480c292644c3419e44a12f", + "version": "1.4.0" + } + }, + { + "package": "swift-nio-ssl-support", + "repositoryURL": "https://github.com/apple/swift-nio-ssl-support.git", + "state": { + "branch": null, + "revision": "c02eec4e0e6d351cd092938cf44195a8e669f555", + "version": "1.0.0" + } + }, + { + "package": "swift-nio-zlib-support", + "repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git", + "state": { + "branch": null, + "revision": "37760e9a52030bb9011972c5213c3350fa9d41fd", + "version": "1.0.0" + } + }, + { + "package": "TemplateKit", + "repositoryURL": "https://github.com/vapor/template-kit.git", + "state": { + "branch": null, + "revision": "aff2d6fc65bfd04579b0201b31a8d6720239c1cf", + "version": "1.1.1" + } + }, + { + "package": "URLEncodedForm", + "repositoryURL": "https://github.com/vapor/url-encoded-form.git", + "state": { + "branch": null, + "revision": "932024f363ee5ff59059cf7d67194a1c271d3d0c", + "version": "1.0.5" + } + }, + { + "package": "Validation", + "repositoryURL": "https://github.com/vapor/validation.git", + "state": { + "branch": null, + "revision": "4de213cf319b694e4ce19e5339592601d4dd3ff6", + "version": "2.1.1" + } + }, + { + "package": "Vapor", + "repositoryURL": "https://github.com/vapor/vapor", + "state": { + "branch": null, + "revision": "c86ada59b31c69f08a6abd4f776537cba48d5df6", + "version": "3.3.0" + } + }, + { + "package": "WebSocket", + "repositoryURL": "https://github.com/vapor/websocket.git", + "state": { + "branch": null, + "revision": "21eb4773e25a8ff96fe347a31fe106900a69fa6a", + "version": "1.1.1" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift index 9bf6ad3e..7502565a 100644 --- a/Package.swift +++ b/Package.swift @@ -14,13 +14,16 @@ let package = Package( dependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), + + // For creating the providers, HTTP response and Request extension + .package(url: "https://github.com/vapor/vapor", from: "3.3.0"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "HTMLKit", - dependencies: []), + dependencies: ["Vapor"]), .testTarget( name: "HTMLKitTests", dependencies: ["HTMLKit"]), diff --git a/README.md b/README.md index deddcf93..a5de5af7 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,24 @@ Render **lightning fast** HTML templates in a *typesafe* way! By using Swift's powerful language features and a pre-rendering algorithm, will HTMLKit render insanely fast templates but also be able to catch bugs that otherwise might occur with other templating options. -## Swift PM +## Getting Started +Add the following in your `Package.swift` file ```swift .package(url: "https://github.com/vapor-community/HTMLKit.git", from: "1.0.0"), ``` +And register the provider and the different templates with in `configure.swift` +```swift +var renderer = HTMLRenderer() +try renderer.add(template: MyTemplates()) + +try services.register(HTMLKitProvider()) +services.register(renderer) +``` -## How fast is HTMLKit? ⚡ +## Some benchmarks? ⚡ -As mentioned HTMLKit is extremely fast, but exactly how fast? +As mentioned HTMLKit is extremely fast since it pre-renders most of the template, and uses `KeyPath`'s instead of decoding the context with `Codable`. But how much will faster will this make the rendering? By using the *Leaf* templating language as a benchmark, HTMLKit was **150x** faster, and compared to *Pointfree* **16-25x** faster. The *Leaf* template used was a fairly complex template and HTMLKit rendered 128 documents in *0.00548 sec*. @@ -22,9 +31,8 @@ The *Leaf* template used was a fairly complex template and HTMLKit rendered 128 Let's get started with the two main protocols to know. -- `TemplateBuilder`: This is a protocol making it easy to render HTML views by giving you access to a lot of helper functions. -- `ContextualTemplate`: This is a protocol conforms to `TemplateBuilder` but also needs a `Context` to render. This could be a struct, protocol etc. -- `StaticView`: This is a protocol conforms to `TemplateBuilder` but needs no `Context` to render. +- `ContextualTemplate`: This is a protocol making it easy to render HTML views by giving you access to a lot of helper functions. But this needs a `Context` to render. This could be a struct, protocol etc. +- `StaticView`: This is a protocol conforms to `ContextualTemplate` but needs no `Context` to render. When creating a view, it is recommend to use either `StaticView` of `ContextualTemplate`, since the `HTMLRenderer` has functions that is tailored for these two protocols. @@ -74,7 +82,7 @@ Bellow is an example of how to render a view with a context: var renderer = HTMLRenderer() try renderer.add(template: SimpleView()) ... -try renderer.render(SimpleView.self, with: .init(value: "hello world")) +try req.renderer().render(SimpleView.self, with: .init(value: "hello world")) ``` This would render: ```html @@ -188,25 +196,29 @@ This would render: ## More Feature Syntax +- Variables: + * A variable that is HTML safe = `variable(\.title)` + * A variable that do not escape anything = `variable(\.title, escaping: .unsafeNone)` + * A variable that is not in the current `Context` (example get a variable in superview) `unsafeVariable(in: BaseTemplate.self, for: \.title)` or `unsafeVariable(... escaping: .unsafeNone)` - Embed: * Where the sub view's `Context` is equal to the super view's `Context` = `embed(SubView())` * Where the sub view's `Context`is variable of the super view's `Context`= `embed(Subview(), withPath: \.subContext)` - ForEach: - * Where the super view's `Context` is an array of the sub view's `Context` = `forEachInContext(render: SubView())` + * Where the super view's `Context` is an array of the sub view's `Context` = `forEach(render: SubView())` * Where the super view's `Context` variable is an array of the sub view's `Context` = `forEach(in \.subContext, render: Subview()` - If: - * If value is a `Bool` = `runtimeIf(\.bool, div.child(...))` - * If value is `nil` = `runtimeIf(isNil: \.optional, div.child(...))` - * If value is not `nil` = `runtimeIf(isNotNil: \.optional, div.child(...))` - * If value conforms to `Equatable` = `runtimeIf(\.int == 2, div.child(...))` - * If value conforms to `Equatable` = `runtimeIf(\.int != 2, div.child(...))` - * If value conforms to `Comparable` = `runtimeIf(\.int < 2, div.child(...))` - * If value conforms to `Comparable` = `runtimeIf(\.int > 2, div.child(...))` + * If the context is a `Bool` = `runtimeIf(\.bool, div.child(...))` + * If the context is `nil` = `runtimeIf(isNil: \.optional, div.child(...))` + * If the context is not `nil` = `runtimeIf(isNotNil: \.optional, div.child(...))` + * If the context conforms to `Equatable` = `runtimeIf(\.int == 2, div.child(...))` + * If the context conforms to `Equatable` = `runtimeIf(\.int != 2, div.child(...))` + * If the context conforms to `Comparable` = `runtimeIf(\.int < 2, div.child(...))` + * If the context conforms to `Comparable` = `runtimeIf(\.int > 2, div.child(...))` * It is also possible to use `||` and `&&` for more complex statments. `runtimeIf(\.bool || \.otherBool, div.child(...))` * `elseIf`has the same statments and is a method on the returned if. `runtimeIf(...).elseIf(...)` * and lastly `else`. `runtimeIf(...).else(div.child(...))` - Dynamic Attributes - * In order to add attributes based on the `Context` you will need to use `dynamic(div)`. This will create a dynamic node and after this you can use if's. `dynamic(div).if(\.bool, add: .checked)` + * In order to add attributes based on the `Context` you can use if's. `div.if(\.bool, add: .checked)` Add custom node types by extending `TemplateBuilder`. diff --git a/Sources/HTMLKit/Conditions.swift b/Sources/HTMLKit/Conditions.swift index 1324aad2..6dcde3fb 100644 --- a/Sources/HTMLKit/Conditions.swift +++ b/Sources/HTMLKit/Conditions.swift @@ -287,6 +287,40 @@ public func && (lhs: TemplateIF.Condition, rhs: TemplateIF.Con return TemplateIF.Condition(condition: AndCondition(first: lhs, second: rhs)) } +/// Creates a `AndCondition` condition +/// +/// - Parameters: +/// - lhs: The key path +/// - rhs: The constant value +/// - Returns: A `TemplateIF.Condition` object +public func && (lhs: KeyPath, rhs: TemplateIF.Condition) -> TemplateIF.Condition where Root: ContextualTemplate { + let lhsCondition = BoolCondition(path: lhs) + return TemplateIF.Condition(condition: AndCondition(first: lhsCondition, second: rhs)) +} + +/// Creates a `AndCondition` condition +/// +/// - Parameters: +/// - lhs: The key path +/// - rhs: The constant value +/// - Returns: A `TemplateIF.Condition` object +public func && (lhs: TemplateIF.Condition, rhs: KeyPath) -> TemplateIF.Condition where Root: ContextualTemplate { + let rhsCondition = BoolCondition(path: rhs) + return TemplateIF.Condition(condition: AndCondition(first: lhs, second: rhsCondition)) +} + +/// Creates a `AndCondition` condition +/// +/// - Parameters: +/// - lhs: The key path +/// - rhs: The constant value +/// - Returns: A `TemplateIF.Condition` object +public func && (lhs: KeyPath, rhs: KeyPath) -> TemplateIF.Condition where Root: ContextualTemplate { + let lhsCondition = BoolCondition(path: lhs) + let rhsCondition = BoolCondition(path: rhs) + return TemplateIF.Condition(condition: AndCondition(first: lhsCondition, second: rhsCondition)) +} + /// Creates a `OrCondition` condition /// /// - Parameters: @@ -296,3 +330,37 @@ public func && (lhs: TemplateIF.Condition, rhs: TemplateIF.Con public func || (lhs: TemplateIF.Condition, rhs: TemplateIF.Condition) -> TemplateIF.Condition where Root: ContextualTemplate { return TemplateIF.Condition(condition: OrCondition(first: lhs, second: rhs)) } + +/// Creates a `AndCondition` condition +/// +/// - Parameters: +/// - lhs: The key path +/// - rhs: The constant value +/// - Returns: A `TemplateIF.Condition` object +public func || (lhs: KeyPath, rhs: TemplateIF.Condition) -> TemplateIF.Condition where Root: ContextualTemplate { + let lhsCondition = BoolCondition(path: lhs) + return TemplateIF.Condition(condition: AndCondition(first: lhsCondition, second: rhs)) +} + +/// Creates a `AndCondition` condition +/// +/// - Parameters: +/// - lhs: The key path +/// - rhs: The constant value +/// - Returns: A `TemplateIF.Condition` object +public func || (lhs: TemplateIF.Condition, rhs: KeyPath) -> TemplateIF.Condition where Root: ContextualTemplate { + let rhsCondition = BoolCondition(path: rhs) + return TemplateIF.Condition(condition: AndCondition(first: lhs, second: rhsCondition)) +} + +/// Creates a `AndCondition` condition +/// +/// - Parameters: +/// - lhs: The key path +/// - rhs: The constant value +/// - Returns: A `TemplateIF.Condition` object +public func || (lhs: KeyPath, rhs: KeyPath) -> TemplateIF.Condition where Root: ContextualTemplate { + let lhsCondition = BoolCondition(path: lhs) + let rhsCondition = BoolCondition(path: rhs) + return TemplateIF.Condition(condition: AndCondition(first: lhsCondition, second: rhsCondition)) +} diff --git a/Sources/HTMLKit/HTMLDocument.swift b/Sources/HTMLKit/HTMLDocument.swift new file mode 100644 index 00000000..7378e18e --- /dev/null +++ b/Sources/HTMLKit/HTMLDocument.swift @@ -0,0 +1,13 @@ + + +struct HTMLDocument: StaticView { + + let content: CompiledTemplate + + func build() -> CompiledTemplate { + return [ + doctype("html"), + content + ] + } +} diff --git a/Sources/HTMLKit/HTMLKitProvider.swift b/Sources/HTMLKit/HTMLKitProvider.swift new file mode 100644 index 00000000..c27f5a93 --- /dev/null +++ b/Sources/HTMLKit/HTMLKitProvider.swift @@ -0,0 +1,26 @@ +// +// HTMLTemplateConfig.swift +// HTMLKit +// +// Created by Mats Mollestad on 11/03/2019. +// + +import Service + +extension HTMLRenderer: Service {} + +/// A provider for the HTMLKit Library +public final class HTMLKitProvider: Provider { + + public init() {} + + public func register(_ services: inout Services) throws { + services.register { (container) in + return try container.make(HTMLRenderer.self) + } + } + + public func didBoot(_ container: Container) throws -> EventLoopFuture { + return .done(on: container) + } +} diff --git a/Sources/HTMLKit/HTMLRenderer.swift b/Sources/HTMLKit/HTMLRenderer.swift index 2c4c6ba1..a9ac6f98 100644 --- a/Sources/HTMLKit/HTMLRenderer.swift +++ b/Sources/HTMLKit/HTMLRenderer.swift @@ -1,5 +1,6 @@ import Foundation +import Vapor /// A struct containing the differnet formulas for the different views. /// @@ -25,8 +26,8 @@ public struct HTMLRenderer { var recoverySuggestion: String? { switch self { - case .unableToRetriveValue, .unableToAddVariable, .unableToRegisterKeyPath: - return "Remember to add .embed(withPath: \\Context.contextPath) when embeding a view" + case .unableToFindFormula: + return "Remember to add the template to the renerer with .add(template: ) or .add(view: )" default: return nil } } @@ -39,22 +40,35 @@ public struct HTMLRenderer { formulaCache = [:] } - /// Renders a brewed formula + /// Renders a `ContextualTemplate` formula /// /// try renderer.render(WelcomeView.self) /// /// - Parameters: /// - type: The view type to render /// - context: The needed context to render the view with - /// - Returns: Returns a rendered view + /// - Returns: Returns a rendered view in a raw `String` /// - Throws: If the formula do not exists, or if the rendering process fails - public func render(_ type: T.Type, with context: T.Context) throws -> String { + public func renderRaw(_ type: T.Type, with context: T.Context) throws -> String { guard let formula = formulaCache[String(reflecting: T.self)] as? Formula else { throw Errors.unableToFindFormula } return try formula.render(with: context) } + /// Renders a `ContextualTemplate` formula + /// + /// try renderer.render(WelcomeView.self) + /// + /// - Parameters: + /// - type: The view type to render + /// - context: The needed context to render the view with + /// - Returns: Returns a rendered view in a `HTTPResponse` + /// - Throws: If the formula do not exists, or if the rendering process fails + public func render(_ type: T.Type, with context: T.Context) throws -> HTTPResponse { + return try HTTPResponse(body: renderRaw(type, with: context)) + } + /// Brews a formula for later use /// /// try renderer.brewFormula(for: WelcomeView.self) @@ -67,23 +81,29 @@ public struct HTMLRenderer { formulaCache[String(reflecting: T.self)] = formula } - public func render(_ type: T.Type) throws -> String where T : StaticView { + /// Renders a `StaticView` formula + /// + /// try renderer.render(WelcomeView.self) + /// + /// - Parameter type: The view type to render + /// - Returns: Returns a rendered view in a raw `String` + /// - Throws: If the formula do not exists, or if the rendering process fails + public func renderRaw(_ type: T.Type) throws -> String where T : StaticView { guard let formula = formulaCache[String(reflecting: T.self)] as? Formula else { throw Errors.unableToFindFormula } return try formula.render(with: .init()) } - /// Brews a formula for later use + /// Renders a `StaticView` formula /// - /// try renderer.brewFormula(for: WelcomeView.self) + /// try renderer.render(WelcomeView.self) /// - /// - Parameter type: The view type to brew - /// - Throws: If the brewing process fails for some reason - public mutating func add(view: T) throws where T : StaticView { - let formula = Formula(view: T.self) - try view.build().brew(formula) - formulaCache[String(reflecting: type(of: view))] = formula + /// - Parameter type: The view type to render + /// - Returns: Returns a rendered view in a `HTTPResponse` + /// - Throws: If the formula do not exists, or if the rendering process fails + public func renderRaw(_ type: T.Type) throws -> HTTPResponse where T : StaticView { + return try HTTPResponse(body: renderRaw(type)) } /// Manage the differnet contextes @@ -95,6 +115,9 @@ public struct HTMLRenderer { /// The different paths from the orignial context fileprivate var contextPaths: [String : AnyKeyPath] + /// Return the `Context` for a `ContextualTemplate` + /// + /// - Returns: The `Context` func value(for type: T.Type) throws -> T.Context where T : ContextualTemplate { if let context = rootContext as? T.Context { return context @@ -105,6 +128,9 @@ public struct HTMLRenderer { } } + /// The value for a `KeyPath` + /// + /// - Returns: The value at the `KeyPath` func value(at path: KeyPath) throws -> Value { if let context = rootContext as? Root { return context[keyPath: path] @@ -221,3 +247,15 @@ public struct HTMLRenderer { } } } + + +extension Request { + + /// Creates a `HTMLRenderer` that can render templates + /// + /// - Returns: A `HTMLRenderer` containing all the templates + /// - Throws: If the shared container could not make the `HTMLRenderer` + func renderer() throws -> HTMLRenderer { + return try sharedContainer.make(HTMLRenderer.self) + } +} diff --git a/Sources/HTMLKit/Protocols/ContextualTemplate.swift b/Sources/HTMLKit/Protocols/ContextualTemplate.swift index 3c43cfc0..65882098 100644 --- a/Sources/HTMLKit/Protocols/ContextualTemplate.swift +++ b/Sources/HTMLKit/Protocols/ContextualTemplate.swift @@ -92,4 +92,8 @@ extension ContextualTemplate { condition.view = render return TemplateIF.init(conditions: condition) } + + public func unsafeVariable(in template: T.Type, for keyPath: KeyPath, escaping: EscapingOption = .safeHTML) -> CompiledTemplate where T : ContextualTemplate, V : CompiledTemplate { + return TemplateVariable.init(referance: .keyPath(keyPath), escaping: escaping) + } } diff --git a/Tests/HTMLKitTests/HTMLKitTests.swift b/Tests/HTMLKitTests/HTMLKitTests.swift index 7453a801..d0a88392 100644 --- a/Tests/HTMLKitTests/HTMLKitTests.swift +++ b/Tests/HTMLKitTests/HTMLKitTests.swift @@ -16,30 +16,30 @@ final class HTMLKitTests: XCTestCase { try renderer.add(template: DynamicAttribute()) try renderer.add(template: SelfLoopingView()) - try renderer.add(view: SimpleView()) - try renderer.add(view: UsingComponent()) - try renderer.add(view: ChainedEqualAttributes()) - try renderer.add(view: ChainedEqualAttributesDataNode()) - - let staticEmbedRender = try renderer.render(StaticEmbedView.self, with: .init(string: "Hello", int: 2)) - let someViewRender = try renderer.render(SomeView.self, with: .contentWith(name: "Mats", title: "Welcome")) - let forEachRender = try renderer.render(ForEachView.self, with: .content(from: ["1", "2", "3"])) - let firstIfRender = try renderer.render(IFView.self, with: .init(name: "Per", age: 19, nullable: nil, bool: false)) - let secondIfRender = try renderer.render(IFView.self, with: .init(name: "Mats", age: 20, nullable: nil, bool: true)) - let thirdIfRender = try renderer.render(IFView.self, with: .init(name: "Per", age: 21, nullable: "Some", bool: false)) - let varialbeRender = try renderer.render(VariableView.self, with: .init(string: "")) - let multipleEmbedRender = try renderer.render(MultipleContextualEmbed.self, with: .init(title: "Welcome", string: "String")) - let nonDynamic = try renderer.render(DynamicAttribute.self, with: .init(isChecked: false, isActive: false)) - let oneDynamic = try renderer.render(DynamicAttribute.self, with: .init(isChecked: false, isActive: true)) - let twoDynamic = try renderer.render(DynamicAttribute.self, with: .init(isChecked: true, isActive: true)) - - let simpleRender = try renderer.render(SimpleView.self) - let chainedRender = try renderer.render(ChainedEqualAttributes.self) - let chaindDataRender = try renderer.render(ChainedEqualAttributesDataNode.self) + try renderer.add(template: SimpleView()) + try renderer.add(template: UsingComponent()) + try renderer.add(template: ChainedEqualAttributes()) + try renderer.add(template: ChainedEqualAttributesDataNode()) + + let staticEmbedRender = try renderer.renderRaw(StaticEmbedView.self, with: .init(string: "Hello", int: 2)) + let someViewRender = try renderer.renderRaw(SomeView.self, with: .contentWith(name: "Mats", title: "Welcome")) + let forEachRender = try renderer.renderRaw(ForEachView.self, with: .content(from: ["1", "2", "3"])) + let firstIfRender = try renderer.renderRaw(IFView.self, with: .init(name: "Per", age: 19, nullable: nil, bool: false)) + let secondIfRender = try renderer.renderRaw(IFView.self, with: .init(name: "Mats", age: 20, nullable: nil, bool: true)) + let thirdIfRender = try renderer.renderRaw(IFView.self, with: .init(name: "Per", age: 21, nullable: "Some", bool: false)) + let varialbeRender = try renderer.renderRaw(VariableView.self, with: .init(string: "")) + let multipleEmbedRender = try renderer.renderRaw(MultipleContextualEmbed.self, with: .init(title: "Welcome", string: "String")) + let nonDynamic = try renderer.renderRaw(DynamicAttribute.self, with: .init(isChecked: false, isActive: false)) + let oneDynamic = try renderer.renderRaw(DynamicAttribute.self, with: .init(isChecked: false, isActive: true)) + let twoDynamic = try renderer.renderRaw(DynamicAttribute.self, with: .init(isChecked: true, isActive: true)) + + let simpleRender = try renderer.renderRaw(SimpleView.self) + let chainedRender = try renderer.renderRaw(ChainedEqualAttributes.self) + let chaindDataRender = try renderer.renderRaw(ChainedEqualAttributesDataNode.self) // let inputRender = try renderer.render(FormInput.self) - XCTAssertEqual(multipleEmbedRender, "WelcomeSome text

String

String

") + XCTAssertEqual(multipleEmbedRender, "WelcomeSome text

String

String

String

Welcome

") XCTAssertEqual(varialbeRender, "

<script>"'&</script>

") XCTAssertEqual(staticEmbedRender, "

Text

Hello

2
") XCTAssertEqual(someViewRender, "Welcome

Hello Mats!

") diff --git a/Tests/HTMLKitTests/HTMLTestDocuments.swift b/Tests/HTMLKitTests/HTMLTestDocuments.swift index 4b277190..15879a1b 100644 --- a/Tests/HTMLKitTests/HTMLTestDocuments.swift +++ b/Tests/HTMLKitTests/HTMLTestDocuments.swift @@ -294,7 +294,8 @@ struct MultipleContextualEmbed: ContextualTemplate { BaseView( body: [ span.child("Some text"), - embed(VariableView(), withPath: \.variable) + embed(VariableView(), withPath: \.variable), + embed(UnsafeVariable(), withPath: \.variable) ]), withPath: \.base) @@ -339,3 +340,19 @@ struct SelfLoopingView: ContextualTemplate { ) } } + +struct UnsafeVariable: ContextualTemplate { + + typealias Context = VariableView.Context + + func build() -> CompiledTemplate { + return div.child( + p.child( + variable(\.string) + ), + p.child( + unsafeVariable(in: MultipleContextualEmbed.self, for: \.base.title) + ) + ) + } +}