diff --git a/Sources/Stencil/Context.swift b/Sources/Stencil/Context.swift index 07136f5..21b2a4e 100644 --- a/Sources/Stencil/Context.swift +++ b/Sources/Stencil/Context.swift @@ -11,19 +11,21 @@ public class Context { /// The context's environment, such as registered extensions, classes, … public let environment: Environment + init(dictionaries: [[String: Any?]], environment: Environment) { + self.dictionaries = dictionaries + self.environment = environment + } + /// Create a context from a dictionary (and an env.) /// /// - Parameters: /// - dictionary: The context's data /// - environment: Environment such as extensions, … - public init(dictionary: [String: Any] = [:], environment: Environment? = nil) { - if !dictionary.isEmpty { - dictionaries = [dictionary] - } else { - dictionaries = [] - } - - self.environment = environment ?? Environment() + public convenience init(dictionary: [String: Any] = [:], environment: Environment? = nil) { + self.init( + dictionaries: dictionary.isEmpty ? [] : [dictionary], + environment: environment ?? Environment() + ) } /// Access variables in this context by name diff --git a/Sources/Stencil/LazyValueWrapper.swift b/Sources/Stencil/LazyValueWrapper.swift new file mode 100644 index 0000000..fc9cd35 --- /dev/null +++ b/Sources/Stencil/LazyValueWrapper.swift @@ -0,0 +1,69 @@ +// +// Stencil +// Copyright © 2022 Stencil +// MIT Licence +// + +/// Used to lazily set context data. Useful for example if you have some data that requires heavy calculations, and may +/// not be used in every render possiblity. +public final class LazyValueWrapper { + private let closure: (Context) throws -> Any + private let context: Context? + private var cachedValue: Any? + + /// Create a wrapper that'll use a **reference** to the current context. + /// This means when the closure is evaluated, it'll use the **active** context at that moment. + /// + /// - Parameters: + /// - closure: The closure to lazily evaluate + public init(closure: @escaping (Context) throws -> Any) { + self.context = nil + self.closure = closure + } + + /// Create a wrapper that'll create a **copy** of the current context. + /// This means when the closure is evaluated, it'll use the context **as it was** when this wrapper was created. + /// + /// - Parameters: + /// - context: The context to use during evaluation + /// - closure: The closure to lazily evaluate + /// - Note: This will use more memory than the other `init` as it needs to keep a copy of the full context around. + public init(copying context: Context, closure: @escaping (Context) throws -> Any) { + self.context = Context(dictionaries: context.dictionaries, environment: context.environment) + self.closure = closure + } + + /// Shortcut for creating a lazy wrapper when you don't need access to the Stencil context. + /// + /// - Parameters: + /// - closure: The closure to lazily evaluate + public init(_ closure: @autoclosure @escaping () throws -> Any) { + self.context = nil + self.closure = { _ in try closure() } + } +} + +extension LazyValueWrapper { + func value(context: Context) throws -> Any { + if let value = cachedValue { + return value + } else { + let value = try closure(self.context ?? context) + cachedValue = value + return value + } + } +} + +extension LazyValueWrapper: Resolvable { + public func resolve(_ context: Context) throws -> Any? { + let value = try self.value(context: context) + return try (value as? Resolvable)?.resolve(context) ?? value + } +} + +extension LazyValueWrapper: Normalizable { + public func normalize() -> Any? { + (cachedValue as? Normalizable)?.normalize() ?? cachedValue + } +} diff --git a/Sources/Stencil/Variable.swift b/Sources/Stencil/Variable.swift index 5fe5c10..1948f4e 100644 --- a/Sources/Stencil/Variable.swift +++ b/Sources/Stencil/Variable.swift @@ -78,6 +78,8 @@ public struct Variable: Equatable, Resolvable { if current == nil { return nil + } else if let lazyCurrent = current as? LazyValueWrapper { + current = try lazyCurrent.value(context: context) } }