diff --git a/Sources/HTMLKit/Framework/Environment/Condition.swift b/Sources/HTMLKit/Framework/Environment/Condition.swift
new file mode 100644
index 00000000..2fa8c671
--- /dev/null
+++ b/Sources/HTMLKit/Framework/Environment/Condition.swift
@@ -0,0 +1,42 @@
+/// A type representing a conditional that compares an environment value against another value
+@_documentation(visibility: internal)
+public struct Condition {
+
+ /// A enumeration of potential comparison
+ public enum Comparison {
+
+ /// Indicates an equal comparison
+ case equal
+
+ /// Indicates a not-equal comparison
+ case unequal
+
+ /// Indicates a greater-than comparison
+ case greater
+
+ /// Indicates a less-than comparison
+ case less
+ }
+
+ /// The left-hand side value
+ internal let lhs: EnvironmentValue
+
+ /// The right-hand side value to test against
+ internal let rhs: any Comparable
+
+ /// The comparison to perfom
+ internal let comparison: Comparison
+
+ /// Initializes a condition
+ ///
+ /// - Parameters:
+ /// - lhs: The origin value
+ /// - rhs: The value to atest against
+ /// - operation: The comparison to perfom
+ public init(lhs: EnvironmentValue, rhs: any Comparable, comparison: Comparison) {
+
+ self.lhs = lhs
+ self.rhs = rhs
+ self.comparison = comparison
+ }
+}
diff --git a/Sources/HTMLKit/Framework/Environment/Conditional.swift b/Sources/HTMLKit/Framework/Environment/Conditional.swift
new file mode 100644
index 00000000..d8904f7b
--- /dev/null
+++ b/Sources/HTMLKit/Framework/Environment/Conditional.swift
@@ -0,0 +1,19 @@
+/// A type that defines a conditonal value which will be evualuated by the renderer.
+@_documentation(visibility: internal)
+public indirect enum Conditional {
+
+ /// Holds an optional
+ case optional(EnvironmentValue)
+
+ /// Holds an relation
+ case relation(Relation)
+
+ /// Holds a condition
+ case condition(Condition)
+
+ /// Holds a negation
+ case negation(Negation)
+
+ /// Holds a value
+ case value(EnvironmentValue)
+}
diff --git a/Sources/HTMLKit/Framework/Environment/Environment.swift b/Sources/HTMLKit/Framework/Environment/Environment.swift
index 32f89fac..0e35029d 100644
--- a/Sources/HTMLKit/Framework/Environment/Environment.swift
+++ b/Sources/HTMLKit/Framework/Environment/Environment.swift
@@ -1,41 +1,367 @@
import Foundation
-/// A type that represents the environment
-public class Environment {
+/// A class that represents the environment
+///
+/// The environment provides storage for various settings used by the renderer
+@_documentation(visibility: internal)
+public final class Environment {
+
+ /// 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
+
+ /// A brief error description.
+ public var description: String {
+
+ switch self {
+ case .unableToCastEnvironmentValue:
+ return "Unable to cast the environment value."
+
+ case .unindendedEnvironmentKey:
+ return "The environment key is not indended."
+
+ case .environmentValueNotFound:
+ return "Unable to retrieve environment value."
+
+ case .environmentObjectNotFound:
+ return "Unable to retrieve environment object."
+ }
+ }
+ }
/// The storage of the environment
private var storage: [AnyKeyPath: Any]
- /// Initiates a manager
+ /// Initializes the environment
public init() {
self.storage = [:]
}
- /// The current time zone of the environment
- public var timeZone: TimeZone?
+ /// The current time zone of the environment
+ public var timeZone: TimeZone? {
+
+ get {
+ retrieve(for: \EnvironmentKeys.timeZone) as? TimeZone
+ }
+ }
- /// The current calender of the environment
- public var calendar: Calendar?
+ /// The current calendar of the environment
+ public var calendar: Calendar? {
+
+ get {
+ retrieve(for: \EnvironmentKeys.calendar) as? Calendar
+ }
+ }
- /// The current local of the environment
- public var locale: Locale?
+ /// The current locale of the environment
+ public var locale: Locale? {
+
+ get {
+ retrieve(for: \EnvironmentKeys.locale) as? Locale
+ }
+ }
/// The current color scheme of the environment
- public var colorScheme: String?
+ public var colorScheme: String? {
+
+ get {
+ retrieve(for: \EnvironmentKeys.colorScheme) as? String
+ }
+ }
- /// Retrieves an item from storage by its path
+ /// Retrieves a value from environment for a given key path
+ ///
+ /// - Parameter path: The key path used to look up the value
+ ///
+ /// - Returns: The value
public func retrieve(for path: AnyKeyPath) -> Any? {
+ return storage[path]
+ }
+
+ /// Inserts or updates a value in the environment for the given key path
+ ///
+ /// - Parameters:
+ /// - value: The value to be stored or updated
+ /// - path: The key path that identifies where the value is stored
+ public func upsert(_ value: T, for path: AnyKeyPath) {
+ storage[path] = value
+ }
+
+ /// Resolves an environment value
+ ///
+ /// - Parameter value: The environment value to resolve
+ ///
+ /// - Returns: The resolved environment value
+ internal func resolve(value: EnvironmentValue) throws -> Any {
- if let value = self.storage[path] {
- return value
+ guard let parent = retrieve(for: value.parentPath) else {
+ throw Errors.environmentObjectNotFound
+ }
+
+ guard let value = parent[keyPath: value.valuePath] else {
+ throw Errors.environmentValueNotFound
}
- return nil
+ return value
}
- /// Adds und updates an item to the storage
- public func upsert(_ value: T, for path: AnyKeyPath) {
- self.storage[path] = value
+ /// Evaluates an environment value
+ ///
+ /// - Parameter value: The value to evaluate
+ ///
+ /// - Returns: The result of evaluation
+ internal func evaluate(value: EnvironmentValue) throws -> Bool {
+
+ guard let boolValue = try resolve(value: value) as? Bool else {
+ throw Errors.unableToCastEnvironmentValue
+ }
+
+ return boolValue
+ }
+
+ /// Evaluates an environment relation
+ ///
+ /// - Parameter relation: The relation to evaluate
+ ///
+ /// - Returns: The result of the evaluation
+ internal func evaluate(relation: Relation) throws -> Bool {
+
+ switch relation.term {
+ case .conjunction:
+
+ var result = true
+
+ switch relation.lhs {
+ case .optional(let optional):
+ result = try evaluate(optional: optional)
+
+ case .condition(let condition):
+ result = try evaluate(condition: condition)
+
+ case .relation(let relation):
+ result = try evaluate(relation: relation)
+
+ case .negation(let negation):
+ result = try evaluate(negation: negation)
+
+ case .value(let value):
+ result = try evaluate(value: value)
+ }
+
+ if !result {
+ // Bail early if the first result already is false
+ return result
+ }
+
+ switch relation.rhs {
+ case .optional(let optional):
+ result = try evaluate(optional: optional)
+
+ case .condition(let condition):
+ result = try evaluate(condition: condition)
+
+ case .relation(let relation):
+ result = try evaluate(relation: relation)
+
+ case .negation(let negation):
+ result = try evaluate(negation: negation)
+
+ case .value(let value):
+ result = try evaluate(value: value)
+ }
+
+ return result
+
+ case .disjunction:
+
+ var result = false
+
+ switch relation.lhs {
+ case .optional(let optional):
+ result = try evaluate(optional: optional)
+
+ case .condition(let condition):
+ result = try evaluate(condition: condition)
+
+ case .relation(let relation):
+ result = try evaluate(relation: relation)
+
+ case .negation(let negation):
+ result = try evaluate(negation: negation)
+
+ case .value(let value):
+ result = try evaluate(value: value)
+ }
+
+ if result {
+ // Bail early if the first result is already true
+ return result
+ }
+
+ switch relation.rhs {
+ case .optional(let optional):
+ result = try evaluate(optional: optional)
+
+ case .condition(let condition):
+ result = try evaluate(condition: condition)
+
+ case .relation(let relation):
+ result = try evaluate(relation: relation)
+
+ case .negation(let negation):
+ result = try evaluate(negation: negation)
+
+ case .value(let value):
+ result = try evaluate(value: value)
+ }
+
+ return result
+ }
+ }
+
+ /// Evaluates an environment condition
+ ///
+ /// - Parameter condition: The condition to evaluate
+ ///
+ /// - Returns: The result of the evaluation
+ internal func evaluate(condition: Condition) throws -> Bool {
+
+ let value = try resolve(value: condition.lhs)
+
+ switch condition.comparison {
+ case .equal:
+ return condition.rhs.equal(value)
+
+ case .greater:
+ return condition.rhs.greater(value)
+
+ case .unequal:
+ return condition.rhs.unequal(value)
+
+ case .less:
+ return condition.rhs.less(value)
+ }
+ }
+
+ /// Evaluates an environment negation
+ ///
+ /// - Parameter negation: The negation to evaluate
+ ///
+ /// - Returns: The result of the evaluation
+ internal func evaluate(negation: Negation) throws -> Bool {
+ return try !evaluate(value: negation.value)
+ }
+
+ /// Evaluates an environment optional
+ ///
+ /// - Parameter optional: The optional to evaluate
+ ///
+ /// - Returns: The result of the evaluation
+ internal func evaluate(optional: EnvironmentValue) throws -> Bool {
+
+ guard let optionalValue = try resolve(value: optional) as? Nullable else {
+ throw Errors.unableToCastEnvironmentValue
+ }
+
+ if !optionalValue.isNull {
+ return true
+ }
+
+ return false
+ }
+}
+
+extension Environment {
+
+ /// Evaluates one condition
+ ///
+ /// - Parameters:
+ /// - condition: The condition to evaluate
+ /// - content: The content for the true statement
+ ///
+ /// - Returns: A environment statement
+ public static func check(_ condition: EnvironmentValue, @ContentBuilder content: () -> [Content]) -> Statement {
+ return Statement(compound: .value(condition), first: content(), second: [])
+ }
+
+ /// Evaluates one condition
+ ///
+ /// - Parameters:
+ /// - condition: The condition to evaluate
+ /// - content: The content for the true statement
+ /// - then: The content for the false statement
+ ///
+ /// - Returns: A environment statement
+ public static func check(_ condition: EnvironmentValue, @ContentBuilder content: () -> [Content], @ContentBuilder then: () -> [Content]) -> Statement {
+ return Statement(compound: .value(condition), first: content(), second: then())
+ }
+
+ /// Evaluates one condition
+ ///
+ /// - Parameters:
+ /// - condition: The condition to evaluate
+ /// - content: The content for the true statement
+ ///
+ /// - Returns: A environment statement
+ public static func check(_ condition: Conditional, @ContentBuilder content: () -> [Content]) -> Statement {
+ return Statement(compound: condition, first: content(), second: [])
+ }
+
+ /// Evaluates one condition
+ ///
+ /// - Parameters:
+ /// - condition: The condition to evaluate
+ /// - content: The content for the true statement
+ /// - then: The content for the false statement
+ ///
+ /// - Returns: A environment statement
+ public static func check(_ condition: Conditional, @ContentBuilder content: () -> [Content], @ContentBuilder then: () -> [Content]) -> Statement {
+ return Statement(compound: condition, first: content(), second: then())
+ }
+
+ /// Unwraps a optional environment value
+ ///
+ /// - Parameters:
+ /// - value: The optional value to unwrap
+ /// - content: The content for some statement
+ ///
+ /// - Returns: A environment statement
+ public static func unwrap(_ value: EnvironmentValue, @ContentBuilder content: (EnvironmentValue) -> [Content]) -> Statement {
+ return Statement(compound: .optional(value), first: content(value), second: [])
+ }
+
+ /// Unwraps a optional environment value
+ ///
+ /// - Parameters:
+ /// - value: The optional value to unwrap
+ /// - content: The content for some statement
+ /// - then: The content for none statement
+ ///
+ /// - Returns: A environment statement
+ public static func unwrap(_ value: EnvironmentValue, @ContentBuilder content: (EnvironmentValue) -> [Content], @ContentBuilder then: () -> [Content]) -> Statement {
+ return Statement(compound: .optional(value), first: content(value), second: then())
+ }
+
+ /// Iterates through a sequence of values
+ ///
+ /// - Parameters:
+ /// - sequence: The sequence to iterate over
+ /// - content: The content for the iteration
+ ///
+ /// - Returns: A environment sequence
+ public static func loop(_ sequence: EnvironmentValue, @ContentBuilder content: (EnvironmentValue) -> [Content]) -> Sequence {
+ return Sequence(value: sequence, content: content(sequence))
}
}
diff --git a/Sources/HTMLKit/Framework/Environment/EnvironmentKeys.swift b/Sources/HTMLKit/Framework/Environment/EnvironmentKeys.swift
index df68b701..39c2154a 100644
--- a/Sources/HTMLKit/Framework/Environment/EnvironmentKeys.swift
+++ b/Sources/HTMLKit/Framework/Environment/EnvironmentKeys.swift
@@ -1,12 +1,19 @@
import Foundation
+/// A set of predefined environment keys
+///
+/// The keys are used on the environment modifiers to configure various settings for the environment.
public struct EnvironmentKeys: Hashable {
- public var calender: Calendar
+ /// A key used to configure the environment's calendar
+ public var calendar: Calendar
+ /// A key used to configure the environment's time zone
public var timeZone: TimeZone
+ /// A key used to configure the environment's locale
public var locale: Locale
+ /// A key used to configure the environment's color scheme
public var colorScheme: String
}
diff --git a/Sources/HTMLKit/Framework/Environment/EnvironmentModifier.swift b/Sources/HTMLKit/Framework/Environment/EnvironmentModifier.swift
index 06a8aab6..46438175 100644
--- a/Sources/HTMLKit/Framework/Environment/EnvironmentModifier.swift
+++ b/Sources/HTMLKit/Framework/Environment/EnvironmentModifier.swift
@@ -1,22 +1,24 @@
-/*
- Abstract:
- The file contains the environment modifier.
- */
-
-/// A type that contains the value and the following content, after modifing the environment.
+/// A type that holds the modification details for the environment
+///
+/// The modifier is received by the renderer and applied to the environment.
@_documentation(visibility: internal)
public struct EnvironmentModifier: Content {
/// The environment key
- public var key: AnyKeyPath
+ internal let key: AnyKeyPath
/// The environment value
- public var value: Any?
+ internal let value: Any?
- /// The following content
- public var content: [Content]
+ /// The sub-content
+ internal let content: [Content]
- /// Initiates a environment modifier
+ /// Initializes an environment modifier
+ ///
+ /// - Parameters:
+ /// - key: The key path of the environment value to be modified
+ /// - value: The new value to update the environment value with
+ /// - content: The sub-content to be rendered
public init(key: AnyKeyPath, value: Any? = nil, content: [Content]) {
self.key = key
diff --git a/Sources/HTMLKit/Framework/Environment/EnvironmentObject.swift b/Sources/HTMLKit/Framework/Environment/EnvironmentObject.swift
index d030f9ce..f47aa845 100644
--- a/Sources/HTMLKit/Framework/Environment/EnvironmentObject.swift
+++ b/Sources/HTMLKit/Framework/Environment/EnvironmentObject.swift
@@ -1,18 +1,20 @@
import Foundation
-/// A property wrapper type to initate an environment object
+/// A property wrapper type to provide access to an environment object
+///
+/// An environment object allows to read shared environment data.
@frozen @propertyWrapper public struct EnvironmentObject {
- /// The wrapped value
+ /// The wrapped object
public var wrappedValue: Wrapper
- /// Converts the type into the wrapped value
+ /// Initialiizes the environment object
public init(_ type: ObjectType.Type) {
self.wrappedValue = .init()
}
- /// A type, that holds the environment object informationen
+ /// A wrapper, that holds the object informationen
@dynamicMemberLookup public struct Wrapper {
/// The path of the parent
@@ -21,20 +23,26 @@ import Foundation
/// The path of the value
internal var path: AnyKeyPath
- /// Initiates a wrapper
+ /// Initializes the wrapper
public init() {
self.path = \WrapperType.self
}
- /// Initiates a wrapper with the necessary information for the environment object
+ /// Initializes the wrapper
+ ///
+ /// - Parameters:
+ /// - parent: The path of the parent
+ /// - path: The path of the value
internal init(parent: AnyKeyPath, path: AnyKeyPath) {
self.parent = parent
self.path = path
}
- /// Looks up for a containing property
+ /// Accesses a wrapped value for a given key path dynamically
+ ///
+ /// - Returns: An environment value
public subscript(dynamicMember member: KeyPath) -> EnvironmentValue {
guard let newPath = self.path.appending(path: member) else {
@@ -48,7 +56,9 @@ import Foundation
return .init(parentPath: self.path, valuePath: newPath)
}
- /// Looks up for a containing model
+ /// Accesses a wrapped model object for a given key path dynamically
+ ///
+ /// - Returns: An environment value
public subscript(dynamicMember member: KeyPath) -> Wrapper where T: ViewModel {
guard let newPath = self.path.appending(path: member) else {
diff --git a/Sources/HTMLKit/Framework/Environment/EnvironmentString.swift b/Sources/HTMLKit/Framework/Environment/EnvironmentString.swift
new file mode 100644
index 00000000..c80b5840
--- /dev/null
+++ b/Sources/HTMLKit/Framework/Environment/EnvironmentString.swift
@@ -0,0 +1,31 @@
+/// A type that represents an environment string
+@_documentation(visibility: internal)
+public struct EnvironmentString: ExpressibleByStringLiteral, ExpressibleByStringInterpolation, Content {
+
+ internal var values: [Content] = []
+
+ public init(stringLiteral: String) {
+ self.values.append(stringLiteral)
+ }
+
+ public init(stringInterpolation: StringInterpolation) {
+ self.values.append(contentsOf: stringInterpolation.values)
+ }
+
+ public struct StringInterpolation: StringInterpolationProtocol {
+
+ internal var values: [Content] = []
+
+ public init(literalCapacity: Int, interpolationCount: Int) {
+ values.reserveCapacity(interpolationCount)
+ }
+
+ public mutating func appendLiteral(_ literal: String) {
+ values.append(literal)
+ }
+
+ public mutating func appendInterpolation(_ env: EnvironmentValue) {
+ values.append(env)
+ }
+ }
+}
diff --git a/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift b/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift
index e9022c67..06afbc0f 100644
--- a/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift
+++ b/Sources/HTMLKit/Framework/Environment/EnvironmentValue.swift
@@ -1,16 +1,23 @@
import Foundation
-/// A type, that acts as a binding value
+/// A type that serves as a placeholder for an environment value
+///
+/// The placeholder will be evaluated and resolved by the renderer when needed.
+@_documentation(visibility: internal)
public struct EnvironmentValue: Content {
/// The path of the values parent
- internal var parentPath: AnyKeyPath
+ internal let parentPath: AnyKeyPath
/// The path of the value
- internal var valuePath: AnyKeyPath
+ internal let valuePath: AnyKeyPath
- /// Initiates a environment value
- public init(parentPath: AnyKeyPath, valuePath: AnyKeyPath) {
+ /// Initializes a environment value
+ ///
+ /// - Parameters:
+ /// - parentPath: The key path of the parent
+ /// - valuePath: The key path of the value
+ internal init(parentPath: AnyKeyPath, valuePath: AnyKeyPath) {
self.parentPath = parentPath
self.valuePath = valuePath
@@ -19,7 +26,36 @@ public struct EnvironmentValue: Content {
extension EnvironmentValue {
- static public func + (lhs: Content, rhs: Self) -> Content {
+ /// Concat environment value with environment value
+ public static func + (lhs: Content, rhs: Self) -> Content {
return [lhs, rhs]
}
+
+ /// Compare an environment value with another comparable value
+ ///
+ /// Makes an unequal evaluation
+ public static func != (lhs: Self, rhs: some Comparable) -> Conditional {
+ return .condition(Condition(lhs: lhs, rhs: rhs, comparison: .unequal))
+ }
+
+ /// Compare an environment value with another comparable value
+ ///
+ /// Makes an equal evaluation
+ public static func == (lhs: Self, rhs: some Comparable) -> Conditional {
+ return .condition(Condition(lhs: lhs, rhs: rhs, comparison: .equal))
+ }
+
+ /// Compare an environment value with another comparable value
+ ///
+ /// Makes an less than evaluation
+ public static func < (lhs: Self, rhs: some Comparable) -> Conditional {
+ return .condition(Condition(lhs: lhs, rhs: rhs, comparison: .less))
+ }
+
+ /// Compare an environment value with another comparable value
+ ///
+ /// Makes an greater than evaluation
+ public static func > (lhs: Self, rhs: some Comparable) -> Conditional {
+ return .condition(Condition(lhs: lhs, rhs: rhs, comparison: .greater))
+ }
}
diff --git a/Sources/HTMLKit/Framework/Environment/Negation.swift b/Sources/HTMLKit/Framework/Environment/Negation.swift
new file mode 100644
index 00000000..042cad6f
--- /dev/null
+++ b/Sources/HTMLKit/Framework/Environment/Negation.swift
@@ -0,0 +1,30 @@
+/// A type thats represents an invert conditional
+@_documentation(visibility: internal)
+public struct Negation {
+
+ /// The left-hand side conditional
+ internal let value: EnvironmentValue
+
+ /// Initializes the negation
+ ///
+ /// - Parameter lhs: The conditional to evaluate
+ public init(value: EnvironmentValue) {
+
+ self.value = value
+ }
+}
+
+/// Creates a invert conditional
+///
+/// ```swift
+/// Environment.when(!value) {
+/// }
+/// ```
+///
+/// - Parameters:
+/// - lhs: The left-hand side conditional
+///
+/// - Returns: A invert conditional
+public prefix func ! (value: EnvironmentValue) -> Conditional {
+ return .negation(Negation(value: value))
+}
diff --git a/Sources/HTMLKit/Framework/Environment/Nullable.swift b/Sources/HTMLKit/Framework/Environment/Nullable.swift
new file mode 100644
index 00000000..4ef47815
--- /dev/null
+++ b/Sources/HTMLKit/Framework/Environment/Nullable.swift
@@ -0,0 +1,9 @@
+/// A type that represent a nullable value.
+///
+/// > Note: This protocol is intended as a temporary workaround.
+@_documentation(visibility: internal)
+internal protocol Nullable {
+
+ /// Checks whether the value is absent without needing to know the underlying type.
+ var isNull: Bool { get }
+}
diff --git a/Sources/HTMLKit/Framework/Environment/Relation.swift b/Sources/HTMLKit/Framework/Environment/Relation.swift
new file mode 100644
index 00000000..fc78dc44
--- /dev/null
+++ b/Sources/HTMLKit/Framework/Environment/Relation.swift
@@ -0,0 +1,138 @@
+/// A type representing the logical relation between two conditionals
+@_documentation(visibility: internal)
+public struct Relation {
+
+ /// A enumeration of potential logical terms
+ public enum Term {
+
+ /// Indicates a conjunction
+ ///
+ /// All conditions must be true for the relation to be true
+ case conjunction
+
+ /// Indicates a disjunction
+ ///
+ /// One condition must be at least true for the relation to be true
+ case disjunction
+ }
+
+ /// The logical term specifying the relation
+ internal let term: Term
+
+ /// The left-hand side conditional
+ internal let lhs: Conditional
+
+ /// The right-hand side conditional
+ internal let rhs: Conditional
+
+ /// Initializes a relation
+ ///
+ /// - Parameters:
+ /// - term: The term on which the relation acts on
+ /// - lhs: The left-hand side conditional
+ /// - rhs: The right-hand side conditional to test against
+ public init(term: Term, lhs: Conditional, rhs: Conditional) {
+
+ self.term = term
+ self.lhs = lhs
+ self.rhs = rhs
+ }
+}
+
+/// Creates a conjunctional relation between two conditionals
+///
+/// ```swift
+/// Environment.when(value > 0 && value) {
+/// }
+/// ```
+///
+/// - Parameters:
+/// - lhs: The left-hand side conditional
+/// - rhs: An environment value
+///
+/// - Returns: A conjunctional relation
+public func && (lhs: Conditional, rhs: EnvironmentValue) -> Conditional {
+ return .relation(Relation(term: .conjunction, lhs: lhs, rhs: .value(rhs)))
+}
+
+/// Creates a conjunctional relation between two conditionals
+///
+/// ```swift
+/// Environment.when(value && value > 0) {
+/// }
+/// ```
+///
+/// - Parameters:
+/// - lhs: The left-hand side conditional
+/// - rhs: An environment value
+///
+/// - Returns: A conjunctional relation
+public func && (lhs: EnvironmentValue, rhs: Conditional) -> Conditional {
+ return .relation(Relation(term: .conjunction, lhs: .value(lhs), rhs: rhs))
+}
+
+
+/// Creates a conjunctional relation between two conditionals
+///
+/// ```swift
+/// Environment.when(value > 0 && value < 2) {
+/// }
+/// ```
+///
+/// - Parameters:
+/// - lhs: The left-hand side conditional
+/// - rhs: The right-hand side conditional
+///
+/// - Returns: A conjunctional relation
+public func && (lhs: Conditional, rhs: Conditional) -> Conditional {
+ return .relation(Relation(term: .conjunction, lhs: lhs, rhs: rhs))
+}
+
+/// Creates a disjunctional relation between two conditionals
+///
+/// ```swift
+/// Environment.when(value > 0 || value) {
+/// }
+/// ```
+///
+/// - Parameters:
+/// - lhs: The left-hand side conditional
+/// - rhs: The right-hand side conditional
+///
+/// - Returns: A disjunctional relation
+public func || (lhs: Conditional, rhs: EnvironmentValue) -> Conditional {
+ return .relation(Relation(term: .disjunction, lhs: lhs, rhs: .value(rhs)))
+}
+
+/// Creates a disjunctional relation between two conditionals
+///
+/// ```swift
+/// Environment.when(value || value > 0) {
+/// }
+/// ```
+///
+/// - Parameters:
+/// - lhs: The left-hand side conditional
+/// - rhs: The right-hand side conditional
+///
+/// - Returns: A disjunctional relation
+public func || (lhs: EnvironmentValue, rhs: Conditional) -> Conditional {
+ return .relation(Relation(term: .disjunction, lhs: .value(lhs), rhs: rhs))
+}
+
+/// Creates a disjunctional relation between two conditionals
+///
+/// ```swift
+/// Environment.when(value > 0 || value < 2) {
+/// }
+/// ```
+///
+/// - Parameters:
+/// - lhs: The left-hand side conditional
+/// - rhs: The right-hand side conditional
+///
+/// - Returns: A disjunctional relation
+public func || (lhs: Conditional, rhs: Conditional) -> Conditional {
+ return .relation(Relation(term: .disjunction, lhs: lhs, rhs: rhs))
+}
+
diff --git a/Sources/HTMLKit/Framework/Environment/Sequence.swift b/Sources/HTMLKit/Framework/Environment/Sequence.swift
new file mode 100644
index 00000000..f591ec81
--- /dev/null
+++ b/Sources/HTMLKit/Framework/Environment/Sequence.swift
@@ -0,0 +1,21 @@
+/// A type representing a loop rendering content within the environment
+@_documentation(visibility: internal)
+public struct Sequence: GlobalElement {
+
+ /// The environment value of the sequence
+ internal let value: EnvironmentValue
+
+ /// The accumulated content
+ internal let content: [Content]
+
+ /// Initializes a loop
+ ///
+ /// - Parameters:
+ /// - value: The environment value to retrieve the sequence from
+ /// - content: The content to render for each item in the sequence
+ public init(value: EnvironmentValue, content: [Content]) {
+
+ self.value = value
+ self.content = content
+ }
+}
diff --git a/Sources/HTMLKit/Framework/Environment/Statement.swift b/Sources/HTMLKit/Framework/Environment/Statement.swift
new file mode 100644
index 00000000..2db7286e
--- /dev/null
+++ b/Sources/HTMLKit/Framework/Environment/Statement.swift
@@ -0,0 +1,26 @@
+/// A type representing a conditional block within the environment
+@_documentation(visibility: internal)
+public struct Statement: GlobalElement {
+
+ /// The compound condition
+ internal let compound: Conditional
+
+ /// The first statement
+ internal let first: [Content]
+
+ /// The second statement
+ internal let second: [Content]
+
+ /// Initializes a statement
+ ///
+ /// - Parameters:
+ /// - compound: The compound of conditionals
+ /// - first: The statement to execute if conditionals are true
+ /// - second: The statement to execute if conditionals are false
+ public init(compound: Conditional, first: [Content], second: [Content]) {
+
+ self.compound = compound
+ self.first = first
+ self.second = second
+ }
+}
diff --git a/Sources/HTMLKit/Framework/Extensions/Comparable+HTMLKit.swift b/Sources/HTMLKit/Framework/Extensions/Comparable+HTMLKit.swift
new file mode 100644
index 00000000..ef966e0f
--- /dev/null
+++ b/Sources/HTMLKit/Framework/Extensions/Comparable+HTMLKit.swift
@@ -0,0 +1,52 @@
+extension Comparable {
+
+ /// Checks for equality
+ ///
+ /// - Parameters:
+ /// - other: The other value to compare
+ ///
+ /// - Returns: The result
+ internal func equal(_ other: Any) -> Bool {
+
+ guard let other = other as? Self else {
+ return false
+ }
+
+ return other == self
+ }
+
+ /// Checks for inequality
+ ///
+ /// - Parameters:
+ /// - other: The other value to compare
+ ///
+ /// - Returns: The result
+ internal func unequal(_ other: Any) -> Bool {
+ return !equal(other)
+ }
+
+ /// Checks for a greater value
+ ///
+ /// - Parameters:
+ /// - other: The other value to compare
+ ///
+ /// - Returns: The result
+ internal func greater(_ other: Any) -> Bool {
+
+ guard let other = other as? Self else {
+ return false
+ }
+
+ return other > self
+ }
+
+ /// Checks for smaller value
+ ///
+ /// - Parameters:
+ /// - other: The other value to compare
+ ///
+ /// - Returns: The result
+ internal func less(_ other: Any) -> Bool {
+ return !greater(other)
+ }
+}
diff --git a/Sources/HTMLKit/Framework/Extensions/Datatypes+Content.swift b/Sources/HTMLKit/Framework/Extensions/Datatypes+Content.swift
index b5444a21..b20c7978 100644
--- a/Sources/HTMLKit/Framework/Extensions/Datatypes+Content.swift
+++ b/Sources/HTMLKit/Framework/Extensions/Datatypes+Content.swift
@@ -15,8 +15,6 @@ extension Float: Content {}
extension Int: Content {}
-extension Optional: Content{}
-
extension String: Content {
static public func + (lhs: Content, rhs: Self) -> Content {
diff --git a/Sources/HTMLKit/Framework/Extensions/Optional+HTMLKit.swift b/Sources/HTMLKit/Framework/Extensions/Optional+HTMLKit.swift
new file mode 100644
index 00000000..d03bc9c4
--- /dev/null
+++ b/Sources/HTMLKit/Framework/Extensions/Optional+HTMLKit.swift
@@ -0,0 +1,9 @@
+extension Optional: Nullable {
+
+ internal var isNull: Bool {
+ return self == nil
+ }
+}
+
+extension Optional: Content {
+}
diff --git a/Sources/HTMLKit/Framework/Primitives/Elements/Element.swift b/Sources/HTMLKit/Framework/Primitives/Elements/Element.swift
index 106e569c..e678ff83 100644
--- a/Sources/HTMLKit/Framework/Primitives/Elements/Element.swift
+++ b/Sources/HTMLKit/Framework/Primitives/Elements/Element.swift
@@ -1,8 +1,3 @@
-/*
- Abstract:
- The file contains the default definition of an element. It defines which properties and methods an element should come with.
- */
-
/// A type that represents any html-element.
@_documentation(visibility: internal)
public protocol Element: Content {
@@ -10,14 +5,31 @@ public protocol Element: Content {
extension Element {
+ /// Sets the environment for the sub-content
+ ///
+ /// - Parameter key: The key
+ ///
+ /// - Returns: The environment modifier
public func environment(key: KeyPath) -> EnvironmentModifier {
return .init(key: key, content: [self])
}
+ /// Supplies a value to the environment
+ ///
+ /// - Parameters:
+ /// - key: The key to store the value with
+ /// - value: The value to be stored
+ ///
+ /// - Returns: The environment modifier
public func environment(key: KeyPath, value: V) -> EnvironmentModifier {
return .init(key: key, value: value, content: [self])
}
+ /// Supplies the object to the environment
+ ///
+ /// - Parameter object: The object to be stored
+ ///
+ /// - Returns: The environment modifier
public func environment(object: T) -> EnvironmentModifier {
return .init(key: \T.self, value: object, content: [self])
}
diff --git a/Sources/HTMLKit/Framework/Rendering/Renderer.swift b/Sources/HTMLKit/Framework/Rendering/Renderer.swift
index 56ba9c6a..503f28c6 100644
--- a/Sources/HTMLKit/Framework/Rendering/Renderer.swift
+++ b/Sources/HTMLKit/Framework/Rendering/Renderer.swift
@@ -9,40 +9,6 @@ import Logging
@_documentation(visibility: internal)
public final class Renderer {
-
- /// 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
-
- /// A brief error description.
- public var description: String {
-
- switch self {
- case .unableToCastEnvironmentValue:
- return "Unable to cast the environment value."
-
- case .unindendedEnvironmentKey:
- return "The environment key is not indended."
-
- case .environmentValueNotFound:
- return "Unable to retrieve environment value."
-
- case .environmentObjectNotFound:
- return "Unable to retrieve environment object."
- }
- }
- }
/// The context environment
private var environment: Environment
@@ -136,6 +102,14 @@ public final class Renderer {
result += escape(content: try render(value: value))
}
+ if let statement = content as? Statement {
+ result += try render(statement: statement)
+ }
+
+ if let loop = content as? Sequence {
+ result += try render(loop: loop)
+ }
+
if let string = content as? MarkdownString {
if !features.contains(.markdown) {
@@ -146,6 +120,10 @@ public final class Renderer {
}
}
+ if let envstring = content as? EnvironmentString {
+ result += try render(envstring: envstring)
+ }
+
if let element = content as? String {
result += escape(content: element)
}
@@ -211,6 +189,14 @@ public final class Renderer {
result += escape(content: try render(value: value))
}
+ if let loop = content as? Sequence {
+ result += try render(loop: loop)
+ }
+
+ if let statement = content as? Statement {
+ result += try render(statement: statement)
+ }
+
if let string = content as? MarkdownString {
if !features.contains(.markdown) {
@@ -221,6 +207,10 @@ public final class Renderer {
}
}
+ if let envstring = content as? EnvironmentString {
+ result += try render(envstring: envstring)
+ }
+
if let element = content as? String {
result += escape(content: element)
}
@@ -329,6 +319,14 @@ public final class Renderer {
result += escape(content: try render(value: value))
}
+ if let statement = content as? Statement {
+ result += try render(statement: statement)
+ }
+
+ if let loop = content as? Sequence {
+ result += try render(loop: loop)
+ }
+
if let string = content as? MarkdownString {
if !features.contains(.markdown) {
@@ -339,6 +337,10 @@ public final class Renderer {
}
}
+ if let envstring = content as? EnvironmentString {
+ result += try render(envstring: envstring)
+ }
+
if let element = content as? String {
result += escape(content: element)
}
@@ -386,31 +388,6 @@ public final class Renderer {
if let value = modifier.value {
self.environment.upsert(value, for: modifier.key)
-
- } else {
-
- if let value = self.environment.retrieve(for: modifier.key) {
-
- if let key = modifier.key as? PartialKeyPath {
-
- switch key {
- case \.locale:
- self.environment.locale = value as? Locale
-
- case \.calender:
- self.environment.calendar = value as? Calendar
-
- case \.timeZone:
- self.environment.timeZone = value as? TimeZone
-
- case \.colorScheme:
- self.environment.colorScheme = value as? String
-
- default:
- throw Errors.unindendedEnvironmentKey
- }
- }
- }
}
return try render(contents: modifier.content)
@@ -419,13 +396,7 @@ public final class Renderer {
/// Renders a environment value.
private func render(value: EnvironmentValue) throws -> String {
- guard let parent = self.environment.retrieve(for: value.parentPath) else {
- throw Errors.environmentObjectNotFound
- }
-
- guard let value = parent[keyPath: value.valuePath] else {
- throw Errors.environmentValueNotFound
- }
+ let value = try self.environment.resolve(value: value)
switch value {
case let floatValue as Float:
@@ -450,9 +421,42 @@ public final class Renderer {
return formatter.string(from: dateValue)
default:
- throw Errors.unableToCastEnvironmentValue
+ throw Environment.Errors.unableToCastEnvironmentValue
}
}
+
+ /// Renders a environment statement
+ ///
+ /// - Parameter statement: The statement to resolve
+ ///
+ /// - Returns: The rendered condition
+ private func render(statement: Statement) throws -> String {
+
+ var result = false
+
+ switch statement.compound {
+ case .optional(let optional):
+ result = try environment.evaluate(optional: optional)
+
+ case .value(let value):
+ result = try environment.evaluate(value: value)
+
+ case .condition(let condition):
+ result = try environment.evaluate(condition: condition)
+
+ case .negation(let negation):
+ result = try environment.evaluate(negation: negation)
+
+ case .relation(let relation):
+ result = try environment.evaluate(relation: relation)
+ }
+
+ if result {
+ return try render(contents: statement.first)
+ }
+
+ return try render(contents: statement.second)
+ }
/// Renders the node attributes.
private func render(attributes: OrderedDictionary) throws -> String {
@@ -481,6 +485,11 @@ public final class Renderer {
return self.markdown.render(string: escape(content: markdown.raw))
}
+ /// Renders a environment interpolation
+ private func render(envstring: EnvironmentString) throws -> String {
+ return try render(contents: envstring.values)
+ }
+
/// Converts specific charaters into encoded values.
private func escape(attribute value: String) -> String {
@@ -503,4 +512,152 @@ public final class Renderer {
return value
}
+
+ /// Renders an environment loop
+ ///
+ /// - Parameter loop: The loop to resolve
+ ///
+ /// - Returns: The rendered loop
+ private func render(loop: Sequence) throws -> String {
+
+ let value = try environment.resolve(value: loop.value)
+
+ guard let sequence = value as? (any Swift.Sequence) else {
+ throw Environment.Errors.unableToCastEnvironmentValue
+ }
+
+ var result = ""
+
+ for value in sequence {
+ try render(loop: loop.content, with: value, on: &result)
+ }
+
+ return result
+ }
+
+ /// Renders the content within an environment loop
+ ///
+ /// - Parameters:
+ /// - contents: The content to render
+ /// - value: The value to resolve the environment value with
+ /// - result: The rendered content
+ private func render(loop contents: [Content], with value: Any, on result: inout String) throws {
+
+ for content in contents {
+
+ if let element = content as? (any ContentNode) {
+ try render(loop: element, with: value, on: &result)
+ }
+
+ if let element = content as? (any CustomNode) {
+ try render(loop: element, with: value, on: &result)
+ }
+
+ if let element = content as? (any EmptyNode) {
+ result += try render(element: element)
+ }
+
+ if let element = content as? (any CommentNode) {
+ result += render(element: element)
+ }
+
+ if let string = content as? LocalizedString {
+ result += try render(localized: string)
+ }
+
+ if let string = content as? MarkdownString {
+
+ if !features.contains(.markdown) {
+ result += escape(content: string.raw)
+
+ } else {
+ result += try render(markdown: string)
+ }
+ }
+
+ if let envstring = content as? EnvironmentString {
+ result += try render(envstring: envstring)
+ }
+
+ if let element = content as? String {
+ result += escape(content: element)
+ }
+
+ if content is EnvironmentValue {
+
+ switch value {
+ case let floatValue as Float:
+ result += String(floatValue)
+
+ case let intValue as Int:
+ result += String(intValue)
+
+ case let doubleValue as Double:
+ result += String(doubleValue)
+
+ case let stringValue as String:
+ result += stringValue
+
+ default:
+ break
+ }
+ }
+ }
+ }
+
+ /// Renders a content element within an environment loop
+ ///
+ /// - Parameters:
+ /// - element: The element to render
+ /// - value: The value to resolve the environment value with
+ /// - result: The result
+ private func render(loop element: some ContentNode, with value: Any, on result: inout String) throws {
+
+ result += "<\(element.name)"
+
+ if let attributes = element.attributes {
+ result += try render(attributes: attributes)
+ }
+
+ result += ">"
+
+ if let contents = element.content as? [Content] {
+ try render(loop: contents, with: value, on: &result)
+ }
+
+ result += "\(element.name)>"
+ }
+
+ /// Renders a custom element within an environment loop
+ ///
+ /// - Parameters:
+ /// - element: The element to render
+ /// - value: The value to resolve the environment value with
+ /// - result: The rendered content
+ private func render(loop element: some CustomNode, with value: Any, on result: inout String) throws {
+
+ result += "<\(element.name)"
+
+ if let attributes = element.attributes {
+ result += try render(attributes: attributes)
+ }
+
+ result += ">"
+
+ if let contents = element.content as? [Content] {
+ try render(loop: contents, with: value, on: &result)
+ }
+
+ result += "\(element.name)>"
+ }
+
+ /// Renders an environment string within a environment loop
+ ///
+ /// - Parameters:
+ /// - envstring: The environment string to render
+ /// - value: The raw value to resolve the environment value with
+ /// - result: The result
+ private func render(loop envstring: EnvironmentString, with value: Any, on result: inout String) throws {
+ try render(loop: envstring.values, with: value, on: &result)
+ }
}
diff --git a/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift b/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift
index fd8f5bfa..311adc2c 100644
--- a/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift
+++ b/Sources/HTMLKitVapor/Extensions/Vapor+HTMLKit.swift
@@ -112,14 +112,14 @@ extension Request {
public var htmlkit: ViewRenderer {
if let acceptLanguage = self.acceptLanguage {
- self.application.htmlkit.environment.locale = HTMLKit.Locale(tag: acceptLanguage)
+ self.application.htmlkit.environment.upsert(HTMLKit.Locale(tag: acceptLanguage), for: \HTMLKit.EnvironmentKeys.locale)
}
return .init(eventLoop: self.eventLoop, configuration: self.application.htmlkit.configuration, logger: self.logger)
}
}
-extension HTMLKit.Renderer.Errors: AbortError {
+extension HTMLKit.Environment.Errors: AbortError {
@_implements(AbortError, reason)
public var abortReason: String { self.description }
@@ -127,7 +127,7 @@ extension HTMLKit.Renderer.Errors: AbortError {
public var status: HTTPResponseStatus { .internalServerError }
}
-extension HTMLKit.Renderer.Errors: DebuggableError {
+extension HTMLKit.Environment.Errors: DebuggableError {
@_implements(DebuggableError, reason)
public var debuggableReason: String { self.description }
diff --git a/Tests/HTMLKitTests/EnvironmentTests.swift b/Tests/HTMLKitTests/EnvironmentTests.swift
index 85176dd9..700cee9a 100644
--- a/Tests/HTMLKitTests/EnvironmentTests.swift
+++ b/Tests/HTMLKitTests/EnvironmentTests.swift
@@ -1,66 +1,416 @@
-/*
- Abstract:
- The file tests the annotations.
- */
-
import HTMLKit
import XCTest
final class EnvironmentTests: XCTestCase {
- struct Object: Encodable {
+ var renderer = Renderer()
+
+ /// Tests the environment access through the environment object
+ ///
+ /// The renderer is expected to evaluate the placeholder and renderer the resulting value.
+ func testEnvironmentAccess() throws {
+
+ struct FamilyObject: ViewModel {
+
+ let name = "Doe"
+ let father = FatherObject()
+ }
+
+ struct FatherObject: ViewModel {
+
+ let avatar = "john_doe.jpeg"
+ let name = "John"
+ }
+
+ struct ParentView: View {
+
+ let content: [Content]
+
+ init(@ContentBuilder content: () -> [Content]) {
+ self.content = content()
+ }
+
+ var body: Content {
+ Division {
+ content
+ }
+ .environment(object: FamilyObject())
+ }
+ }
+
+ struct ChildView: View {
+
+ @EnvironmentObject(FamilyObject.self)
+ var object
+
+ var body: Content {
+ ParentView {
+ Section{
+ Image()
+ .source(object.father.avatar)
+ Heading1 {
+ object.name
+ }
+ Paragraph {
+ object.father.name + " " + object.name
+ }
+ }
+ }
+ }
+ }
+
+ XCTAssertEqual(try renderer.render(view: ChildView()),
+ """
+ \
+
\
+ \
+ Doe
\
+ John Doe
\
+ \
+
+ """
+ )
+ }
+
+ /// Tests condtion evaluation for the environment
+ ///
+ /// The renderer is expected to evaluated the condition correctly and renderer the right statement based on the condition.
+ func testEnvironmentCondition() throws {
+
+ struct TestObject: ViewModel {
+
+ let firstName = "Jane"
+ let age = 40
+ let loggedIn = true
+ }
+
+ struct TestView: View {
+
+ @EnvironmentObject(TestObject.self)
+ var object
+
+ var body: Content {
+ Paragraph {
+
+ // Should return true
+ Environment.check(object.loggedIn) {
+ "True"
+ }
+
+ // Should return false
+ Environment.check(!object.loggedIn) {
+ "True"
+ } then: {
+ "False"
+ }
+
+ // Should return false
+ Environment.check(object.firstName == "John") {
+ "True"
+ } then: {
+ "False"
+ }
+
+ // The counter test, should return true
+ Environment.check(object.firstName == "Jane") {
+ "True"
+ }
+
+ // Should return true
+ Environment.check(object.firstName != "John") {
+ "True"
+ }
+
+ // The counter test, should return false
+ Environment.check(object.firstName != "Jane") {
+ "True"
+ } then: {
+ "False"
+ }
+
+ // Should return false
+ Environment.check(object.age > 41) {
+ "True"
+ } then: {
+ "False"
+ }
+
+ // The counter test, should return true
+ Environment.check(object.age > 39) {
+ "True"
+ }
+
+ // Should return true
+ Environment.check(object.age < 41) {
+ "True"
+ }
+
+ // The counter test, should return false
+ Environment.check(object.age < 39) {
+ "True"
+ } then: {
+ "False"
+ }
+ }
+ .environment(object: TestObject())
+ }
+ }
+
+ XCTAssertEqual(try renderer.render(view: TestView()),
+ """
+ TrueFalseFalseTrueTrueFalseFalseTrueTrueFalse
+ """
+ )
+ }
+
+ /// Tests the evaluation of a statement with a conjunctional relation
+ func testConditionConjuction() throws {
+
+ struct TestObject: ViewModel {
+
+ let age = 40
+ }
+
+ struct TestView: View {
+
+ @EnvironmentObject(TestObject.self)
+ var object
+
+ var body: Content {
+ Paragraph {
+
+ // The relation is true, cause both conditions are true
+ Environment.check(object.age > 39 && object.age < 41) {
+ "True"
+ } then: {
+ "False"
+ }
+
+ // The relation is false, cause both conditions are false
+ Environment.check(object.age < 39 && object.age > 41) {
+ "True"
+ } then: {
+ "False"
+ }
+
+ // The relation is false, cause the first condition is false
+ Environment.check(object.age > 41 && object.age > 39) {
+ "True"
+ } then: {
+ "False"
+ }
+
+ // The relation is false, cause the second condition is false
+ Environment.check(object.age > 39 && object.age > 41) {
+ "True"
+ } then: {
+ "False"
+ }
+ }
+ .environment(object: TestObject())
+ }
+ }
- var title: String = "Welcome to WWDC"
- var name: String = "Mattes!"
- var image: String = "wwdc.jpeg"
+ XCTAssertEqual(try renderer.render(view: TestView()),
+ """
+ TrueFalseFalseFalse
+ """
+ )
}
- struct ParentView: View {
+ /// Tests the evaluation of a statement with a disjunctional relation
+ func testConditionDisjunction() throws {
+
+ struct TestObject: ViewModel {
+
+ let age = 40
+ }
+
+ struct TestView: View {
+
+ @EnvironmentObject(TestObject.self)
+ var object
+
+ var body: Content {
+ Paragraph {
+
+ // The relation is true, cause the second condition is true
+ Environment.check(object.age > 41 || object.age == 40) {
+ "True"
+ } then: {
+ "False"
+ }
+
+ // The relation is true, cause the first condition is true
+ Environment.check(object.age == 40 || object.age > 41) {
+ "True"
+ } then: {
+ "False"
+ }
+
+ // The relation is false, cause all conditions are false
+ Environment.check(object.age == 50 || object.age > 41) {
+ "True"
+ } then: {
+ "False"
+ }
+ }
+ .environment(object: TestObject())
+ }
+ }
- var content: [Content]
+ XCTAssertEqual(try renderer.render(view: TestView()),
+ """
+ TrueTrueFalse
+ """
+ )
+ }
+
+ /// Tests the evaluation of various relation combinations
+ func testConditionConjunctionAndDisjunction() throws {
- init(@ContentBuilder content: () -> [Content]) {
- self.content = content()
+ struct TestObject: ViewModel {
+
+ let age = 40
}
- var body: Content {
- Division {
- content
+ struct TestView: View {
+
+ @EnvironmentObject(TestObject.self)
+ var object
+
+ var body: Content {
+ Paragraph {
+
+ // The statement is true, cause the first relation is true
+ Environment.check(object.age == 40 || object.age > 41 && object.age < 39) {
+ "True"
+ } then: {
+ "False"
+ }
+
+ // The statement is true, cause the second relation is true
+ Environment.check(object.age == 50 || object.age < 41 && object.age > 39) {
+ "True"
+ } then: {
+ "False"
+ }
+ }
+ .environment(object: TestObject())
}
- .environment(object: Object())
}
+
+ XCTAssertEqual(try renderer.render(view: TestView()),
+ """
+ TrueTrue
+ """
+ )
}
- struct ChildView: View {
+ /// Tests the iteration over a sequence of environment values
+ func testEnvironmentLoop() throws {
- @EnvironmentObject(Object.self)
- var object
+ struct TestObject: ViewModel {
+
+ let children = ["Janek", "Janet"]
+ }
- var body: Content {
- ParentView {
- Section{
- Image()
- .source(object.image)
- Heading2 {
- object.title + " " + object.name
+ struct TestView: View {
+
+ @EnvironmentObject(TestObject.self)
+ var object
+
+ var body: Content {
+ Paragraph {
+ Environment.loop(object.children) { child in
+ Paragraph {
+ child
+ }
}
}
+ .environment(object: TestObject())
}
}
+
+ XCTAssertEqual(try renderer.render(view: TestView()),
+ """
+ \
+
Janek
\
+ Janet
\
+
+ """
+ )
}
- var renderer = Renderer()
+ /// Tests the conditional rendering based on an optional environment value
+ ///
+ /// The renderer is expected to evaluate the presence of the value and render the right content
+ /// accordingly.
+ func testEnvironmentUnwrap() throws {
+
+ struct TestObject: ViewModel {
+
+ let some: String? = "Some"
+ let none: String? = nil
+ }
+
+ struct TestView: View {
+
+ @EnvironmentObject(TestObject.self)
+ var object
+
+ var body: Content {
+ Paragraph {
+
+ // Should return none, cause the value is nil
+ Environment.unwrap(object.none) { value in
+ value
+ } then: {
+ "None"
+ }
+
+ // Should return the value
+ Environment.unwrap(object.some) { value in
+ value
+ }
+ }
+ .environment(object: TestObject())
+ }
+ }
+
+ XCTAssertEqual(try renderer.render(view: TestView()),
+ """
+ NoneSome
+ """
+ )
+ }
- func testEnvironment() throws {
+ /// Tests the string interpolation with an environment value
+ ///
+ /// The renderer is expected to render the string correctly
+ func testStringInterpolationWithEnvironment() throws {
- XCTAssertEqual(try renderer.render(view: ChildView()),
+ struct TestObject: ViewModel {
+
+ let name = "Jane"
+ }
+
+ struct TestView: View {
+
+ @EnvironmentObject(TestObject.self)
+ var object
+
+ var body: Content {
+ Paragraph {
+ EnvironmentString("Hello, how are you \(object.name)?")
+ }
+ .environment(object: TestObject())
+ }
+ }
+
+ XCTAssertEqual(try renderer.render(view: TestView()),
"""
- \
-
\
- \
- Welcome to WWDC Mattes!
\
- \
-
+ Hello, how are you Jane?
"""
)
}
diff --git a/Tests/HTMLKitVaporTests/ProviderTests.swift b/Tests/HTMLKitVaporTests/ProviderTests.swift
index 517f46a8..5940ebcc 100644
--- a/Tests/HTMLKitVaporTests/ProviderTests.swift
+++ b/Tests/HTMLKitVaporTests/ProviderTests.swift
@@ -42,7 +42,6 @@ final class ProviderTests: XCTestCase {
content
}
}
- .environment(object: TestObject())
}
}
@@ -332,12 +331,17 @@ final class ProviderTests: XCTestCase {
}
}
+ /// Tests the access to environment through provider
+ ///
+ /// The provider is expected to recieve the environment object and resolve it based on the request.
func testEnvironmentIntegration() throws {
let app = Application(.testing)
defer { app.shutdown() }
+ app.htmlkit.environment.upsert(TestObject(), for: \TestObject.self)
+
app.get("test") { request async throws -> Vapor.View in
return try await request.htmlkit.render(TestPage.SipplingView())
}
@@ -389,4 +393,76 @@ final class ProviderTests: XCTestCase {
)
}
}
+
+ /// Tests the error reporting to Vapor for issues that may occur during environment access.
+ ///
+ /// The error is expected to be classified as an internal server error and includes a error message.
+ func testEnvironmentErrorReporting() throws {
+
+ struct TestObject {
+
+ let firstName = "Jane"
+ }
+
+ struct UnkownObject: HTMLKit.View {
+
+ @EnvironmentObject(TestObject.self)
+ var object
+
+ var body: HTMLKit.Content {
+ Paragraph {
+ Environment.check(object.firstName) {
+ "True"
+ }
+ }
+ }
+ }
+
+ struct WrongCast: HTMLKit.View {
+
+ @EnvironmentObject(TestObject.self)
+ var object
+
+ var body: HTMLKit.Content {
+ Paragraph {
+ Environment.check(object.firstName) {
+ "True"
+ }
+ }
+ .environment(object: TestObject())
+ }
+ }
+
+ let app = Application(.testing)
+
+ defer { app.shutdown() }
+
+ app.get("unkownobject") { request async throws -> Vapor.View in
+
+ return try await request.htmlkit.render(UnkownObject())
+ }
+
+ app.get("wrongcast") { request async throws -> Vapor.View in
+
+ return try await request.htmlkit.render(WrongCast())
+ }
+
+ try app.test(.GET, "unkownobject") { response in
+
+ XCTAssertEqual(response.status, .internalServerError)
+
+ let abort = try response.content.decode(AbortResponse.self)
+
+ XCTAssertEqual(abort.reason, "Unable to retrieve environment object.")
+ }
+
+ try app.test(.GET, "wrongcast") { response in
+
+ XCTAssertEqual(response.status, .internalServerError)
+
+ let abort = try response.content.decode(AbortResponse.self)
+
+ XCTAssertEqual(abort.reason, "Unable to cast the environment value.")
+ }
+ }
}