From 25d1507159ad317fc81efb5ca7898dbb57575389 Mon Sep 17 00:00:00 2001 From: "T. R. Bernstein" <137705289+trbernstein@users.noreply.github.com> Date: Tue, 30 Sep 2025 19:02:50 +0200 Subject: [PATCH] refactor: Use tabs for indent --- Sources/Stencil/Context.swift | 180 +-- Sources/Stencil/DynamicMemberLookup.swift | 22 +- Sources/Stencil/Environment.swift | 150 +-- Sources/Stencil/Errors.swift | 114 +- Sources/Stencil/Expression.swift | 460 ++++---- Sources/Stencil/Extension.swift | 160 +-- Sources/Stencil/FilterTag.swift | 52 +- Sources/Stencil/Filters.swift | 186 +-- Sources/Stencil/ForTag.swift | 452 ++++---- Sources/Stencil/IfTag.swift | 508 ++++----- Sources/Stencil/Include.swift | 78 +- Sources/Stencil/Inheritance.swift | 250 ++-- Sources/Stencil/KeyPath.swift | 178 +-- Sources/Stencil/LazyValueWrapper.swift | 88 +- Sources/Stencil/Lexer.swift | 416 +++---- Sources/Stencil/Loader.swift | 172 +-- Sources/Stencil/Node.swift | 266 ++--- Sources/Stencil/NowTag.swift | 62 +- Sources/Stencil/Parser.swift | 440 +++---- Sources/Stencil/Template.swift | 124 +- Sources/Stencil/Tokenizer.swift | 234 ++-- Sources/Stencil/TrimBehaviour.swift | 116 +- Sources/Stencil/Variable.swift | 422 +++---- Tests/StencilTests/ContextSpec.swift | 246 ++-- .../EnvironmentBaseAndChildTemplateSpec.swift | 208 ++-- .../EnvironmentIncludeTemplateSpec.swift | 142 +-- Tests/StencilTests/EnvironmentSpec.swift | 358 +++--- Tests/StencilTests/ExpressionSpec.swift | 698 ++++++------ Tests/StencilTests/FilterSpec.swift | 716 ++++++------ Tests/StencilTests/FilterTagSpec.swift | 88 +- Tests/StencilTests/ForNodeSpec.swift | 1014 ++++++++--------- Tests/StencilTests/Helpers.swift | 82 +- Tests/StencilTests/IfNodeSpec.swift | 454 ++++---- Tests/StencilTests/IncludeSpec.swift | 110 +- Tests/StencilTests/InheritanceSpec.swift | 108 +- Tests/StencilTests/LexerSpec.swift | 236 ++-- Tests/StencilTests/LoaderSpec.swift | 76 +- Tests/StencilTests/NodeSpec.swift | 190 +-- Tests/StencilTests/NowNodeSpec.swift | 78 +- Tests/StencilTests/ParserSpec.swift | 118 +- Tests/StencilTests/StencilSpec.swift | 94 +- Tests/StencilTests/TemplateSpec.swift | 24 +- Tests/StencilTests/TokenSpec.swift | 46 +- Tests/StencilTests/VariableSpec.swift | 630 +++++----- 44 files changed, 5423 insertions(+), 5423 deletions(-) diff --git a/Sources/Stencil/Context.swift b/Sources/Stencil/Context.swift index 7331b35..ca222a8 100644 --- a/Sources/Stencil/Context.swift +++ b/Sources/Stencil/Context.swift @@ -1,106 +1,106 @@ /// A container for template variables. public class Context { - var dictionaries: [[String: Any?]] + var dictionaries: [[String: Any?]] - /// The context's environment, such as registered extensions, classes, … - public let environment: Environment + /// 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 - } + 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 convenience init(dictionary: [String: Any] = [:], environment: Environment? = nil) { - self.init( - dictionaries: dictionary.isEmpty ? [] : [dictionary], - environment: environment ?? Environment() - ) - } + /// Create a context from a dictionary (and an env.) + /// + /// - Parameters: + /// - dictionary: The context's data + /// - environment: Environment such as extensions, … + 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 - public subscript(key: String) -> Any? { - /// Retrieves a variable's value, starting at the current context and going upwards - get { - for dictionary in Array(dictionaries.reversed()) { - if let value = dictionary[key] { - return value - } - } + /// Access variables in this context by name + public subscript(key: String) -> Any? { + /// Retrieves a variable's value, starting at the current context and going upwards + get { + for dictionary in Array(dictionaries.reversed()) { + if let value = dictionary[key] { + return value + } + } - return nil - } + return nil + } - /// Set a variable in the current context, deleting the variable if it's nil - set(value) { - if var dictionary = dictionaries.popLast() { - dictionary[key] = value - dictionaries.append(dictionary) - } - } - } + /// Set a variable in the current context, deleting the variable if it's nil + set(value) { + if var dictionary = dictionaries.popLast() { + dictionary[key] = value + dictionaries.append(dictionary) + } + } + } - /// Push a new level into the Context - /// - /// - Parameters: - /// - dictionary: The new level data - fileprivate func push(_ dictionary: [String: Any] = [:]) { - dictionaries.append(dictionary) - } + /// Push a new level into the Context + /// + /// - Parameters: + /// - dictionary: The new level data + fileprivate func push(_ dictionary: [String: Any] = [:]) { + dictionaries.append(dictionary) + } - /// Pop the last level off of the Context - /// - /// - returns: The popped level - // swiftlint:disable:next discouraged_optional_collection - fileprivate func pop() -> [String: Any?]? { - dictionaries.popLast() - } + /// Pop the last level off of the Context + /// + /// - returns: The popped level + // swiftlint:disable:next discouraged_optional_collection + fileprivate func pop() -> [String: Any?]? { + dictionaries.popLast() + } - /// Push a new level onto the context for the duration of the execution of the given closure - /// - /// - Parameters: - /// - dictionary: The new level data - /// - closure: The closure to execute - /// - returns: Return value of the closure - public func push(dictionary: [String: Any] = [:], closure: (() throws -> Result)) rethrows -> Result { - push(dictionary) - defer { _ = pop() } - return try closure() - } + /// Push a new level onto the context for the duration of the execution of the given closure + /// + /// - Parameters: + /// - dictionary: The new level data + /// - closure: The closure to execute + /// - returns: Return value of the closure + public func push(dictionary: [String: Any] = [:], closure: (() throws -> Result)) rethrows -> Result { + push(dictionary) + defer { _ = pop() } + return try closure() + } - /// Flatten all levels of context data into 1, merging duplicate variables - /// - /// - returns: All collected variables - public func flatten() -> [String: Any] { - var accumulator: [String: Any] = [:] + /// Flatten all levels of context data into 1, merging duplicate variables + /// + /// - returns: All collected variables + public func flatten() -> [String: Any] { + var accumulator: [String: Any] = [:] - for dictionary in dictionaries { - for (key, value) in dictionary { - if let value = value { - accumulator.updateValue(value, forKey: key) - } - } - } + for dictionary in dictionaries { + for (key, value) in dictionary { + if let value = value { + accumulator.updateValue(value, forKey: key) + } + } + } - return accumulator - } + return accumulator + } - /// Cache result of block by its name in the context top-level, so that it can be later rendered - /// via `{{ block.name }}` - /// - /// - Parameters: - /// - name: The name of the stored block - /// - content: The block's rendered content - public func cacheBlock(_ name: String, content: String) { - if var block = dictionaries.first?["block"] as? [String: String] { - block[name] = content - dictionaries[0]["block"] = block - } else { - dictionaries.insert(["block": [name: content]], at: 0) - } - } + /// Cache result of block by its name in the context top-level, so that it can be later rendered + /// via `{{ block.name }}` + /// + /// - Parameters: + /// - name: The name of the stored block + /// - content: The block's rendered content + public func cacheBlock(_ name: String, content: String) { + if var block = dictionaries.first?["block"] as? [String: String] { + block[name] = content + dictionaries[0]["block"] = block + } else { + dictionaries.insert(["block": [name: content]], at: 0) + } + } } diff --git a/Sources/Stencil/DynamicMemberLookup.swift b/Sources/Stencil/DynamicMemberLookup.swift index be1eec8..668f8ec 100644 --- a/Sources/Stencil/DynamicMemberLookup.swift +++ b/Sources/Stencil/DynamicMemberLookup.swift @@ -1,18 +1,18 @@ /// Marker protocol so we can know which types support `@dynamicMemberLookup`. Add this to your own types that support /// lookup by String. public protocol DynamicMemberLookup { - /// Get a value for a given `String` key - subscript(dynamicMember member: String) -> Any? { get } + /// Get a value for a given `String` key + subscript(dynamicMember member: String) -> Any? { get } } public extension DynamicMemberLookup where Self: RawRepresentable { - /// Get a value for a given `String` key - subscript(dynamicMember member: String) -> Any? { - switch member { - case "rawValue": - return rawValue - default: - return nil - } - } + /// Get a value for a given `String` key + subscript(dynamicMember member: String) -> Any? { + switch member { + case "rawValue": + return rawValue + default: + return nil + } + } } diff --git a/Sources/Stencil/Environment.swift b/Sources/Stencil/Environment.swift index e758de3..273eeda 100644 --- a/Sources/Stencil/Environment.swift +++ b/Sources/Stencil/Environment.swift @@ -1,84 +1,84 @@ /// Container for environment data, such as registered extensions public struct Environment { - /// The class for loading new templates - public let templateClass: Template.Type - /// List of registered extensions - public var extensions: [Extension] - /// How to handle whitespace - public var trimBehaviour: TrimBehaviour - /// Mechanism for loading new files - public var loader: Loader? + /// The class for loading new templates + public let templateClass: Template.Type + /// List of registered extensions + public var extensions: [Extension] + /// How to handle whitespace + public var trimBehaviour: TrimBehaviour + /// Mechanism for loading new files + public var loader: Loader? - /// Basic initializer - /// - /// - Parameters: - /// - loader: Mechanism for loading new files - /// - extensions: List of extension containers - /// - templateClass: Class for newly loaded templates - /// - trimBehaviour: How to handle whitespace - public init( - loader: Loader? = nil, - extensions: [Extension] = [], - templateClass: Template.Type = Template.self, - trimBehaviour: TrimBehaviour = .nothing - ) { - self.templateClass = templateClass - self.loader = loader - self.extensions = extensions + [DefaultExtension()] - self.trimBehaviour = trimBehaviour - } + /// Basic initializer + /// + /// - Parameters: + /// - loader: Mechanism for loading new files + /// - extensions: List of extension containers + /// - templateClass: Class for newly loaded templates + /// - trimBehaviour: How to handle whitespace + public init( + loader: Loader? = nil, + extensions: [Extension] = [], + templateClass: Template.Type = Template.self, + trimBehaviour: TrimBehaviour = .nothing + ) { + self.templateClass = templateClass + self.loader = loader + self.extensions = extensions + [DefaultExtension()] + self.trimBehaviour = trimBehaviour + } - /// Load a template with the given name - /// - /// - Parameters: - /// - name: Name of the template - /// - returns: Loaded template instance - public func loadTemplate(name: String) throws -> Template { - if let loader = loader { - return try loader.loadTemplate(name: name, environment: self) - } else { - throw TemplateDoesNotExist(templateNames: [name], loader: nil) - } - } + /// Load a template with the given name + /// + /// - Parameters: + /// - name: Name of the template + /// - returns: Loaded template instance + public func loadTemplate(name: String) throws -> Template { + if let loader = loader { + return try loader.loadTemplate(name: name, environment: self) + } else { + throw TemplateDoesNotExist(templateNames: [name], loader: nil) + } + } - /// Load a template with the given names - /// - /// - Parameters: - /// - names: Names of the template - /// - returns: Loaded template instance - public func loadTemplate(names: [String]) throws -> Template { - if let loader = loader { - return try loader.loadTemplate(names: names, environment: self) - } else { - throw TemplateDoesNotExist(templateNames: names, loader: nil) - } - } + /// Load a template with the given names + /// + /// - Parameters: + /// - names: Names of the template + /// - returns: Loaded template instance + public func loadTemplate(names: [String]) throws -> Template { + if let loader = loader { + return try loader.loadTemplate(names: names, environment: self) + } else { + throw TemplateDoesNotExist(templateNames: names, loader: nil) + } + } - /// Render a template with the given name, providing some data - /// - /// - Parameters: - /// - name: Name of the template - /// - context: Data for rendering - /// - returns: Rendered output - public func renderTemplate(name: String, context: [String: Any] = [:]) throws -> String { - let template = try loadTemplate(name: name) - return try render(template: template, context: context) - } + /// Render a template with the given name, providing some data + /// + /// - Parameters: + /// - name: Name of the template + /// - context: Data for rendering + /// - returns: Rendered output + public func renderTemplate(name: String, context: [String: Any] = [:]) throws -> String { + let template = try loadTemplate(name: name) + return try render(template: template, context: context) + } - /// Render the given template string, providing some data - /// - /// - Parameters: - /// - string: Template string - /// - context: Data for rendering - /// - returns: Rendered output - public func renderTemplate(string: String, context: [String: Any] = [:]) throws -> String { - let template = templateClass.init(templateString: string, environment: self) - return try render(template: template, context: context) - } + /// Render the given template string, providing some data + /// + /// - Parameters: + /// - string: Template string + /// - context: Data for rendering + /// - returns: Rendered output + public func renderTemplate(string: String, context: [String: Any] = [:]) throws -> String { + let template = templateClass.init(templateString: string, environment: self) + return try render(template: template, context: context) + } - func render(template: Template, context: [String: Any]) throws -> String { - // update template environment as it can be created from string literal with default environment - template.environment = self - return try template.render(context) - } + func render(template: Template, context: [String: Any]) throws -> String { + // update template environment as it can be created from string literal with default environment + template.environment = self + return try template.render(context) + } } diff --git a/Sources/Stencil/Errors.swift b/Sources/Stencil/Errors.swift index 423fe87..0f9deae 100644 --- a/Sources/Stencil/Errors.swift +++ b/Sources/Stencil/Errors.swift @@ -1,81 +1,81 @@ public class TemplateDoesNotExist: Error, CustomStringConvertible { - let templateNames: [String] - let loader: Loader? + let templateNames: [String] + let loader: Loader? - public init(templateNames: [String], loader: Loader? = nil) { - self.templateNames = templateNames - self.loader = loader - } + public init(templateNames: [String], loader: Loader? = nil) { + self.templateNames = templateNames + self.loader = loader + } - public var description: String { - let templates = templateNames.joined(separator: ", ") + public var description: String { + let templates = templateNames.joined(separator: ", ") - if let loader = loader { - return "Template named `\(templates)` does not exist in loader \(loader)" - } + if let loader = loader { + return "Template named `\(templates)` does not exist in loader \(loader)" + } - return "Template named `\(templates)` does not exist. No loaders found" - } + return "Template named `\(templates)` does not exist. No loaders found" + } } public struct TemplateSyntaxError: Error, Equatable, CustomStringConvertible { - public let reason: String - public var description: String { reason } - public internal(set) var token: Token? - public internal(set) var stackTrace: [Token] - public var templateName: String? { token?.sourceMap.filename } - var allTokens: [Token] { - stackTrace + (token.map { [$0] } ?? []) - } + public let reason: String + public var description: String { reason } + public internal(set) var token: Token? + public internal(set) var stackTrace: [Token] + public var templateName: String? { token?.sourceMap.filename } + var allTokens: [Token] { + stackTrace + (token.map { [$0] } ?? []) + } - public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) { - self.reason = reason - self.stackTrace = stackTrace - self.token = token - } + public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) { + self.reason = reason + self.stackTrace = stackTrace + self.token = token + } - public init(_ description: String) { - self.init(reason: description) - } + public init(_ description: String) { + self.init(reason: description) + } } extension Error { - func withToken(_ token: Token?) -> Error { - if var error = self as? TemplateSyntaxError { - error.token = error.token ?? token - return error - } else { - return TemplateSyntaxError(reason: "\(self)", token: token) - } - } + func withToken(_ token: Token?) -> Error { + if var error = self as? TemplateSyntaxError { + error.token = error.token ?? token + return error + } else { + return TemplateSyntaxError(reason: "\(self)", token: token) + } + } } public protocol ErrorReporter: AnyObject { - func renderError(_ error: Error) -> String + func renderError(_ error: Error) -> String } open class SimpleErrorReporter: ErrorReporter { - open func renderError(_ error: Error) -> String { - guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription } + open func renderError(_ error: Error) -> String { + guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription } - func describe(token: Token) -> String { - let templateName = token.sourceMap.filename ?? "" - let location = token.sourceMap.location - let highlight = """ - \(String(Array(repeating: " ", count: location.lineOffset)))\ - ^\(String(Array(repeating: "~", count: max(token.contents.count - 1, 0)))) - """ + func describe(token: Token) -> String { + let templateName = token.sourceMap.filename ?? "" + let location = token.sourceMap.location + let highlight = """ + \(String(Array(repeating: " ", count: location.lineOffset)))\ + ^\(String(Array(repeating: "~", count: max(token.contents.count - 1, 0)))) + """ - return """ - \(templateName)\(location.lineNumber):\(location.lineOffset): error: \(templateError.reason) - \(location.content) - \(highlight) - """ - } + return """ + \(templateName)\(location.lineNumber):\(location.lineOffset): error: \(templateError.reason) + \(location.content) + \(highlight) + """ + } var descriptions = templateError.stackTrace.reduce(into: []) { $0.append(describe(token: $1)) } - let description = templateError.token.map(describe(token:)) ?? templateError.reason - descriptions.append(description) - return descriptions.joined(separator: "\n") - } + let description = templateError.token.map(describe(token:)) ?? templateError.reason + descriptions.append(description) + return descriptions.joined(separator: "\n") + } } diff --git a/Sources/Stencil/Expression.swift b/Sources/Stencil/Expression.swift index 1e8ab18..2a6d2cd 100644 --- a/Sources/Stencil/Expression.swift +++ b/Sources/Stencil/Expression.swift @@ -1,327 +1,327 @@ public protocol Expression: CustomStringConvertible, Resolvable { - func evaluate(context: Context) throws -> Bool + func evaluate(context: Context) throws -> Bool } extension Expression { - func resolve(_ context: Context) throws -> Any? { - try "\(evaluate(context: context))" - } + func resolve(_ context: Context) throws -> Any? { + try "\(evaluate(context: context))" + } } protocol InfixOperator: Expression { - init(lhs: Expression, rhs: Expression) + init(lhs: Expression, rhs: Expression) } protocol PrefixOperator: Expression { - init(expression: Expression) + init(expression: Expression) } final class StaticExpression: Expression, CustomStringConvertible { - let value: Bool + let value: Bool - init(value: Bool) { - self.value = value - } + init(value: Bool) { + self.value = value + } - func evaluate(context: Context) throws -> Bool { - value - } + func evaluate(context: Context) throws -> Bool { + value + } - var description: String { - "\(value)" - } + var description: String { + "\(value)" + } } final class VariableExpression: Expression, CustomStringConvertible { - let variable: Resolvable + let variable: Resolvable - init(variable: Resolvable) { - self.variable = variable - } + init(variable: Resolvable) { + self.variable = variable + } - var description: String { - "(variable: \(variable))" - } + var description: String { + "(variable: \(variable))" + } - func resolve(_ context: Context) throws -> Any? { - try variable.resolve(context) - } + func resolve(_ context: Context) throws -> Any? { + try variable.resolve(context) + } - /// Resolves a variable in the given context as boolean - func evaluate(context: Context) throws -> Bool { - let result = try variable.resolve(context) - var truthy = false + /// Resolves a variable in the given context as boolean + func evaluate(context: Context) throws -> Bool { + let result = try variable.resolve(context) + var truthy = false - if let result = result as? [Any] { - truthy = !result.isEmpty - } else if let result = result as? [String: Any] { - truthy = !result.isEmpty - } else if let result = result as? Bool { - truthy = result - } else if let result = result as? String { - truthy = !result.isEmpty - } else if let value = result, let result = toNumber(value: value) { - truthy = result > 0 - } else if result != nil { - truthy = true - } + if let result = result as? [Any] { + truthy = !result.isEmpty + } else if let result = result as? [String: Any] { + truthy = !result.isEmpty + } else if let result = result as? Bool { + truthy = result + } else if let result = result as? String { + truthy = !result.isEmpty + } else if let value = result, let result = toNumber(value: value) { + truthy = result > 0 + } else if result != nil { + truthy = true + } - return truthy - } + return truthy + } } final class NotExpression: Expression, PrefixOperator, CustomStringConvertible { - let expression: Expression + let expression: Expression - init(expression: Expression) { - self.expression = expression - } + init(expression: Expression) { + self.expression = expression + } - var description: String { - "not \(expression)" - } + var description: String { + "not \(expression)" + } - func evaluate(context: Context) throws -> Bool { - try !expression.evaluate(context: context) - } + func evaluate(context: Context) throws -> Bool { + try !expression.evaluate(context: context) + } } final class InExpression: Expression, InfixOperator, CustomStringConvertible { - let lhs: Expression - let rhs: Expression + let lhs: Expression + let rhs: Expression - init(lhs: Expression, rhs: Expression) { - self.lhs = lhs - self.rhs = rhs - } + init(lhs: Expression, rhs: Expression) { + self.lhs = lhs + self.rhs = rhs + } - var description: String { - "(\(lhs) in \(rhs))" - } + var description: String { + "(\(lhs) in \(rhs))" + } - func evaluate(context: Context) throws -> Bool { - if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression { - let lhsValue = try lhs.variable.resolve(context) - let rhsValue = try rhs.variable.resolve(context) + func evaluate(context: Context) throws -> Bool { + if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression { + let lhsValue = try lhs.variable.resolve(context) + let rhsValue = try rhs.variable.resolve(context) - if let lhs = lhsValue as? AnyHashable, let rhs = rhsValue as? [AnyHashable] { - return rhs.contains(lhs) - } else if let lhs = lhsValue as? Int, let rhs = rhsValue as? CountableClosedRange { - return rhs.contains(lhs) - } else if let lhs = lhsValue as? Int, let rhs = rhsValue as? CountableRange { - return rhs.contains(lhs) - } else if let lhs = lhsValue as? String, let rhs = rhsValue as? String { - return rhs.contains(lhs) - } else if lhsValue == nil && rhsValue == nil { - return true - } - } + if let lhs = lhsValue as? AnyHashable, let rhs = rhsValue as? [AnyHashable] { + return rhs.contains(lhs) + } else if let lhs = lhsValue as? Int, let rhs = rhsValue as? CountableClosedRange { + return rhs.contains(lhs) + } else if let lhs = lhsValue as? Int, let rhs = rhsValue as? CountableRange { + return rhs.contains(lhs) + } else if let lhs = lhsValue as? String, let rhs = rhsValue as? String { + return rhs.contains(lhs) + } else if lhsValue == nil && rhsValue == nil { + return true + } + } - return false - } + return false + } } final class OrExpression: Expression, InfixOperator, CustomStringConvertible { - let lhs: Expression - let rhs: Expression + let lhs: Expression + let rhs: Expression - init(lhs: Expression, rhs: Expression) { - self.lhs = lhs - self.rhs = rhs - } + init(lhs: Expression, rhs: Expression) { + self.lhs = lhs + self.rhs = rhs + } - var description: String { - "(\(lhs) or \(rhs))" - } + var description: String { + "(\(lhs) or \(rhs))" + } - func evaluate(context: Context) throws -> Bool { - let lhs = try self.lhs.evaluate(context: context) - if lhs { - return lhs - } + func evaluate(context: Context) throws -> Bool { + let lhs = try self.lhs.evaluate(context: context) + if lhs { + return lhs + } - return try rhs.evaluate(context: context) - } + return try rhs.evaluate(context: context) + } } final class AndExpression: Expression, InfixOperator, CustomStringConvertible { - let lhs: Expression - let rhs: Expression + let lhs: Expression + let rhs: Expression - init(lhs: Expression, rhs: Expression) { - self.lhs = lhs - self.rhs = rhs - } + init(lhs: Expression, rhs: Expression) { + self.lhs = lhs + self.rhs = rhs + } - var description: String { - "(\(lhs) and \(rhs))" - } + var description: String { + "(\(lhs) and \(rhs))" + } - func evaluate(context: Context) throws -> Bool { - let lhs = try self.lhs.evaluate(context: context) - if !lhs { - return lhs - } + func evaluate(context: Context) throws -> Bool { + let lhs = try self.lhs.evaluate(context: context) + if !lhs { + return lhs + } - return try rhs.evaluate(context: context) - } + return try rhs.evaluate(context: context) + } } class EqualityExpression: Expression, InfixOperator, CustomStringConvertible { - let lhs: Expression - let rhs: Expression + let lhs: Expression + let rhs: Expression - required init(lhs: Expression, rhs: Expression) { - self.lhs = lhs - self.rhs = rhs - } + required init(lhs: Expression, rhs: Expression) { + self.lhs = lhs + self.rhs = rhs + } - var description: String { - "(\(lhs) == \(rhs))" - } + var description: String { + "(\(lhs) == \(rhs))" + } - func evaluate(context: Context) throws -> Bool { - if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression { - let lhsValue = try lhs.variable.resolve(context) - let rhsValue = try rhs.variable.resolve(context) + func evaluate(context: Context) throws -> Bool { + if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression { + let lhsValue = try lhs.variable.resolve(context) + let rhsValue = try rhs.variable.resolve(context) - if let lhs = lhsValue, let rhs = rhsValue { - if let lhs = toNumber(value: lhs), let rhs = toNumber(value: rhs) { - return lhs == rhs - } else if let lhs = lhsValue as? String, let rhs = rhsValue as? String { - return lhs == rhs - } else if let lhs = lhsValue as? Bool, let rhs = rhsValue as? Bool { - return lhs == rhs - } - } else if lhsValue == nil && rhsValue == nil { - return true - } - } + if let lhs = lhsValue, let rhs = rhsValue { + if let lhs = toNumber(value: lhs), let rhs = toNumber(value: rhs) { + return lhs == rhs + } else if let lhs = lhsValue as? String, let rhs = rhsValue as? String { + return lhs == rhs + } else if let lhs = lhsValue as? Bool, let rhs = rhsValue as? Bool { + return lhs == rhs + } + } else if lhsValue == nil && rhsValue == nil { + return true + } + } - return false - } + return false + } } class NumericExpression: Expression, InfixOperator, CustomStringConvertible { - let lhs: Expression - let rhs: Expression + let lhs: Expression + let rhs: Expression - required init(lhs: Expression, rhs: Expression) { - self.lhs = lhs - self.rhs = rhs - } + required init(lhs: Expression, rhs: Expression) { + self.lhs = lhs + self.rhs = rhs + } - var description: String { - "(\(lhs) \(symbol) \(rhs))" - } + var description: String { + "(\(lhs) \(symbol) \(rhs))" + } - func evaluate(context: Context) throws -> Bool { - if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression { - let lhsValue = try lhs.variable.resolve(context) - let rhsValue = try rhs.variable.resolve(context) + func evaluate(context: Context) throws -> Bool { + if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression { + let lhsValue = try lhs.variable.resolve(context) + let rhsValue = try rhs.variable.resolve(context) - if let lhs = lhsValue, let rhs = rhsValue { - if let lhs = toNumber(value: lhs), let rhs = toNumber(value: rhs) { - return compare(lhs: lhs, rhs: rhs) - } - } - } + if let lhs = lhsValue, let rhs = rhsValue { + if let lhs = toNumber(value: lhs), let rhs = toNumber(value: rhs) { + return compare(lhs: lhs, rhs: rhs) + } + } + } - return false - } + return false + } - var symbol: String { - "" - } + var symbol: String { + "" + } - func compare(lhs: Number, rhs: Number) -> Bool { - false - } + func compare(lhs: Number, rhs: Number) -> Bool { + false + } } class MoreThanExpression: NumericExpression { - override var symbol: String { - ">" - } + override var symbol: String { + ">" + } - override func compare(lhs: Number, rhs: Number) -> Bool { - lhs > rhs - } + override func compare(lhs: Number, rhs: Number) -> Bool { + lhs > rhs + } } class MoreThanEqualExpression: NumericExpression { - override var symbol: String { - ">=" - } + override var symbol: String { + ">=" + } - override func compare(lhs: Number, rhs: Number) -> Bool { - lhs >= rhs - } + override func compare(lhs: Number, rhs: Number) -> Bool { + lhs >= rhs + } } class LessThanExpression: NumericExpression { - override var symbol: String { - "<" - } + override var symbol: String { + "<" + } - override func compare(lhs: Number, rhs: Number) -> Bool { - lhs < rhs - } + override func compare(lhs: Number, rhs: Number) -> Bool { + lhs < rhs + } } class LessThanEqualExpression: NumericExpression { - override var symbol: String { - "<=" - } + override var symbol: String { + "<=" + } - override func compare(lhs: Number, rhs: Number) -> Bool { - lhs <= rhs - } + override func compare(lhs: Number, rhs: Number) -> Bool { + lhs <= rhs + } } class InequalityExpression: EqualityExpression { - override var description: String { - "(\(lhs) != \(rhs))" - } + override var description: String { + "(\(lhs) != \(rhs))" + } - override func evaluate(context: Context) throws -> Bool { - try !super.evaluate(context: context) - } + override func evaluate(context: Context) throws -> Bool { + try !super.evaluate(context: context) + } } // swiftlint:disable:next cyclomatic_complexity func toNumber(value: Any) -> Number? { - if let value = value as? Float { - return Number(value) - } else if let value = value as? Double { - return Number(value) - } else if let value = value as? UInt { - return Number(value) - } else if let value = value as? Int { - return Number(value) - } else if let value = value as? Int8 { - return Number(value) - } else if let value = value as? Int16 { - return Number(value) - } else if let value = value as? Int32 { - return Number(value) - } else if let value = value as? Int64 { - return Number(value) - } else if let value = value as? UInt8 { - return Number(value) - } else if let value = value as? UInt16 { - return Number(value) - } else if let value = value as? UInt32 { - return Number(value) - } else if let value = value as? UInt64 { - return Number(value) - } else if let value = value as? Number { - return value - } else if let value = value as? Float64 { - return Number(value) - } else if let value = value as? Float32 { - return Number(value) - } + if let value = value as? Float { + return Number(value) + } else if let value = value as? Double { + return Number(value) + } else if let value = value as? UInt { + return Number(value) + } else if let value = value as? Int { + return Number(value) + } else if let value = value as? Int8 { + return Number(value) + } else if let value = value as? Int16 { + return Number(value) + } else if let value = value as? Int32 { + return Number(value) + } else if let value = value as? Int64 { + return Number(value) + } else if let value = value as? UInt8 { + return Number(value) + } else if let value = value as? UInt16 { + return Number(value) + } else if let value = value as? UInt32 { + return Number(value) + } else if let value = value as? UInt64 { + return Number(value) + } else if let value = value as? Number { + return value + } else if let value = value as? Float64 { + return Number(value) + } else if let value = value as? Float32 { + return Number(value) + } - return nil + return nil } diff --git a/Sources/Stencil/Extension.swift b/Sources/Stencil/Extension.swift index 42d5431..955c73d 100644 --- a/Sources/Stencil/Extension.swift +++ b/Sources/Stencil/Extension.swift @@ -1,103 +1,103 @@ /// Container for registered tags and filters open class Extension { - typealias TagParser = (TokenParser, Token) throws -> NodeType + typealias TagParser = (TokenParser, Token) throws -> NodeType - var tags = [String: TagParser]() - var filters = [String: Filter]() + var tags = [String: TagParser]() + var filters = [String: Filter]() - /// Simple initializer - public init() { - } + /// Simple initializer + public init() { + } - /// Registers a new template tag - public func registerTag(_ name: String, parser: @escaping (TokenParser, Token) throws -> NodeType) { - tags[name] = parser - } + /// Registers a new template tag + public func registerTag(_ name: String, parser: @escaping (TokenParser, Token) throws -> NodeType) { + tags[name] = parser + } - /// Registers a simple template tag with a name and a handler - public func registerSimpleTag(_ name: String, handler: @escaping (Context) throws -> String) { - registerTag(name) { _, token in - SimpleNode(token: token, handler: handler) - } - } + /// Registers a simple template tag with a name and a handler + public func registerSimpleTag(_ name: String, handler: @escaping (Context) throws -> String) { + registerTag(name) { _, token in + SimpleNode(token: token, handler: handler) + } + } - /// Registers boolean filter with it's negative counterpart - public func registerFilter(name: String, negativeFilterName: String, filter: @escaping (Any?) throws -> Bool?) { - // swiftlint:disable:previous discouraged_optional_boolean - filters[name] = .simple(filter) - filters[negativeFilterName] = .simple { value in - guard let result = try filter(value) else { return nil } - return !result - } - } + /// Registers boolean filter with it's negative counterpart + public func registerFilter(name: String, negativeFilterName: String, filter: @escaping (Any?) throws -> Bool?) { + // swiftlint:disable:previous discouraged_optional_boolean + filters[name] = .simple(filter) + filters[negativeFilterName] = .simple { value in + guard let result = try filter(value) else { return nil } + return !result + } + } - /// Registers a template filter with the given name - public func registerFilter(_ name: String, filter: @escaping (Any?) throws -> Any?) { - filters[name] = .simple(filter) - } + /// Registers a template filter with the given name + public func registerFilter(_ name: String, filter: @escaping (Any?) throws -> Any?) { + filters[name] = .simple(filter) + } - /// Registers a template filter with the given name - public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?]) throws -> Any?) { - filters[name] = .arguments { value, args, _ in try filter(value, args) } - } + /// Registers a template filter with the given name + public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?]) throws -> Any?) { + filters[name] = .arguments { value, args, _ in try filter(value, args) } + } - /// Registers a template filter with the given name - public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?], Context) throws -> Any?) { - filters[name] = .arguments(filter) - } + /// Registers a template filter with the given name + public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?], Context) throws -> Any?) { + filters[name] = .arguments(filter) + } } class DefaultExtension: Extension { - override init() { - super.init() - registerDefaultTags() - registerDefaultFilters() - } + override init() { + super.init() + registerDefaultTags() + registerDefaultFilters() + } - fileprivate func registerDefaultTags() { - registerTag("for", parser: ForNode.parse) - registerTag("break", parser: LoopTerminationNode.parse) - registerTag("continue", parser: LoopTerminationNode.parse) - registerTag("if", parser: IfNode.parse) - registerTag("ifnot", parser: IfNode.parse_ifnot) - #if !os(Linux) - registerTag("now", parser: NowNode.parse) - #endif - registerTag("include", parser: IncludeNode.parse) - registerTag("extends", parser: ExtendsNode.parse) - registerTag("block", parser: BlockNode.parse) - registerTag("filter", parser: FilterNode.parse) - } + fileprivate func registerDefaultTags() { + registerTag("for", parser: ForNode.parse) + registerTag("break", parser: LoopTerminationNode.parse) + registerTag("continue", parser: LoopTerminationNode.parse) + registerTag("if", parser: IfNode.parse) + registerTag("ifnot", parser: IfNode.parse_ifnot) + #if !os(Linux) + registerTag("now", parser: NowNode.parse) + #endif + registerTag("include", parser: IncludeNode.parse) + registerTag("extends", parser: ExtendsNode.parse) + registerTag("block", parser: BlockNode.parse) + registerTag("filter", parser: FilterNode.parse) + } - fileprivate func registerDefaultFilters() { - registerFilter("default", filter: defaultFilter) - registerFilter("capitalize", filter: capitalise) - registerFilter("uppercase", filter: uppercase) - registerFilter("lowercase", filter: lowercase) - registerFilter("join", filter: joinFilter) - registerFilter("split", filter: splitFilter) - registerFilter("indent", filter: indentFilter) - registerFilter("filter", filter: filterFilter) - } + fileprivate func registerDefaultFilters() { + registerFilter("default", filter: defaultFilter) + registerFilter("capitalize", filter: capitalise) + registerFilter("uppercase", filter: uppercase) + registerFilter("lowercase", filter: lowercase) + registerFilter("join", filter: joinFilter) + registerFilter("split", filter: splitFilter) + registerFilter("indent", filter: indentFilter) + registerFilter("filter", filter: filterFilter) + } } protocol FilterType { - func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any? + func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any? } enum Filter: FilterType { - case simple(((Any?) throws -> Any?)) - case arguments(((Any?, [Any?], Context) throws -> Any?)) + case simple(((Any?) throws -> Any?)) + case arguments(((Any?, [Any?], Context) throws -> Any?)) - func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any? { - switch self { - case let .simple(filter): - if !arguments.isEmpty { - throw TemplateSyntaxError("Can't invoke filter with an argument") - } - return try filter(value) - case let .arguments(filter): - return try filter(value, arguments, context) - } - } + func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any? { + switch self { + case let .simple(filter): + if !arguments.isEmpty { + throw TemplateSyntaxError("Can't invoke filter with an argument") + } + return try filter(value) + case let .arguments(filter): + return try filter(value, arguments, context) + } + } } diff --git a/Sources/Stencil/FilterTag.swift b/Sources/Stencil/FilterTag.swift index e623b53..cdd4969 100644 --- a/Sources/Stencil/FilterTag.swift +++ b/Sources/Stencil/FilterTag.swift @@ -1,36 +1,36 @@ class FilterNode: NodeType { - let resolvable: Resolvable - let nodes: [NodeType] - let token: Token? + let resolvable: Resolvable + let nodes: [NodeType] + let token: Token? - class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { - let bits = token.components + class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { + let bits = token.components - guard bits.count == 2 else { - throw TemplateSyntaxError("'filter' tag takes one argument, the filter expression") - } + guard bits.count == 2 else { + throw TemplateSyntaxError("'filter' tag takes one argument, the filter expression") + } - let blocks = try parser.parse(until(["endfilter"])) + let blocks = try parser.parse(until(["endfilter"])) - guard parser.nextToken() != nil else { - throw TemplateSyntaxError("`endfilter` was not found.") - } + guard parser.nextToken() != nil else { + throw TemplateSyntaxError("`endfilter` was not found.") + } - let resolvable = try parser.compileFilter("filter_value|\(bits[1])", containedIn: token) - return FilterNode(nodes: blocks, resolvable: resolvable, token: token) - } + let resolvable = try parser.compileFilter("filter_value|\(bits[1])", containedIn: token) + return FilterNode(nodes: blocks, resolvable: resolvable, token: token) + } - init(nodes: [NodeType], resolvable: Resolvable, token: Token) { - self.nodes = nodes - self.resolvable = resolvable - self.token = token - } + init(nodes: [NodeType], resolvable: Resolvable, token: Token) { + self.nodes = nodes + self.resolvable = resolvable + self.token = token + } - func render(_ context: Context) throws -> String { - let value = try renderNodes(nodes, context) + func render(_ context: Context) throws -> String { + let value = try renderNodes(nodes, context) - return try context.push(dictionary: ["filter_value": value]) { - try VariableNode(variable: resolvable, token: token).render(context) - } - } + return try context.push(dictionary: ["filter_value": value]) { + try VariableNode(variable: resolvable, token: token).render(context) + } + } } diff --git a/Sources/Stencil/Filters.swift b/Sources/Stencil/Filters.swift index 2a719e4..3196c45 100644 --- a/Sources/Stencil/Filters.swift +++ b/Sources/Stencil/Filters.swift @@ -1,133 +1,133 @@ func capitalise(_ value: Any?) -> Any? { - if let array = value as? [Any?] { - return array.map { stringify($0).capitalized } - } else { - return stringify(value).capitalized - } + if let array = value as? [Any?] { + return array.map { stringify($0).capitalized } + } else { + return stringify(value).capitalized + } } func uppercase(_ value: Any?) -> Any? { - if let array = value as? [Any?] { - return array.map { stringify($0).uppercased() } - } else { - return stringify(value).uppercased() - } + if let array = value as? [Any?] { + return array.map { stringify($0).uppercased() } + } else { + return stringify(value).uppercased() + } } func lowercase(_ value: Any?) -> Any? { - if let array = value as? [Any?] { - return array.map { stringify($0).lowercased() } - } else { - return stringify(value).lowercased() - } + if let array = value as? [Any?] { + return array.map { stringify($0).lowercased() } + } else { + return stringify(value).lowercased() + } } func defaultFilter(value: Any?, arguments: [Any?]) -> Any? { - // value can be optional wrapping nil, so this way we check for underlying value - if let value = value, String(describing: value) != "nil" { - return value - } + // value can be optional wrapping nil, so this way we check for underlying value + if let value = value, String(describing: value) != "nil" { + return value + } - for argument in arguments { - if let argument = argument { - return argument - } - } + for argument in arguments { + if let argument = argument { + return argument + } + } - return nil + return nil } func joinFilter(value: Any?, arguments: [Any?]) throws -> Any? { - guard arguments.count < 2 else { - throw TemplateSyntaxError("'join' filter takes at most one argument") - } + guard arguments.count < 2 else { + throw TemplateSyntaxError("'join' filter takes at most one argument") + } - let separator = stringify(arguments.first ?? "") + let separator = stringify(arguments.first ?? "") - if let value = value as? [Any] { - return value - .map(stringify) - .joined(separator: separator) - } + if let value = value as? [Any] { + return value + .map(stringify) + .joined(separator: separator) + } - return value + return value } func splitFilter(value: Any?, arguments: [Any?]) throws -> Any? { - guard arguments.count < 2 else { - throw TemplateSyntaxError("'split' filter takes at most one argument") - } + guard arguments.count < 2 else { + throw TemplateSyntaxError("'split' filter takes at most one argument") + } - let separator = stringify(arguments.first ?? " ") - if let value = value as? String { - return value.components(separatedBy: separator) - } + let separator = stringify(arguments.first ?? " ") + if let value = value as? String { + return value.components(separatedBy: separator) + } - return value + return value } func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? { - guard arguments.count <= 3 else { - throw TemplateSyntaxError("'indent' filter can take at most 3 arguments") - } + guard arguments.count <= 3 else { + throw TemplateSyntaxError("'indent' filter can take at most 3 arguments") + } - var indentWidth = 4 - if !arguments.isEmpty { - guard let value = arguments[0] as? Int else { - throw TemplateSyntaxError( - """ - 'indent' filter width argument must be an Integer (\(String(describing: arguments[0]))) - """ - ) - } - indentWidth = value - } + var indentWidth = 4 + if !arguments.isEmpty { + guard let value = arguments[0] as? Int else { + throw TemplateSyntaxError( + """ + 'indent' filter width argument must be an Integer (\(String(describing: arguments[0]))) + """ + ) + } + indentWidth = value + } - var indentationChar = " " - if arguments.count > 1 { - guard let value = arguments[1] as? String else { - throw TemplateSyntaxError( - """ - 'indent' filter indentation argument must be a String (\(String(describing: arguments[1])) - """ - ) - } - indentationChar = value - } + var indentationChar = " " + if arguments.count > 1 { + guard let value = arguments[1] as? String else { + throw TemplateSyntaxError( + """ + 'indent' filter indentation argument must be a String (\(String(describing: arguments[1])) + """ + ) + } + indentationChar = value + } - var indentFirst = false - if arguments.count > 2 { - guard let value = arguments[2] as? Bool else { - throw TemplateSyntaxError("'indent' filter indentFirst argument must be a Bool") - } - indentFirst = value - } + var indentFirst = false + if arguments.count > 2 { + guard let value = arguments[2] as? Bool else { + throw TemplateSyntaxError("'indent' filter indentFirst argument must be a Bool") + } + indentFirst = value + } - let indentation = [String](repeating: indentationChar, count: indentWidth).joined() - return indent(stringify(value), indentation: indentation, indentFirst: indentFirst) + let indentation = [String](repeating: indentationChar, count: indentWidth).joined() + return indent(stringify(value), indentation: indentation, indentFirst: indentFirst) } func indent(_ content: String, indentation: String, indentFirst: Bool) -> String { - guard !indentation.isEmpty else { return content } + guard !indentation.isEmpty else { return content } - var lines = content.components(separatedBy: .newlines) - let firstLine = (indentFirst ? indentation : "") + lines.removeFirst() - let result = lines.reduce(into: [firstLine]) { result, line in - result.append(line.isEmpty ? "" : "\(indentation)\(line)") - } - return result.joined(separator: "\n") + var lines = content.components(separatedBy: .newlines) + let firstLine = (indentFirst ? indentation : "") + lines.removeFirst() + let result = lines.reduce(into: [firstLine]) { result, line in + result.append(line.isEmpty ? "" : "\(indentation)\(line)") + } + return result.joined(separator: "\n") } func filterFilter(value: Any?, arguments: [Any?], context: Context) throws -> Any? { - guard let value = value else { return nil } - guard arguments.count == 1 else { - throw TemplateSyntaxError("'filter' filter takes one argument") - } + guard let value = value else { return nil } + guard arguments.count == 1 else { + throw TemplateSyntaxError("'filter' filter takes one argument") + } - let attribute = stringify(arguments[0]) + let attribute = stringify(arguments[0]) - let expr = try context.environment.compileFilter("$0|\(attribute)") - return try context.push(dictionary: ["$0": value]) { - try expr.resolve(context) - } + let expr = try context.environment.compileFilter("$0|\(attribute)") + return try context.push(dictionary: ["$0": value]) { + try expr.resolve(context) + } } diff --git a/Sources/Stencil/ForTag.swift b/Sources/Stencil/ForTag.swift index b81fc75..7e86a38 100644 --- a/Sources/Stencil/ForTag.swift +++ b/Sources/Stencil/ForTag.swift @@ -1,274 +1,274 @@ import Foundation class ForNode: NodeType { - let resolvable: Resolvable - let loopVariables: [String] - let nodes: [NodeType] - let emptyNodes: [NodeType] - let `where`: Expression? - let label: String? - let token: Token? + let resolvable: Resolvable + let loopVariables: [String] + let nodes: [NodeType] + let emptyNodes: [NodeType] + let `where`: Expression? + let label: String? + let token: Token? - class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { - var components = token.components + class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { + var components = token.components - var label: String? - if components.first?.hasSuffix(":") == true { - label = String(components.removeFirst().dropLast()) - } + var label: String? + if components.first?.hasSuffix(":") == true { + label = String(components.removeFirst().dropLast()) + } - func hasToken(_ token: String, at index: Int) -> Bool { - components.count > (index + 1) && components[index] == token - } + func hasToken(_ token: String, at index: Int) -> Bool { + components.count > (index + 1) && components[index] == token + } - func endsOrHasToken(_ token: String, at index: Int) -> Bool { - components.count == index || hasToken(token, at: index) - } + func endsOrHasToken(_ token: String, at index: Int) -> Bool { + components.count == index || hasToken(token, at: index) + } - guard hasToken("in", at: 2) && endsOrHasToken("where", at: 4) else { - throw TemplateSyntaxError("'for' statements should use the syntax: `for in [where ]`.") - } + guard hasToken("in", at: 2) && endsOrHasToken("where", at: 4) else { + throw TemplateSyntaxError("'for' statements should use the syntax: `for in [where ]`.") + } - let loopVariables = components[1] - .split(separator: ",") - .map(String.init) - .map { $0.trim(character: " ") } + let loopVariables = components[1] + .split(separator: ",") + .map(String.init) + .map { $0.trim(character: " ") } - let resolvable = try parser.compileResolvable(components[3], containedIn: token) + let resolvable = try parser.compileResolvable(components[3], containedIn: token) - let `where` = hasToken("where", at: 4) - ? try parser.compileExpression(components: Array(components.suffix(from: 5)), token: token) - : nil + let `where` = hasToken("where", at: 4) + ? try parser.compileExpression(components: Array(components.suffix(from: 5)), token: token) + : nil - let forNodes = try parser.parse(until(["endfor", "empty"])) + let forNodes = try parser.parse(until(["endfor", "empty"])) - guard let token = parser.nextToken() else { - throw TemplateSyntaxError("`endfor` was not found.") - } + guard let token = parser.nextToken() else { + throw TemplateSyntaxError("`endfor` was not found.") + } - var emptyNodes = [NodeType]() - if token.contents == "empty" { - emptyNodes = try parser.parse(until(["endfor"])) - _ = parser.nextToken() - } + var emptyNodes = [NodeType]() + if token.contents == "empty" { + emptyNodes = try parser.parse(until(["endfor"])) + _ = parser.nextToken() + } - return ForNode( - resolvable: resolvable, - loopVariables: loopVariables, - nodes: forNodes, - emptyNodes: emptyNodes, - where: `where`, - label: label, - token: token - ) - } + return ForNode( + resolvable: resolvable, + loopVariables: loopVariables, + nodes: forNodes, + emptyNodes: emptyNodes, + where: `where`, + label: label, + token: token + ) + } - init( - resolvable: Resolvable, - loopVariables: [String], - nodes: [NodeType], - emptyNodes: [NodeType], - where: Expression? = nil, - label: String? = nil, - token: Token? = nil - ) { - self.resolvable = resolvable - self.loopVariables = loopVariables - self.nodes = nodes - self.emptyNodes = emptyNodes - self.where = `where` - self.label = label - self.token = token - } + init( + resolvable: Resolvable, + loopVariables: [String], + nodes: [NodeType], + emptyNodes: [NodeType], + where: Expression? = nil, + label: String? = nil, + token: Token? = nil + ) { + self.resolvable = resolvable + self.loopVariables = loopVariables + self.nodes = nodes + self.emptyNodes = emptyNodes + self.where = `where` + self.label = label + self.token = token + } - func render(_ context: Context) throws -> String { - var values = try resolve(context) + func render(_ context: Context) throws -> String { + var values = try resolve(context) - if let `where` = self.where { - values = try values.filter { item -> Bool in - try push(value: item, context: context) { - try `where`.evaluate(context: context) - } - } - } + if let `where` = self.where { + values = try values.filter { item -> Bool in + try push(value: item, context: context) { + try `where`.evaluate(context: context) + } + } + } - if !values.isEmpty { - let count = values.count - var result = "" + if !values.isEmpty { + let count = values.count + var result = "" - // collect parent loop contexts - let parentLoopContexts = (context["forloop"] as? [String: Any])? - .filter { ($1 as? [String: Any])?["label"] != nil } ?? [:] + // collect parent loop contexts + let parentLoopContexts = (context["forloop"] as? [String: Any])? + .filter { ($1 as? [String: Any])?["label"] != nil } ?? [:] - for (index, item) in zip(0..., values) { - var forContext: [String: Any] = [ - "first": index == 0, - "last": index == (count - 1), - "counter": index + 1, - "counter0": index, - "length": count - ] - if let label = label { - forContext["label"] = label - forContext[label] = forContext - } - forContext.merge(parentLoopContexts) { lhs, _ in lhs } + for (index, item) in zip(0..., values) { + var forContext: [String: Any] = [ + "first": index == 0, + "last": index == (count - 1), + "counter": index + 1, + "counter0": index, + "length": count + ] + if let label = label { + forContext["label"] = label + forContext[label] = forContext + } + forContext.merge(parentLoopContexts) { lhs, _ in lhs } - var shouldBreak = false - result += try context.push(dictionary: ["forloop": forContext]) { - defer { - // if outer loop should be continued we should break from current loop - if let shouldContinueLabel = context[LoopTerminationNode.continueContextKey] as? String { - shouldBreak = shouldContinueLabel != label || label == nil - } else { - shouldBreak = context[LoopTerminationNode.breakContextKey] != nil - } - } - return try push(value: item, context: context) { - try renderNodes(nodes, context) - } - } + var shouldBreak = false + result += try context.push(dictionary: ["forloop": forContext]) { + defer { + // if outer loop should be continued we should break from current loop + if let shouldContinueLabel = context[LoopTerminationNode.continueContextKey] as? String { + shouldBreak = shouldContinueLabel != label || label == nil + } else { + shouldBreak = context[LoopTerminationNode.breakContextKey] != nil + } + } + return try push(value: item, context: context) { + try renderNodes(nodes, context) + } + } - if shouldBreak { - break - } - } + if shouldBreak { + break + } + } - return result - } else { - return try context.push { - try renderNodes(emptyNodes, context) - } - } - } + return result + } else { + return try context.push { + try renderNodes(emptyNodes, context) + } + } + } - private func push(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result { - if loopVariables.isEmpty { - return try context.push { - try closure() - } - } + private func push(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result { + if loopVariables.isEmpty { + return try context.push { + try closure() + } + } - let valueMirror = Mirror(reflecting: value) - if case .tuple? = valueMirror.displayStyle { - if loopVariables.count > Int(valueMirror.children.count) { - throw TemplateSyntaxError("Tuple '\(value)' has less values than loop variables") - } - var variablesContext = [String: Any]() - valueMirror.children.prefix(loopVariables.count).enumerated().forEach { offset, element in - if loopVariables[offset] != "_" { - variablesContext[loopVariables[offset]] = element.value - } - } + let valueMirror = Mirror(reflecting: value) + if case .tuple? = valueMirror.displayStyle { + if loopVariables.count > Int(valueMirror.children.count) { + throw TemplateSyntaxError("Tuple '\(value)' has less values than loop variables") + } + var variablesContext = [String: Any]() + valueMirror.children.prefix(loopVariables.count).enumerated().forEach { offset, element in + if loopVariables[offset] != "_" { + variablesContext[loopVariables[offset]] = element.value + } + } - return try context.push(dictionary: variablesContext) { - try closure() - } - } + return try context.push(dictionary: variablesContext) { + try closure() + } + } - return try context.push(dictionary: [loopVariables.first ?? "": value]) { - try closure() - } - } + return try context.push(dictionary: [loopVariables.first ?? "": value]) { + try closure() + } + } - private func resolve(_ context: Context) throws -> [Any] { - let resolved = try resolvable.resolve(context) + private func resolve(_ context: Context) throws -> [Any] { + let resolved = try resolvable.resolve(context) - var values: [Any] - if let dictionary = resolved as? [String: Any], !dictionary.isEmpty { - values = dictionary.sorted { $0.key < $1.key } - } else if let array = resolved as? [Any] { - values = array - } else if let range = resolved as? CountableClosedRange { - values = Array(range) - } else if let range = resolved as? CountableRange { - values = Array(range) - } else if let resolved = resolved { - let mirror = Mirror(reflecting: resolved) - switch mirror.displayStyle { - case .struct, .tuple: - values = Array(mirror.children) - case .class: - var children = Array(mirror.children) - var currentMirror: Mirror? = mirror - while let superclassMirror = currentMirror?.superclassMirror { - children.append(contentsOf: superclassMirror.children) - currentMirror = superclassMirror - } - values = Array(children) - default: - values = [] - } - } else { - values = [] - } + var values: [Any] + if let dictionary = resolved as? [String: Any], !dictionary.isEmpty { + values = dictionary.sorted { $0.key < $1.key } + } else if let array = resolved as? [Any] { + values = array + } else if let range = resolved as? CountableClosedRange { + values = Array(range) + } else if let range = resolved as? CountableRange { + values = Array(range) + } else if let resolved = resolved { + let mirror = Mirror(reflecting: resolved) + switch mirror.displayStyle { + case .struct, .tuple: + values = Array(mirror.children) + case .class: + var children = Array(mirror.children) + var currentMirror: Mirror? = mirror + while let superclassMirror = currentMirror?.superclassMirror { + children.append(contentsOf: superclassMirror.children) + currentMirror = superclassMirror + } + values = Array(children) + default: + values = [] + } + } else { + values = [] + } - return values - } + return values + } } struct LoopTerminationNode: NodeType { - static let breakContextKey = "_internal_forloop_break" - static let continueContextKey = "_internal_forloop_continue" + static let breakContextKey = "_internal_forloop_break" + static let continueContextKey = "_internal_forloop_continue" - let name: String - let label: String? - let token: Token? + let name: String + let label: String? + let token: Token? - var contextKey: String { - "_internal_forloop_\(name)" - } + var contextKey: String { + "_internal_forloop_\(name)" + } - private init(name: String, label: String? = nil, token: Token? = nil) { - self.name = name - self.label = label - self.token = token - } + private init(name: String, label: String? = nil, token: Token? = nil) { + self.name = name + self.label = label + self.token = token + } - static func parse(_ parser: TokenParser, token: Token) throws -> Self { - let components = token.components + static func parse(_ parser: TokenParser, token: Token) throws -> Self { + let components = token.components - guard components.count <= 2 else { - throw TemplateSyntaxError("'\(token.contents)' can accept only one parameter") - } - guard parser.hasOpenedForTag() else { - throw TemplateSyntaxError("'\(token.contents)' can be used only inside loop body") - } + guard components.count <= 2 else { + throw TemplateSyntaxError("'\(token.contents)' can accept only one parameter") + } + guard parser.hasOpenedForTag() else { + throw TemplateSyntaxError("'\(token.contents)' can be used only inside loop body") + } - return Self(name: components[0], label: components.count == 2 ? components[1] : nil, token: token) - } + return Self(name: components[0], label: components.count == 2 ? components[1] : nil, token: token) + } - func render(_ context: Context) throws -> String { - let offset = zip(0..., context.dictionaries).reversed().first { _, dictionary in - guard let forContext = dictionary["forloop"] as? [String: Any], - dictionary["forloop"] != nil else { return false } + func render(_ context: Context) throws -> String { + let offset = zip(0..., context.dictionaries).reversed().first { _, dictionary in + guard let forContext = dictionary["forloop"] as? [String: Any], + dictionary["forloop"] != nil else { return false } - if let label = label { - return label == forContext["label"] as? String - } else { - return true - } - }?.0 + if let label = label { + return label == forContext["label"] as? String + } else { + return true + } + }?.0 - if let offset = offset { - context.dictionaries[offset][contextKey] = label ?? true - } else if let label = label { - throw TemplateSyntaxError("No loop labeled '\(label)' is currently running") - } else { - throw TemplateSyntaxError("No loop is currently running") - } + if let offset = offset { + context.dictionaries[offset][contextKey] = label ?? true + } else if let label = label { + throw TemplateSyntaxError("No loop labeled '\(label)' is currently running") + } else { + throw TemplateSyntaxError("No loop is currently running") + } - return "" - } + return "" + } } private extension TokenParser { - func hasOpenedForTag() -> Bool { - var openForCount = 0 - for parsedToken in parsedTokens.reversed() where parsedToken.kind == .block { - if parsedToken.components.first == "endfor" { openForCount -= 1 } - if parsedToken.components.first == "for" { openForCount += 1 } - } - return openForCount > 0 - } + func hasOpenedForTag() -> Bool { + var openForCount = 0 + for parsedToken in parsedTokens.reversed() where parsedToken.kind == .block { + if parsedToken.components.first == "endfor" { openForCount -= 1 } + if parsedToken.components.first == "for" { openForCount += 1 } + } + return openForCount > 0 + } } diff --git a/Sources/Stencil/IfTag.swift b/Sources/Stencil/IfTag.swift index bd8e3b3..cc5339c 100644 --- a/Sources/Stencil/IfTag.swift +++ b/Sources/Stencil/IfTag.swift @@ -1,314 +1,314 @@ enum Operator { - case infix(String, Int, InfixOperator.Type) - case prefix(String, Int, PrefixOperator.Type) + case infix(String, Int, InfixOperator.Type) + case prefix(String, Int, PrefixOperator.Type) - var name: String { - switch self { - case .infix(let name, _, _): - return name - case .prefix(let name, _, _): - return name - } - } + var name: String { + switch self { + case .infix(let name, _, _): + return name + case .prefix(let name, _, _): + return name + } + } - static let all: [Self] = [ - .infix("in", 5, InExpression.self), - .infix("or", 6, OrExpression.self), - .infix("and", 7, AndExpression.self), - .prefix("not", 8, NotExpression.self), - .infix("==", 10, EqualityExpression.self), - .infix("!=", 10, InequalityExpression.self), - .infix(">", 10, MoreThanExpression.self), - .infix(">=", 10, MoreThanEqualExpression.self), - .infix("<", 10, LessThanExpression.self), - .infix("<=", 10, LessThanEqualExpression.self) - ] + static let all: [Self] = [ + .infix("in", 5, InExpression.self), + .infix("or", 6, OrExpression.self), + .infix("and", 7, AndExpression.self), + .prefix("not", 8, NotExpression.self), + .infix("==", 10, EqualityExpression.self), + .infix("!=", 10, InequalityExpression.self), + .infix(">", 10, MoreThanExpression.self), + .infix(">=", 10, MoreThanEqualExpression.self), + .infix("<", 10, LessThanExpression.self), + .infix("<=", 10, LessThanEqualExpression.self) + ] } func findOperator(name: String) -> Operator? { - for `operator` in Operator.all where `operator`.name == name { - return `operator` - } + for `operator` in Operator.all where `operator`.name == name { + return `operator` + } - return nil + return nil } indirect enum IfToken { - case infix(name: String, bindingPower: Int, operatorType: InfixOperator.Type) - case prefix(name: String, bindingPower: Int, operatorType: PrefixOperator.Type) - case variable(Resolvable) - case subExpression(Expression) - case end + case infix(name: String, bindingPower: Int, operatorType: InfixOperator.Type) + case prefix(name: String, bindingPower: Int, operatorType: PrefixOperator.Type) + case variable(Resolvable) + case subExpression(Expression) + case end - var bindingPower: Int { - switch self { - case .infix(_, let bindingPower, _): - return bindingPower - case .prefix(_, let bindingPower, _): - return bindingPower - case .variable: - return 0 - case .subExpression: - return 0 - case .end: - return 0 - } - } + var bindingPower: Int { + switch self { + case .infix(_, let bindingPower, _): + return bindingPower + case .prefix(_, let bindingPower, _): + return bindingPower + case .variable: + return 0 + case .subExpression: + return 0 + case .end: + return 0 + } + } - func nullDenotation(parser: IfExpressionParser) throws -> Expression { - switch self { - case .infix(let name, _, _): - throw TemplateSyntaxError("'if' expression error: infix operator '\(name)' doesn't have a left hand side") - case .prefix(_, let bindingPower, let operatorType): - let expression = try parser.expression(bindingPower: bindingPower) - return operatorType.init(expression: expression) - case .variable(let variable): - return VariableExpression(variable: variable) - case .subExpression(let expression): - return expression - case .end: - throw TemplateSyntaxError("'if' expression error: end") - } - } + func nullDenotation(parser: IfExpressionParser) throws -> Expression { + switch self { + case .infix(let name, _, _): + throw TemplateSyntaxError("'if' expression error: infix operator '\(name)' doesn't have a left hand side") + case .prefix(_, let bindingPower, let operatorType): + let expression = try parser.expression(bindingPower: bindingPower) + return operatorType.init(expression: expression) + case .variable(let variable): + return VariableExpression(variable: variable) + case .subExpression(let expression): + return expression + case .end: + throw TemplateSyntaxError("'if' expression error: end") + } + } - func leftDenotation(left: Expression, parser: IfExpressionParser) throws -> Expression { - switch self { - case .infix(_, let bindingPower, let operatorType): - let right = try parser.expression(bindingPower: bindingPower) - return operatorType.init(lhs: left, rhs: right) - case .prefix(let name, _, _): - throw TemplateSyntaxError("'if' expression error: prefix operator '\(name)' was called with a left hand side") - case .variable(let variable): - throw TemplateSyntaxError("'if' expression error: variable '\(variable)' was called with a left hand side") - case .subExpression: - throw TemplateSyntaxError("'if' expression error: sub expression was called with a left hand side") - case .end: - throw TemplateSyntaxError("'if' expression error: end") - } - } + func leftDenotation(left: Expression, parser: IfExpressionParser) throws -> Expression { + switch self { + case .infix(_, let bindingPower, let operatorType): + let right = try parser.expression(bindingPower: bindingPower) + return operatorType.init(lhs: left, rhs: right) + case .prefix(let name, _, _): + throw TemplateSyntaxError("'if' expression error: prefix operator '\(name)' was called with a left hand side") + case .variable(let variable): + throw TemplateSyntaxError("'if' expression error: variable '\(variable)' was called with a left hand side") + case .subExpression: + throw TemplateSyntaxError("'if' expression error: sub expression was called with a left hand side") + case .end: + throw TemplateSyntaxError("'if' expression error: end") + } + } - var isEnd: Bool { - switch self { - case .end: - return true - default: - return false - } - } + var isEnd: Bool { + switch self { + case .end: + return true + default: + return false + } + } } final class IfExpressionParser { - let tokens: [IfToken] - var position: Int = 0 + let tokens: [IfToken] + var position: Int = 0 - private init(tokens: [IfToken]) { - self.tokens = tokens - } + private init(tokens: [IfToken]) { + self.tokens = tokens + } - static func parser(components: [String], environment: Environment, token: Token) throws -> IfExpressionParser { - try IfExpressionParser(components: ArraySlice(components), environment: environment, token: token) - } + static func parser(components: [String], environment: Environment, token: Token) throws -> IfExpressionParser { + try IfExpressionParser(components: ArraySlice(components), environment: environment, token: token) + } - private init(components: ArraySlice, environment: Environment, token: Token) throws { - var parsedComponents = Set() - var bracketsBalance = 0 - // swiftlint:disable:next closure_body_length - self.tokens = try zip(components.indices, components).compactMap { index, component in - guard !parsedComponents.contains(index) else { return nil } + private init(components: ArraySlice, environment: Environment, token: Token) throws { + var parsedComponents = Set() + var bracketsBalance = 0 + // swiftlint:disable:next closure_body_length + self.tokens = try zip(components.indices, components).compactMap { index, component in + guard !parsedComponents.contains(index) else { return nil } - if component == "(" { - bracketsBalance += 1 - let (expression, parsedCount) = try Self.subExpression( - from: components.suffix(from: index + 1), - environment: environment, - token: token - ) - parsedComponents.formUnion(Set(index...(index + parsedCount))) - return .subExpression(expression) - } else if component == ")" { - bracketsBalance -= 1 - if bracketsBalance < 0 { - throw TemplateSyntaxError("'if' expression error: missing opening bracket") - } - parsedComponents.insert(index) - return nil - } else { - parsedComponents.insert(index) - if let `operator` = findOperator(name: component) { - switch `operator` { - case .infix(let name, let bindingPower, let operatorType): - return .infix(name: name, bindingPower: bindingPower, operatorType: operatorType) - case .prefix(let name, let bindingPower, let operatorType): - return .prefix(name: name, bindingPower: bindingPower, operatorType: operatorType) - } - } - return .variable(try environment.compileResolvable(component, containedIn: token)) - } - } - } + if component == "(" { + bracketsBalance += 1 + let (expression, parsedCount) = try Self.subExpression( + from: components.suffix(from: index + 1), + environment: environment, + token: token + ) + parsedComponents.formUnion(Set(index...(index + parsedCount))) + return .subExpression(expression) + } else if component == ")" { + bracketsBalance -= 1 + if bracketsBalance < 0 { + throw TemplateSyntaxError("'if' expression error: missing opening bracket") + } + parsedComponents.insert(index) + return nil + } else { + parsedComponents.insert(index) + if let `operator` = findOperator(name: component) { + switch `operator` { + case .infix(let name, let bindingPower, let operatorType): + return .infix(name: name, bindingPower: bindingPower, operatorType: operatorType) + case .prefix(let name, let bindingPower, let operatorType): + return .prefix(name: name, bindingPower: bindingPower, operatorType: operatorType) + } + } + return .variable(try environment.compileResolvable(component, containedIn: token)) + } + } + } - private static func subExpression( - from components: ArraySlice, - environment: Environment, - token: Token - ) throws -> (Expression, Int) { - var bracketsBalance = 1 - let subComponents = components.prefix { component in - if component == "(" { - bracketsBalance += 1 - } else if component == ")" { - bracketsBalance -= 1 - } - return bracketsBalance != 0 - } - if bracketsBalance > 0 { - throw TemplateSyntaxError("'if' expression error: missing closing bracket") - } + private static func subExpression( + from components: ArraySlice, + environment: Environment, + token: Token + ) throws -> (Expression, Int) { + var bracketsBalance = 1 + let subComponents = components.prefix { component in + if component == "(" { + bracketsBalance += 1 + } else if component == ")" { + bracketsBalance -= 1 + } + return bracketsBalance != 0 + } + if bracketsBalance > 0 { + throw TemplateSyntaxError("'if' expression error: missing closing bracket") + } - let expressionParser = try IfExpressionParser(components: subComponents, environment: environment, token: token) - let expression = try expressionParser.parse() - return (expression, subComponents.count) - } + let expressionParser = try IfExpressionParser(components: subComponents, environment: environment, token: token) + let expression = try expressionParser.parse() + return (expression, subComponents.count) + } - var currentToken: IfToken { - if tokens.count > position { - return tokens[position] - } + var currentToken: IfToken { + if tokens.count > position { + return tokens[position] + } - return .end - } + return .end + } - var nextToken: IfToken { - position += 1 - return currentToken - } + var nextToken: IfToken { + position += 1 + return currentToken + } - func parse() throws -> Expression { - let expression = try self.expression() + func parse() throws -> Expression { + let expression = try self.expression() - if !currentToken.isEnd { - throw TemplateSyntaxError("'if' expression error: dangling token") - } + if !currentToken.isEnd { + throw TemplateSyntaxError("'if' expression error: dangling token") + } - return expression - } + return expression + } - func expression(bindingPower: Int = 0) throws -> Expression { - var token = currentToken - position += 1 + func expression(bindingPower: Int = 0) throws -> Expression { + var token = currentToken + position += 1 - var left = try token.nullDenotation(parser: self) + var left = try token.nullDenotation(parser: self) - while bindingPower < currentToken.bindingPower { - token = currentToken - position += 1 - left = try token.leftDenotation(left: left, parser: self) - } + while bindingPower < currentToken.bindingPower { + token = currentToken + position += 1 + left = try token.leftDenotation(left: left, parser: self) + } - return left - } + return left + } } /// Represents an if condition and the associated nodes when the condition /// evaluates final class IfCondition { - let expression: Expression? - let nodes: [NodeType] + let expression: Expression? + let nodes: [NodeType] - init(expression: Expression?, nodes: [NodeType]) { - self.expression = expression - self.nodes = nodes - } + init(expression: Expression?, nodes: [NodeType]) { + self.expression = expression + self.nodes = nodes + } - func render(_ context: Context) throws -> String { - try context.push { - try renderNodes(nodes, context) - } - } + func render(_ context: Context) throws -> String { + try context.push { + try renderNodes(nodes, context) + } + } } class IfNode: NodeType { - let conditions: [IfCondition] - let token: Token? + let conditions: [IfCondition] + let token: Token? - class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { - var components = token.components - components.removeFirst() + class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { + var components = token.components + components.removeFirst() - let expression = try parser.compileExpression(components: components, token: token) - let nodes = try parser.parse(until(["endif", "elif", "else"])) - var conditions: [IfCondition] = [ - IfCondition(expression: expression, nodes: nodes) - ] + let expression = try parser.compileExpression(components: components, token: token) + let nodes = try parser.parse(until(["endif", "elif", "else"])) + var conditions: [IfCondition] = [ + IfCondition(expression: expression, nodes: nodes) + ] - var nextToken = parser.nextToken() - while let current = nextToken, current.contents.hasPrefix("elif") { - var components = current.components - components.removeFirst() - let expression = try parser.compileExpression(components: components, token: current) + var nextToken = parser.nextToken() + while let current = nextToken, current.contents.hasPrefix("elif") { + var components = current.components + components.removeFirst() + let expression = try parser.compileExpression(components: components, token: current) - let nodes = try parser.parse(until(["endif", "elif", "else"])) - nextToken = parser.nextToken() - conditions.append(IfCondition(expression: expression, nodes: nodes)) - } + let nodes = try parser.parse(until(["endif", "elif", "else"])) + nextToken = parser.nextToken() + conditions.append(IfCondition(expression: expression, nodes: nodes)) + } - if let current = nextToken, current.contents == "else" { - conditions.append(IfCondition(expression: nil, nodes: try parser.parse(until(["endif"])))) - nextToken = parser.nextToken() - } + if let current = nextToken, current.contents == "else" { + conditions.append(IfCondition(expression: nil, nodes: try parser.parse(until(["endif"])))) + nextToken = parser.nextToken() + } - guard let current = nextToken, current.contents == "endif" else { - throw TemplateSyntaxError("`endif` was not found.") - } + guard let current = nextToken, current.contents == "endif" else { + throw TemplateSyntaxError("`endif` was not found.") + } - return IfNode(conditions: conditions, token: token) - } + return IfNode(conditions: conditions, token: token) + } - class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType { - var components = token.components - guard components.count == 2 else { - throw TemplateSyntaxError("'ifnot' statements should use the following syntax 'ifnot condition'.") - } - components.removeFirst() - var trueNodes = [NodeType]() - var falseNodes = [NodeType]() + class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType { + var components = token.components + guard components.count == 2 else { + throw TemplateSyntaxError("'ifnot' statements should use the following syntax 'ifnot condition'.") + } + components.removeFirst() + var trueNodes = [NodeType]() + var falseNodes = [NodeType]() - let expression = try parser.compileExpression(components: components, token: token) - falseNodes = try parser.parse(until(["endif", "else"])) + let expression = try parser.compileExpression(components: components, token: token) + falseNodes = try parser.parse(until(["endif", "else"])) - guard let token = parser.nextToken() else { - throw TemplateSyntaxError("`endif` was not found.") - } + guard let token = parser.nextToken() else { + throw TemplateSyntaxError("`endif` was not found.") + } - if token.contents == "else" { - trueNodes = try parser.parse(until(["endif"])) - _ = parser.nextToken() - } + if token.contents == "else" { + trueNodes = try parser.parse(until(["endif"])) + _ = parser.nextToken() + } - return IfNode(conditions: [ - IfCondition(expression: expression, nodes: trueNodes), - IfCondition(expression: nil, nodes: falseNodes) - ], token: token) - } + return IfNode(conditions: [ + IfCondition(expression: expression, nodes: trueNodes), + IfCondition(expression: nil, nodes: falseNodes) + ], token: token) + } - init(conditions: [IfCondition], token: Token? = nil) { - self.conditions = conditions - self.token = token - } + init(conditions: [IfCondition], token: Token? = nil) { + self.conditions = conditions + self.token = token + } - func render(_ context: Context) throws -> String { - for condition in conditions { - if let expression = condition.expression { - let truthy = try expression.evaluate(context: context) + func render(_ context: Context) throws -> String { + for condition in conditions { + if let expression = condition.expression { + let truthy = try expression.evaluate(context: context) - if truthy { - return try condition.render(context) - } - } else { - return try condition.render(context) - } - } + if truthy { + return try condition.render(context) + } + } else { + return try condition.render(context) + } + } - return "" - } + return "" + } } diff --git a/Sources/Stencil/Include.swift b/Sources/Stencil/Include.swift index aa174ba..c0141ea 100644 --- a/Sources/Stencil/Include.swift +++ b/Sources/Stencil/Include.swift @@ -1,48 +1,48 @@ class IncludeNode: NodeType { - let templateName: Variable - let includeContext: String? - let token: Token? + let templateName: Variable + let includeContext: String? + let token: Token? - class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { - let bits = token.components + class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { + let bits = token.components - guard bits.count == 2 || bits.count == 3 else { - throw TemplateSyntaxError( - """ - 'include' tag requires one argument, the template file to be included. \ - A second optional argument can be used to specify the context that will \ - be passed to the included file - """ - ) - } + guard bits.count == 2 || bits.count == 3 else { + throw TemplateSyntaxError( + """ + 'include' tag requires one argument, the template file to be included. \ + A second optional argument can be used to specify the context that will \ + be passed to the included file + """ + ) + } - return IncludeNode(templateName: Variable(bits[1]), includeContext: bits.count == 3 ? bits[2] : nil, token: token) - } + return IncludeNode(templateName: Variable(bits[1]), includeContext: bits.count == 3 ? bits[2] : nil, token: token) + } - init(templateName: Variable, includeContext: String? = nil, token: Token) { - self.templateName = templateName - self.includeContext = includeContext - self.token = token - } + init(templateName: Variable, includeContext: String? = nil, token: Token) { + self.templateName = templateName + self.includeContext = includeContext + self.token = token + } - func render(_ context: Context) throws -> String { - guard let templateName = try self.templateName.resolve(context) as? String else { - throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string") - } + func render(_ context: Context) throws -> String { + guard let templateName = try self.templateName.resolve(context) as? String else { + throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string") + } - let template = try context.environment.loadTemplate(name: templateName) + let template = try context.environment.loadTemplate(name: templateName) - do { - let subContext = includeContext.flatMap { context[$0] as? [String: Any] } ?? [:] - return try context.push(dictionary: subContext) { - try template.render(context) - } - } catch { - if let error = error as? TemplateSyntaxError { - throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens) - } else { - throw error - } - } - } + do { + let subContext = includeContext.flatMap { context[$0] as? [String: Any] } ?? [:] + return try context.push(dictionary: subContext) { + try template.render(context) + } + } catch { + if let error = error as? TemplateSyntaxError { + throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens) + } else { + throw error + } + } + } } diff --git a/Sources/Stencil/Inheritance.swift b/Sources/Stencil/Inheritance.swift index c4a1326..d2377d8 100644 --- a/Sources/Stencil/Inheritance.swift +++ b/Sources/Stencil/Inheritance.swift @@ -1,158 +1,158 @@ class BlockContext { - class var contextKey: String { "block_context" } + class var contextKey: String { "block_context" } - // contains mapping of block names to their nodes and templates where they are defined - var blocks: [String: [BlockNode]] + // contains mapping of block names to their nodes and templates where they are defined + var blocks: [String: [BlockNode]] - init(blocks: [String: BlockNode]) { - self.blocks = [:] - blocks.forEach { self.blocks[$0.key] = [$0.value] } - } + init(blocks: [String: BlockNode]) { + self.blocks = [:] + blocks.forEach { self.blocks[$0.key] = [$0.value] } + } - func push(_ block: BlockNode, forKey blockName: String) { - if var blocks = blocks[blockName] { - blocks.append(block) - self.blocks[blockName] = blocks - } else { - self.blocks[blockName] = [block] - } - } + func push(_ block: BlockNode, forKey blockName: String) { + if var blocks = blocks[blockName] { + blocks.append(block) + self.blocks[blockName] = blocks + } else { + self.blocks[blockName] = [block] + } + } - func pop(_ blockName: String) -> BlockNode? { - if var blocks = blocks[blockName] { - let block = blocks.removeFirst() - if blocks.isEmpty { - self.blocks.removeValue(forKey: blockName) - } else { - self.blocks[blockName] = blocks - } - return block - } else { - return nil - } - } + func pop(_ blockName: String) -> BlockNode? { + if var blocks = blocks[blockName] { + let block = blocks.removeFirst() + if blocks.isEmpty { + self.blocks.removeValue(forKey: blockName) + } else { + self.blocks[blockName] = blocks + } + return block + } else { + return nil + } + } } extension Collection { - func any(_ closure: (Iterator.Element) -> Bool) -> Iterator.Element? { - for element in self where closure(element) { - return element - } + func any(_ closure: (Iterator.Element) -> Bool) -> Iterator.Element? { + for element in self where closure(element) { + return element + } - return nil - } + return nil + } } class ExtendsNode: NodeType { - let templateName: Variable - let blocks: [String: BlockNode] - let token: Token? + let templateName: Variable + let blocks: [String: BlockNode] + let token: Token? - class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { - let bits = token.components + class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { + let bits = token.components - guard bits.count == 2 else { - throw TemplateSyntaxError("'extends' takes one argument, the template file to be extended") - } + guard bits.count == 2 else { + throw TemplateSyntaxError("'extends' takes one argument, the template file to be extended") + } - let parsedNodes = try parser.parse() - guard (parsedNodes.any { $0 is Self }) == nil else { - throw TemplateSyntaxError("'extends' cannot appear more than once in the same template") - } + let parsedNodes = try parser.parse() + guard (parsedNodes.any { $0 is Self }) == nil else { + throw TemplateSyntaxError("'extends' cannot appear more than once in the same template") + } - let blockNodes = parsedNodes.compactMap { $0 as? BlockNode } - let nodes = blockNodes.reduce(into: [String: BlockNode]()) { accumulator, node in - accumulator[node.name] = node - } + let blockNodes = parsedNodes.compactMap { $0 as? BlockNode } + let nodes = blockNodes.reduce(into: [String: BlockNode]()) { accumulator, node in + accumulator[node.name] = node + } - return ExtendsNode(templateName: Variable(bits[1]), blocks: nodes, token: token) - } + return ExtendsNode(templateName: Variable(bits[1]), blocks: nodes, token: token) + } - init(templateName: Variable, blocks: [String: BlockNode], token: Token) { - self.templateName = templateName - self.blocks = blocks - self.token = token - } + init(templateName: Variable, blocks: [String: BlockNode], token: Token) { + self.templateName = templateName + self.blocks = blocks + self.token = token + } - func render(_ context: Context) throws -> String { - guard let templateName = try self.templateName.resolve(context) as? String else { - throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string") - } + func render(_ context: Context) throws -> String { + guard let templateName = try self.templateName.resolve(context) as? String else { + throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string") + } - let baseTemplate = try context.environment.loadTemplate(name: templateName) + let baseTemplate = try context.environment.loadTemplate(name: templateName) - let blockContext: BlockContext - if let currentBlockContext = context[BlockContext.contextKey] as? BlockContext { - blockContext = currentBlockContext - for (name, block) in blocks { - blockContext.push(block, forKey: name) - } - } else { - blockContext = BlockContext(blocks: blocks) - } + let blockContext: BlockContext + if let currentBlockContext = context[BlockContext.contextKey] as? BlockContext { + blockContext = currentBlockContext + for (name, block) in blocks { + blockContext.push(block, forKey: name) + } + } else { + blockContext = BlockContext(blocks: blocks) + } - do { - // pushes base template and renders it's content - // block_context contains all blocks from child templates - return try context.push(dictionary: [BlockContext.contextKey: blockContext]) { - try baseTemplate.render(context) - } - } catch { - // if error template is already set (see catch in BlockNode) - // and it happend in the same template as current template - // there is no need to wrap it in another error - if let error = error as? TemplateSyntaxError, error.templateName != token?.sourceMap.filename { - throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens) - } else { - throw error - } - } - } + do { + // pushes base template and renders it's content + // block_context contains all blocks from child templates + return try context.push(dictionary: [BlockContext.contextKey: blockContext]) { + try baseTemplate.render(context) + } + } catch { + // if error template is already set (see catch in BlockNode) + // and it happend in the same template as current template + // there is no need to wrap it in another error + if let error = error as? TemplateSyntaxError, error.templateName != token?.sourceMap.filename { + throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens) + } else { + throw error + } + } + } } class BlockNode: NodeType { - let name: String - let nodes: [NodeType] - let token: Token? + let name: String + let nodes: [NodeType] + let token: Token? - class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { - let bits = token.components + class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { + let bits = token.components - guard bits.count == 2 else { - throw TemplateSyntaxError("'block' tag takes one argument, the block name") - } + guard bits.count == 2 else { + throw TemplateSyntaxError("'block' tag takes one argument, the block name") + } - let blockName = bits[1] - let nodes = try parser.parse(until(["endblock"])) - _ = parser.nextToken() - return BlockNode(name: blockName, nodes: nodes, token: token) - } + let blockName = bits[1] + let nodes = try parser.parse(until(["endblock"])) + _ = parser.nextToken() + return BlockNode(name: blockName, nodes: nodes, token: token) + } - init(name: String, nodes: [NodeType], token: Token) { - self.name = name - self.nodes = nodes - self.token = token - } + init(name: String, nodes: [NodeType], token: Token) { + self.name = name + self.nodes = nodes + self.token = token + } - func render(_ context: Context) throws -> String { - if let blockContext = context[BlockContext.contextKey] as? BlockContext, let child = blockContext.pop(name) { - let childContext: [String: Any] = [ - BlockContext.contextKey: blockContext, - "block": ["super": try self.render(context)] - ] + func render(_ context: Context) throws -> String { + if let blockContext = context[BlockContext.contextKey] as? BlockContext, let child = blockContext.pop(name) { + let childContext: [String: Any] = [ + BlockContext.contextKey: blockContext, + "block": ["super": try self.render(context)] + ] - // render extension node - do { - return try context.push(dictionary: childContext) { - try child.render(context) - } - } catch { - throw error.withToken(child.token) - } - } + // render extension node + do { + return try context.push(dictionary: childContext) { + try child.render(context) + } + } catch { + throw error.withToken(child.token) + } + } - let result = try renderNodes(nodes, context) - context.cacheBlock(name, content: result) - return result - } + let result = try renderNodes(nodes, context) + context.cacheBlock(name, content: result) + return result + } } diff --git a/Sources/Stencil/KeyPath.swift b/Sources/Stencil/KeyPath.swift index 98767b7..d13307b 100644 --- a/Sources/Stencil/KeyPath.swift +++ b/Sources/Stencil/KeyPath.swift @@ -2,111 +2,111 @@ import Foundation /// A structure used to represent a template variable, and to resolve it in a given context. final class KeyPath { - private var components = [String]() - private var current = "" - private var partialComponents = [String]() - private var subscriptLevel = 0 + private var components = [String]() + private var current = "" + private var partialComponents = [String]() + private var subscriptLevel = 0 - let variable: String - let context: Context + let variable: String + let context: Context - // Split the keypath string and resolve references if possible - init(_ variable: String, in context: Context) { - self.variable = variable - self.context = context - } + // Split the keypath string and resolve references if possible + init(_ variable: String, in context: Context) { + self.variable = variable + self.context = context + } - func parse() throws -> [String] { - defer { - components = [] - current = "" - partialComponents = [] - subscriptLevel = 0 - } + func parse() throws -> [String] { + defer { + components = [] + current = "" + partialComponents = [] + subscriptLevel = 0 + } - for character in variable { - switch character { - case "." where subscriptLevel == 0: - try foundSeparator() - case "[": - try openBracket() - case "]": - try closeBracket() - default: - try addCharacter(character) - } - } - try finish() + for character in variable { + switch character { + case "." where subscriptLevel == 0: + try foundSeparator() + case "[": + try openBracket() + case "]": + try closeBracket() + default: + try addCharacter(character) + } + } + try finish() - return components - } + return components + } - private func foundSeparator() throws { - if !current.isEmpty { - partialComponents.append(current) - } + private func foundSeparator() throws { + if !current.isEmpty { + partialComponents.append(current) + } - guard !partialComponents.isEmpty else { - throw TemplateSyntaxError("Unexpected '.' in variable '\(variable)'") - } + guard !partialComponents.isEmpty else { + throw TemplateSyntaxError("Unexpected '.' in variable '\(variable)'") + } - components += partialComponents - current = "" - partialComponents = [] - } + components += partialComponents + current = "" + partialComponents = [] + } - // when opening the first bracket, we must have a partial component - private func openBracket() throws { - guard !partialComponents.isEmpty || !current.isEmpty else { - throw TemplateSyntaxError("Unexpected '[' in variable '\(variable)'") - } + // when opening the first bracket, we must have a partial component + private func openBracket() throws { + guard !partialComponents.isEmpty || !current.isEmpty else { + throw TemplateSyntaxError("Unexpected '[' in variable '\(variable)'") + } - if subscriptLevel > 0 { - current.append("[") - } else if !current.isEmpty { - partialComponents.append(current) - current = "" - } + if subscriptLevel > 0 { + current.append("[") + } else if !current.isEmpty { + partialComponents.append(current) + current = "" + } - subscriptLevel += 1 - } + subscriptLevel += 1 + } - // for a closing bracket at root level, try to resolve the reference - private func closeBracket() throws { - guard subscriptLevel > 0 else { - throw TemplateSyntaxError("Unbalanced ']' in variable '\(variable)'") - } + // for a closing bracket at root level, try to resolve the reference + private func closeBracket() throws { + guard subscriptLevel > 0 else { + throw TemplateSyntaxError("Unbalanced ']' in variable '\(variable)'") + } - if subscriptLevel > 1 { - current.append("]") - } else if !current.isEmpty, - let value = try Variable(current).resolve(context) { - partialComponents.append("\(value)") - current = "" - } else { - throw TemplateSyntaxError("Unable to resolve subscript '\(current)' in variable '\(variable)'") - } + if subscriptLevel > 1 { + current.append("]") + } else if !current.isEmpty, + let value = try Variable(current).resolve(context) { + partialComponents.append("\(value)") + current = "" + } else { + throw TemplateSyntaxError("Unable to resolve subscript '\(current)' in variable '\(variable)'") + } - subscriptLevel -= 1 - } + subscriptLevel -= 1 + } - private func addCharacter(_ character: Character) throws { - guard partialComponents.isEmpty || subscriptLevel > 0 else { - throw TemplateSyntaxError("Unexpected character '\(character)' in variable '\(variable)'") - } + private func addCharacter(_ character: Character) throws { + guard partialComponents.isEmpty || subscriptLevel > 0 else { + throw TemplateSyntaxError("Unexpected character '\(character)' in variable '\(variable)'") + } - current.append(character) - } + current.append(character) + } - private func finish() throws { - // check if we have a last piece - if !current.isEmpty { - partialComponents.append(current) - } - components += partialComponents + private func finish() throws { + // check if we have a last piece + if !current.isEmpty { + partialComponents.append(current) + } + components += partialComponents - guard subscriptLevel == 0 else { - throw TemplateSyntaxError("Unbalanced subscript brackets in variable '\(variable)'") - } - } + guard subscriptLevel == 0 else { + throw TemplateSyntaxError("Unbalanced subscript brackets in variable '\(variable)'") + } + } } diff --git a/Sources/Stencil/LazyValueWrapper.swift b/Sources/Stencil/LazyValueWrapper.swift index ed07f04..1efc7c0 100644 --- a/Sources/Stencil/LazyValueWrapper.swift +++ b/Sources/Stencil/LazyValueWrapper.swift @@ -1,57 +1,57 @@ /// 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? + 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 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 - } + /// 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() } - } + /// 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 - } - } + 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 - } + public func resolve(_ context: Context) throws -> Any? { + let value = try self.value(context: context) + return try (value as? Resolvable)?.resolve(context) ?? value + } } diff --git a/Sources/Stencil/Lexer.swift b/Sources/Stencil/Lexer.swift index b558b49..0140d63 100644 --- a/Sources/Stencil/Lexer.swift +++ b/Sources/Stencil/Lexer.swift @@ -7,251 +7,251 @@ public typealias ContentLocation = (content: String, lineNumber: UInt, lineOffse // swiftlint:enable large_tuple struct Lexer { - let templateName: String? - let templateString: String - let lines: [Line] + let templateName: String? + let templateString: String + let lines: [Line] - /// The potential token start characters. In a template these appear after a - /// `{` character, for example `{{`, `{%`, `{#`, ... - private static let tokenChars: [Unicode.Scalar] = ["{", "%", "#"] + /// The potential token start characters. In a template these appear after a + /// `{` character, for example `{{`, `{%`, `{#`, ... + private static let tokenChars: [Unicode.Scalar] = ["{", "%", "#"] - /// The minimum length of a tag - private static let tagLength = 2 + /// The minimum length of a tag + private static let tagLength = 2 - /// The token end characters, corresponding to their token start characters. - /// For example, a variable token starts with `{{` and ends with `}}` - private static let tokenCharMap: [Unicode.Scalar: Unicode.Scalar] = [ - "{": "}", - "%": "%", - "#": "#" - ] + /// The token end characters, corresponding to their token start characters. + /// For example, a variable token starts with `{{` and ends with `}}` + private static let tokenCharMap: [Unicode.Scalar: Unicode.Scalar] = [ + "{": "}", + "%": "%", + "#": "#" + ] - /// Characters controlling whitespace trimming behaviour - private static let behaviourMap: [Character: WhitespaceBehaviour.Behaviour] = [ - "+": .keep, - "-": .trim - ] + /// Characters controlling whitespace trimming behaviour + private static let behaviourMap: [Character: WhitespaceBehaviour.Behaviour] = [ + "+": .keep, + "-": .trim + ] - init(templateName: String? = nil, templateString: String) { - self.templateName = templateName - self.templateString = templateString + init(templateName: String? = nil, templateString: String) { + self.templateName = templateName + self.templateString = templateString - self.lines = zip(1..., templateString.components(separatedBy: .newlines)).compactMap { index, line in - guard !line.isEmpty, - let range = templateString.range(of: line) else { return nil } - return (content: line, number: UInt(index), range) - } - } + self.lines = zip(1..., templateString.components(separatedBy: .newlines)).compactMap { index, line in + guard !line.isEmpty, + let range = templateString.range(of: line) else { return nil } + return (content: line, number: UInt(index), range) + } + } - private func behaviour(string: String, tagLength: Int) -> WhitespaceBehaviour { - let leftIndex = string.index(string.startIndex, offsetBy: tagLength, limitedBy: string.endIndex) - let rightIndex = string.index(string.endIndex, offsetBy: -(tagLength + 1), limitedBy: string.startIndex) + private func behaviour(string: String, tagLength: Int) -> WhitespaceBehaviour { + let leftIndex = string.index(string.startIndex, offsetBy: tagLength, limitedBy: string.endIndex) + let rightIndex = string.index(string.endIndex, offsetBy: -(tagLength + 1), limitedBy: string.startIndex) - return WhitespaceBehaviour( - leading: Self.behaviourMap[leftIndex.map { string[$0] } ?? " "] ?? .unspecified, - trailing: Self.behaviourMap[rightIndex.map { string[$0] } ?? " "] ?? .unspecified - ) - } + return WhitespaceBehaviour( + leading: Self.behaviourMap[leftIndex.map { string[$0] } ?? " "] ?? .unspecified, + trailing: Self.behaviourMap[rightIndex.map { string[$0] } ?? " "] ?? .unspecified + ) + } - /// Create a token that will be passed on to the parser, with the given - /// content and a range. The content will be tested to see if it's a - /// `variable`, a `block` or a `comment`, otherwise it'll default to a simple - /// `text` token. - /// - /// - Parameters: - /// - string: The content string of the token - /// - range: The range within the template content, used for smart - /// error reporting - func createToken(string: String, at range: Range) -> Token { - func strip(length: (Int, Int) = (Self.tagLength, Self.tagLength)) -> String { - guard string.count > (length.0 + length.1) else { return "" } - let trimmed = String(string.dropFirst(length.0).dropLast(length.1)) - .components(separatedBy: "\n") - .filter { !$0.isEmpty } - .map { $0.trim(character: " ") } - .joined(separator: " ") - return trimmed - } + /// Create a token that will be passed on to the parser, with the given + /// content and a range. The content will be tested to see if it's a + /// `variable`, a `block` or a `comment`, otherwise it'll default to a simple + /// `text` token. + /// + /// - Parameters: + /// - string: The content string of the token + /// - range: The range within the template content, used for smart + /// error reporting + func createToken(string: String, at range: Range) -> Token { + func strip(length: (Int, Int) = (Self.tagLength, Self.tagLength)) -> String { + guard string.count > (length.0 + length.1) else { return "" } + let trimmed = String(string.dropFirst(length.0).dropLast(length.1)) + .components(separatedBy: "\n") + .filter { !$0.isEmpty } + .map { $0.trim(character: " ") } + .joined(separator: " ") + return trimmed + } - if string.hasPrefix("{{") || string.hasPrefix("{%") || string.hasPrefix("{#") { - let behaviour = string.hasPrefix("{%") ? behaviour(string: string, tagLength: Self.tagLength) : .unspecified - let stripLengths = ( - Self.tagLength + (behaviour.leading != .unspecified ? 1 : 0), - Self.tagLength + (behaviour.trailing != .unspecified ? 1 : 0) - ) + if string.hasPrefix("{{") || string.hasPrefix("{%") || string.hasPrefix("{#") { + let behaviour = string.hasPrefix("{%") ? behaviour(string: string, tagLength: Self.tagLength) : .unspecified + let stripLengths = ( + Self.tagLength + (behaviour.leading != .unspecified ? 1 : 0), + Self.tagLength + (behaviour.trailing != .unspecified ? 1 : 0) + ) - let value = strip(length: stripLengths) - let range = templateString.range(of: value, range: range) ?? range - let location = rangeLocation(range) - let sourceMap = SourceMap(filename: templateName, location: location) + let value = strip(length: stripLengths) + let range = templateString.range(of: value, range: range) ?? range + let location = rangeLocation(range) + let sourceMap = SourceMap(filename: templateName, location: location) - if string.hasPrefix("{{") { - return .variable(value: value, at: sourceMap) - } else if string.hasPrefix("{%") { - return .block(value: strip(length: stripLengths), at: sourceMap, whitespace: behaviour) - } else if string.hasPrefix("{#") { - return .comment(value: value, at: sourceMap) - } - } + if string.hasPrefix("{{") { + return .variable(value: value, at: sourceMap) + } else if string.hasPrefix("{%") { + return .block(value: strip(length: stripLengths), at: sourceMap, whitespace: behaviour) + } else if string.hasPrefix("{#") { + return .comment(value: value, at: sourceMap) + } + } - let location = rangeLocation(range) - let sourceMap = SourceMap(filename: templateName, location: location) - return .text(value: string, at: sourceMap) - } + let location = rangeLocation(range) + let sourceMap = SourceMap(filename: templateName, location: location) + return .text(value: string, at: sourceMap) + } - /// Transforms the template into a list of tokens, that will eventually be - /// passed on to the parser. - /// - /// - Returns: The list of tokens (see `createToken(string: at:)`). - func tokenize() -> [Token] { - var tokens: [Token] = [] + /// Transforms the template into a list of tokens, that will eventually be + /// passed on to the parser. + /// + /// - Returns: The list of tokens (see `createToken(string: at:)`). + func tokenize() -> [Token] { + var tokens: [Token] = [] - let scanner = Scanner(templateString) - while !scanner.isEmpty { - if let (char, text) = scanner.scanForTokenStart(Self.tokenChars) { - if !text.isEmpty { - tokens.append(createToken(string: text, at: scanner.range)) - } + let scanner = Scanner(templateString) + while !scanner.isEmpty { + if let (char, text) = scanner.scanForTokenStart(Self.tokenChars) { + if !text.isEmpty { + tokens.append(createToken(string: text, at: scanner.range)) + } - guard let end = Self.tokenCharMap[char] else { continue } - let result = scanner.scanForTokenEnd(end) - tokens.append(createToken(string: result, at: scanner.range)) - } else { - tokens.append(createToken(string: scanner.content, at: scanner.range)) - scanner.content = "" - } - } + guard let end = Self.tokenCharMap[char] else { continue } + let result = scanner.scanForTokenEnd(end) + tokens.append(createToken(string: result, at: scanner.range)) + } else { + tokens.append(createToken(string: scanner.content, at: scanner.range)) + scanner.content = "" + } + } - return tokens - } + return tokens + } - /// Finds the line matching the given range (for a token) - /// - /// - Parameter range: The range to search for. - /// - Returns: The content for that line, the line number and offset within - /// the line. - func rangeLocation(_ range: Range) -> ContentLocation { - guard let line = self.lines.first(where: { $0.range.contains(range.lowerBound) }) else { - return ("", 0, 0) - } - let offset = templateString.distance(from: line.range.lowerBound, to: range.lowerBound) - return (line.content, line.number, offset) - } + /// Finds the line matching the given range (for a token) + /// + /// - Parameter range: The range to search for. + /// - Returns: The content for that line, the line number and offset within + /// the line. + func rangeLocation(_ range: Range) -> ContentLocation { + guard let line = self.lines.first(where: { $0.range.contains(range.lowerBound) }) else { + return ("", 0, 0) + } + let offset = templateString.distance(from: line.range.lowerBound, to: range.lowerBound) + return (line.content, line.number, offset) + } } class Scanner { - let originalContent: String - var content: String - var range: Range + let originalContent: String + var content: String + var range: Range - /// The start delimiter for a token. - private static let tokenStartDelimiter: Unicode.Scalar = "{" - /// And the corresponding end delimiter for a token. - private static let tokenEndDelimiter: Unicode.Scalar = "}" + /// The start delimiter for a token. + private static let tokenStartDelimiter: Unicode.Scalar = "{" + /// And the corresponding end delimiter for a token. + private static let tokenEndDelimiter: Unicode.Scalar = "}" - init(_ content: String) { - self.originalContent = content - self.content = content - range = content.unicodeScalars.startIndex.. String { - var foundChar = false + /// Scans for the end of a token, with a specific ending character. If we're + /// searching for the end of a block token `%}`, this method receives a `%`. + /// The scanner will search for that `%` followed by a `}`. + /// + /// Note: if the end of a token is found, the `content` and `range` + /// properties are updated to reflect this. `content` will be set to what + /// remains of the template after the token. `range` will be set to the range + /// of the token within the template. + /// + /// - Parameter tokenChar: The token end character to search for. + /// - Returns: The content of a token, or "" if no token end was found. + func scanForTokenEnd(_ tokenChar: Unicode.Scalar) -> String { + var foundChar = false - for (index, char) in zip(0..., content.unicodeScalars) { - if foundChar && char == Self.tokenEndDelimiter { - let result = String(content.unicodeScalars.prefix(index + 1)) - content = String(content.unicodeScalars.dropFirst(index + 1)) - range = range.upperBound.. (Unicode.Scalar, String)? { - var foundBrace = false + /// Scans for the start of a token, with a list of potential starting + /// characters. To scan for the start of variables (`{{`), blocks (`{%`) and + /// comments (`{#`), this method receives the characters `{`, `%` and `#`. + /// The scanner will search for a `{`, followed by one of the search + /// characters. It will give the found character, and the content that came + /// before the token. + /// + /// Note: if the start of a token is found, the `content` and `range` + /// properties are updated to reflect this. `content` will be set to what + /// remains of the template starting with the token. `range` will be set to + /// the start of the token within the template. + /// + /// - Parameter tokenChars: List of token start characters to search for. + /// - Returns: The found token start character, together with the content + /// before the token, or nil of no token start was found. + func scanForTokenStart(_ tokenChars: [Unicode.Scalar]) -> (Unicode.Scalar, String)? { + var foundBrace = false - range = range.upperBound.. String.Index? { - var index = startIndex + func findFirstNot(character: Character) -> String.Index? { + var index = startIndex - while index != endIndex { - if character != self[index] { - return index - } - index = self.index(after: index) - } + while index != endIndex { + if character != self[index] { + return index + } + index = self.index(after: index) + } - return nil - } + return nil + } - func findLastNot(character: Character) -> String.Index? { - var index = self.index(before: endIndex) + func findLastNot(character: Character) -> String.Index? { + var index = self.index(before: endIndex) - while index != startIndex { - if character != self[index] { - return self.index(after: index) - } - index = self.index(before: index) - } + while index != startIndex { + if character != self[index] { + return self.index(after: index) + } + index = self.index(before: index) + } - return nil - } + return nil + } - func trim(character: Character) -> String { - let first = findFirstNot(character: character) ?? startIndex - let last = findLastNot(character: character) ?? endIndex - return String(self[first.. String { + let first = findFirstNot(character: character) ?? startIndex + let last = findLastNot(character: character) ?? endIndex + return String(self[first.. Template - /// Load a template with the given list of names - func loadTemplate(names: [String], environment: Environment) throws -> Template + /// Load a template with the given name + func loadTemplate(name: String, environment: Environment) throws -> Template + /// Load a template with the given list of names + func loadTemplate(names: [String], environment: Environment) throws -> Template } extension Loader { - /// Default implementation, tries to load the first template that exists from the list of given names - public func loadTemplate(names: [String], environment: Environment) throws -> Template { - for name in names { - do { - return try loadTemplate(name: name, environment: environment) - } catch is TemplateDoesNotExist { - continue - } catch { - throw error - } - } + /// Default implementation, tries to load the first template that exists from the list of given names + public func loadTemplate(names: [String], environment: Environment) throws -> Template { + for name in names { + do { + return try loadTemplate(name: name, environment: environment) + } catch is TemplateDoesNotExist { + continue + } catch { + throw error + } + } - throw TemplateDoesNotExist(templateNames: names, loader: self) - } + throw TemplateDoesNotExist(templateNames: names, loader: self) + } } // A class for loading a template from disk public class FileSystemLoader: Loader, CustomStringConvertible { - public let paths: [Path] + public let paths: [Path] - public init(paths: [Path]) { - self.paths = paths - } + public init(paths: [Path]) { + self.paths = paths + } - public init(bundle: [Bundle]) { - self.paths = bundle.compactMap { bundle in - Path(bundle.path) - } - } + public init(bundle: [Bundle]) { + self.paths = bundle.compactMap { bundle in + Path(bundle.path) + } + } - public var description: String { - "FileSystemLoader(\(paths))" - } + public var description: String { + "FileSystemLoader(\(paths))" + } - public func loadTemplate(name: String, environment: Environment) throws -> Template { - for path in paths { - let templatePath = try path.safeJoin(path: name) + public func loadTemplate(name: String, environment: Environment) throws -> Template { + for path in paths { + let templatePath = try path.safeJoin(path: name) - if !templatePath.exists { - continue - } + if !templatePath.exists { + continue + } - let content: String = try String(contentsOf: templatePath) - return environment.templateClass.init(templateString: content, environment: environment, name: name) - } + let content: String = try String(contentsOf: templatePath) + return environment.templateClass.init(templateString: content, environment: environment, name: name) + } - throw TemplateDoesNotExist(templateNames: [name], loader: self) - } + throw TemplateDoesNotExist(templateNames: [name], loader: self) + } - public func loadTemplate(names: [String], environment: Environment) throws -> Template { - for path in paths { - for templateName in names { - let templatePath = try path.safeJoin(path: templateName) + public func loadTemplate(names: [String], environment: Environment) throws -> Template { + for path in paths { + for templateName in names { + let templatePath = try path.safeJoin(path: templateName) - if templatePath.exists { - let content: String = try String(contentsOf: templatePath) - return environment.templateClass.init(templateString: content, environment: environment, name: templateName) - } - } - } + if templatePath.exists { + let content: String = try String(contentsOf: templatePath) + return environment.templateClass.init(templateString: content, environment: environment, name: templateName) + } + } + } - throw TemplateDoesNotExist(templateNames: names, loader: self) - } + throw TemplateDoesNotExist(templateNames: names, loader: self) + } } public class DictionaryLoader: Loader { - public let templates: [String: String] + public let templates: [String: String] - public init(templates: [String: String]) { - self.templates = templates - } + public init(templates: [String: String]) { + self.templates = templates + } - public func loadTemplate(name: String, environment: Environment) throws -> Template { - if let content = templates[name] { - return environment.templateClass.init(templateString: content, environment: environment, name: name) - } + public func loadTemplate(name: String, environment: Environment) throws -> Template { + if let content = templates[name] { + return environment.templateClass.init(templateString: content, environment: environment, name: name) + } - throw TemplateDoesNotExist(templateNames: [name], loader: self) - } + throw TemplateDoesNotExist(templateNames: [name], loader: self) + } - public func loadTemplate(names: [String], environment: Environment) throws -> Template { - for name in names { - if let content = templates[name] { - return environment.templateClass.init(templateString: content, environment: environment, name: name) - } - } + public func loadTemplate(names: [String], environment: Environment) throws -> Template { + for name in names { + if let content = templates[name] { + return environment.templateClass.init(templateString: content, environment: environment, name: name) + } + } - throw TemplateDoesNotExist(templateNames: names, loader: self) - } + throw TemplateDoesNotExist(templateNames: names, loader: self) + } } extension Path { - func safeJoin(path: String) throws -> Path { - let newPath = self / path + func safeJoin(path: String) throws -> Path { + let newPath = self / path - if !newPath.string.hasPrefix(self.string) { - throw SuspiciousFileOperation(basePath: self, path: newPath) - } + if !newPath.string.hasPrefix(self.string) { + throw SuspiciousFileOperation(basePath: self, path: newPath) + } - return newPath - } + return newPath + } } class SuspiciousFileOperation: Error { - let basePath: Path - let path: Path + let basePath: Path + let path: Path - init(basePath: Path, path: Path) { - self.basePath = basePath - self.path = path - } + init(basePath: Path, path: Path) { + self.basePath = basePath + self.path = path + } - var description: String { - "Path `\(path)` is located outside of base path `\(basePath)`" - } + var description: String { + "Path `\(path)` is located outside of base path `\(basePath)`" + } } diff --git a/Sources/Stencil/Node.swift b/Sources/Stencil/Node.swift index 462b254..87ace42 100644 --- a/Sources/Stencil/Node.swift +++ b/Sources/Stencil/Node.swift @@ -2,184 +2,184 @@ import Foundation /// Represents a parsed node public protocol NodeType { - /// Render the node in the given context - func render(_ context: Context) throws -> String + /// Render the node in the given context + func render(_ context: Context) throws -> String - /// Reference to this node's token - var token: Token? { get } + /// Reference to this node's token + var token: Token? { get } } /// Render the collection of nodes in the given context public func renderNodes(_ nodes: [NodeType], _ context: Context) throws -> String { - var result = "" + var result = "" - for node in nodes { - do { - result += try node.render(context) - } catch { - throw error.withToken(node.token) - } + for node in nodes { + do { + result += try node.render(context) + } catch { + throw error.withToken(node.token) + } - let shouldBreak = context[LoopTerminationNode.breakContextKey] != nil - let shouldContinue = context[LoopTerminationNode.continueContextKey] != nil + let shouldBreak = context[LoopTerminationNode.breakContextKey] != nil + let shouldContinue = context[LoopTerminationNode.continueContextKey] != nil - if shouldBreak || shouldContinue { - break - } - } + if shouldBreak || shouldContinue { + break + } + } - return result + return result } /// Simple node, used for triggering a closure during rendering public class SimpleNode: NodeType { - public let handler: (Context) throws -> String - public let token: Token? + public let handler: (Context) throws -> String + public let token: Token? - public init(token: Token, handler: @escaping (Context) throws -> String) { - self.token = token - self.handler = handler - } + public init(token: Token, handler: @escaping (Context) throws -> String) { + self.token = token + self.handler = handler + } - public func render(_ context: Context) throws -> String { - try handler(context) - } + public func render(_ context: Context) throws -> String { + try handler(context) + } } /// Represents a block of text, renders the text public class TextNode: NodeType { - public let text: String - public let token: Token? - public let trimBehaviour: TrimBehaviour + public let text: String + public let token: Token? + public let trimBehaviour: TrimBehaviour - public init(text: String, trimBehaviour: TrimBehaviour = .nothing) { - self.text = text - self.token = nil - self.trimBehaviour = trimBehaviour - } + public init(text: String, trimBehaviour: TrimBehaviour = .nothing) { + self.text = text + self.token = nil + self.trimBehaviour = trimBehaviour + } - public func render(_ context: Context) throws -> String { - var string = self.text - if trimBehaviour.leading != .nothing, !string.isEmpty { - let range = NSRange(.. String { + var string = self.text + if trimBehaviour.leading != .nothing, !string.isEmpty { + let range = NSRange(.. Any? + /// Try to resolve this with the given context + func resolve(_ context: Context) throws -> Any? } /// Represents a variable, renders the variable, may have conditional expressions. public class VariableNode: NodeType { - public let variable: Resolvable - public var token: Token? - let condition: Expression? - let elseExpression: Resolvable? + public let variable: Resolvable + public var token: Token? + let condition: Expression? + let elseExpression: Resolvable? - class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { - let components = token.components + class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { + let components = token.components - func hasToken(_ token: String, at index: Int) -> Bool { - components.count > (index + 1) && components[index] == token - } - func compileResolvable(_ components: [String], containedIn token: Token) throws -> Resolvable { - try (try? parser.compileExpression(components: components, token: token)) ?? - parser.compileFilter(components.joined(separator: " "), containedIn: token) - } + func hasToken(_ token: String, at index: Int) -> Bool { + components.count > (index + 1) && components[index] == token + } + func compileResolvable(_ components: [String], containedIn token: Token) throws -> Resolvable { + try (try? parser.compileExpression(components: components, token: token)) ?? + parser.compileFilter(components.joined(separator: " "), containedIn: token) + } - let variable: Resolvable - let condition: Expression? - let elseExpression: Resolvable? + let variable: Resolvable + let condition: Expression? + let elseExpression: Resolvable? - if hasToken("if", at: 1) { - variable = try compileResolvable([components[0]], containedIn: token) + if hasToken("if", at: 1) { + variable = try compileResolvable([components[0]], containedIn: token) - let components = components.suffix(from: 2) - if let elseIndex = components.firstIndex(of: "else") { - condition = try parser.compileExpression(components: Array(components.prefix(upTo: elseIndex)), token: token) - let elseToken = Array(components.suffix(from: elseIndex.advanced(by: 1))) - elseExpression = try compileResolvable(elseToken, containedIn: token) - } else { - condition = try parser.compileExpression(components: Array(components), token: token) - elseExpression = nil - } - } else if !components.isEmpty { - variable = try compileResolvable(components, containedIn: token) - condition = nil - elseExpression = nil - } else { - throw TemplateSyntaxError(reason: "Missing variable name", token: token) - } + let components = components.suffix(from: 2) + if let elseIndex = components.firstIndex(of: "else") { + condition = try parser.compileExpression(components: Array(components.prefix(upTo: elseIndex)), token: token) + let elseToken = Array(components.suffix(from: elseIndex.advanced(by: 1))) + elseExpression = try compileResolvable(elseToken, containedIn: token) + } else { + condition = try parser.compileExpression(components: Array(components), token: token) + elseExpression = nil + } + } else if !components.isEmpty { + variable = try compileResolvable(components, containedIn: token) + condition = nil + elseExpression = nil + } else { + throw TemplateSyntaxError(reason: "Missing variable name", token: token) + } - return VariableNode(variable: variable, token: token, condition: condition, elseExpression: elseExpression) - } + return VariableNode(variable: variable, token: token, condition: condition, elseExpression: elseExpression) + } - public init(variable: Resolvable, token: Token? = nil) { - self.variable = variable - self.token = token - self.condition = nil - self.elseExpression = nil - } + public init(variable: Resolvable, token: Token? = nil) { + self.variable = variable + self.token = token + self.condition = nil + self.elseExpression = nil + } - init(variable: Resolvable, token: Token? = nil, condition: Expression?, elseExpression: Resolvable?) { - self.variable = variable - self.token = token - self.condition = condition - self.elseExpression = elseExpression - } + init(variable: Resolvable, token: Token? = nil, condition: Expression?, elseExpression: Resolvable?) { + self.variable = variable + self.token = token + self.condition = condition + self.elseExpression = elseExpression + } - public init(variable: String, token: Token? = nil) { - self.variable = Variable(variable) - self.token = token - self.condition = nil - self.elseExpression = nil - } + public init(variable: String, token: Token? = nil) { + self.variable = Variable(variable) + self.token = token + self.condition = nil + self.elseExpression = nil + } - public func render(_ context: Context) throws -> String { - if let condition = self.condition, try condition.evaluate(context: context) == false { - return try elseExpression?.resolve(context).map(stringify) ?? "" - } + public func render(_ context: Context) throws -> String { + if let condition = self.condition, try condition.evaluate(context: context) == false { + return try elseExpression?.resolve(context).map(stringify) ?? "" + } - let result = try variable.resolve(context) - return stringify(result) - } + let result = try variable.resolve(context) + return stringify(result) + } } func stringify(_ result: Any?) -> String { - if let result = result as? String { - return result - } else if let array = result as? [Any?] { - return unwrap(array).description - } else if let result = result as? CustomStringConvertible { - return result.description - } else if let result = result as? NSObject { - return result.description - } + if let result = result as? String { + return result + } else if let array = result as? [Any?] { + return unwrap(array).description + } else if let result = result as? CustomStringConvertible { + return result.description + } else if let result = result as? NSObject { + return result.description + } - return "" + return "" } func unwrap(_ array: [Any?]) -> [Any] { - array.map { (item: Any?) -> Any in - if let item = item { - if let items = item as? [Any?] { - return unwrap(items) - } else { - return item - } - } else { - return item as Any - } - } + array.map { (item: Any?) -> Any in + if let item = item { + if let items = item as? [Any?] { + return unwrap(items) + } else { + return item + } + } else { + return item as Any + } + } } diff --git a/Sources/Stencil/NowTag.swift b/Sources/Stencil/NowTag.swift index bad6627..a557716 100644 --- a/Sources/Stencil/NowTag.swift +++ b/Sources/Stencil/NowTag.swift @@ -2,43 +2,43 @@ import Foundation class NowNode: NodeType { - let format: Variable - let token: Token? + let format: Variable + let token: Token? - class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { - var format: Variable? + class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { + var format: Variable? - let components = token.components - guard components.count <= 2 else { - throw TemplateSyntaxError("'now' tags may only have one argument: the format string.") - } - if components.count == 2 { - format = Variable(components[1]) - } + let components = token.components + guard components.count <= 2 else { + throw TemplateSyntaxError("'now' tags may only have one argument: the format string.") + } + if components.count == 2 { + format = Variable(components[1]) + } - return NowNode(format: format, token: token) - } + return NowNode(format: format, token: token) + } - init(format: Variable?, token: Token? = nil) { - self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"") - self.token = token - } + init(format: Variable?, token: Token? = nil) { + self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"") + self.token = token + } - func render(_ context: Context) throws -> String { - let date = Date() - let format = try self.format.resolve(context) + func render(_ context: Context) throws -> String { + let date = Date() + let format = try self.format.resolve(context) - var formatter: DateFormatter - if let format = format as? DateFormatter { - formatter = format - } else if let format = format as? String { - formatter = DateFormatter() - formatter.dateFormat = format - } else { - return "" - } + var formatter: DateFormatter + if let format = format as? DateFormatter { + formatter = format + } else if let format = format as? String { + formatter = DateFormatter() + formatter.dateFormat = format + } else { + return "" + } - return formatter.string(from: date) - } + return formatter.string(from: date) + } } #endif diff --git a/Sources/Stencil/Parser.swift b/Sources/Stencil/Parser.swift index 66e1ed5..306dd9b 100644 --- a/Sources/Stencil/Parser.swift +++ b/Sources/Stencil/Parser.swift @@ -1,272 +1,272 @@ /// Creates a checker that will stop parsing if it encounters a list of tags. /// Useful for example for scanning until a given "end"-node. public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) { - { _, token in - if let name = token.components.first { - for tag in tags where name == tag { - return true - } - } + { _, token in + if let name = token.components.first { + for tag in tags where name == tag { + return true + } + } - return false - } + return false + } } /// A class for parsing an array of tokens and converts them into a collection of Node's public class TokenParser { - /// Parser for finding a kind of node - public typealias TagParser = (TokenParser, Token) throws -> NodeType + /// Parser for finding a kind of node + public typealias TagParser = (TokenParser, Token) throws -> NodeType - fileprivate var tokens: [Token] - fileprivate(set) var parsedTokens: [Token] = [] - fileprivate let environment: Environment - fileprivate var previousWhiteSpace: WhitespaceBehaviour.Behaviour? + fileprivate var tokens: [Token] + fileprivate(set) var parsedTokens: [Token] = [] + fileprivate let environment: Environment + fileprivate var previousWhiteSpace: WhitespaceBehaviour.Behaviour? - /// Simple initializer - public init(tokens: [Token], environment: Environment) { - self.tokens = tokens - self.environment = environment - } + /// Simple initializer + public init(tokens: [Token], environment: Environment) { + self.tokens = tokens + self.environment = environment + } - /// Parse the given tokens into nodes - public func parse() throws -> [NodeType] { - try parse(nil) - } + /// Parse the given tokens into nodes + public func parse() throws -> [NodeType] { + try parse(nil) + } - /// Parse nodes until a specific "something" is detected, determined by the provided closure. - /// Combine this with the `until(:)` function above to scan nodes until a given token. - public func parse(_ parseUntil: ((_ parser: TokenParser, _ token: Token) -> (Bool))?) throws -> [NodeType] { - var nodes = [NodeType]() + /// Parse nodes until a specific "something" is detected, determined by the provided closure. + /// Combine this with the `until(:)` function above to scan nodes until a given token. + public func parse(_ parseUntil: ((_ parser: TokenParser, _ token: Token) -> (Bool))?) throws -> [NodeType] { + var nodes = [NodeType]() - while !tokens.isEmpty { - guard let token = nextToken() else { break } + while !tokens.isEmpty { + guard let token = nextToken() else { break } - switch token.kind { - case .text: - nodes.append(TextNode(text: token.contents, trimBehaviour: trimBehaviour)) - case .variable: - previousWhiteSpace = nil - try nodes.append(VariableNode.parse(self, token: token)) - case .block: - previousWhiteSpace = token.whitespace?.trailing - if let parseUntil = parseUntil, parseUntil(self, token) { - prependToken(token) - return nodes - } + switch token.kind { + case .text: + nodes.append(TextNode(text: token.contents, trimBehaviour: trimBehaviour)) + case .variable: + previousWhiteSpace = nil + try nodes.append(VariableNode.parse(self, token: token)) + case .block: + previousWhiteSpace = token.whitespace?.trailing + if let parseUntil = parseUntil, parseUntil(self, token) { + prependToken(token) + return nodes + } - if var tag = token.components.first { - do { - // special case for labeled tags (such as for loops) - if tag.hasSuffix(":") && token.components.count >= 2 { - tag = token.components[1] - } + if var tag = token.components.first { + do { + // special case for labeled tags (such as for loops) + if tag.hasSuffix(":") && token.components.count >= 2 { + tag = token.components[1] + } - let parser = try environment.findTag(name: tag) - let node = try parser(self, token) - nodes.append(node) - } catch { - throw error.withToken(token) - } - } - case .comment: - previousWhiteSpace = nil - continue - } - } + let parser = try environment.findTag(name: tag) + let node = try parser(self, token) + nodes.append(node) + } catch { + throw error.withToken(token) + } + } + case .comment: + previousWhiteSpace = nil + continue + } + } - return nodes - } + return nodes + } - /// Pop the next token (returning it) - public func nextToken() -> Token? { - if !tokens.isEmpty { - let nextToken = tokens.remove(at: 0) - parsedTokens.append(nextToken) - return nextToken - } + /// Pop the next token (returning it) + public func nextToken() -> Token? { + if !tokens.isEmpty { + let nextToken = tokens.remove(at: 0) + parsedTokens.append(nextToken) + return nextToken + } - return nil - } + return nil + } - func peekWhitespace() -> WhitespaceBehaviour.Behaviour? { - tokens.first?.whitespace?.leading - } + func peekWhitespace() -> WhitespaceBehaviour.Behaviour? { + tokens.first?.whitespace?.leading + } - /// Insert a token - public func prependToken(_ token: Token) { - tokens.insert(token, at: 0) - if parsedTokens.last == token { - parsedTokens.removeLast() - } - } + /// Insert a token + public func prependToken(_ token: Token) { + tokens.insert(token, at: 0) + if parsedTokens.last == token { + parsedTokens.removeLast() + } + } - /// Create filter expression from a string contained in provided token - public func compileFilter(_ filterToken: String, containedIn token: Token) throws -> Resolvable { - try environment.compileFilter(filterToken, containedIn: token) - } + /// Create filter expression from a string contained in provided token + public func compileFilter(_ filterToken: String, containedIn token: Token) throws -> Resolvable { + try environment.compileFilter(filterToken, containedIn: token) + } - /// Create boolean expression from components contained in provided token - public func compileExpression(components: [String], token: Token) throws -> Expression { - try environment.compileExpression(components: components, containedIn: token) - } + /// Create boolean expression from components contained in provided token + public func compileExpression(components: [String], token: Token) throws -> Expression { + try environment.compileExpression(components: components, containedIn: token) + } - /// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token - public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable { - try environment.compileResolvable(token, containedIn: containingToken) - } + /// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token + public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable { + try environment.compileResolvable(token, containedIn: containingToken) + } - private var trimBehaviour: TrimBehaviour { - var behaviour: TrimBehaviour = .nothing + private var trimBehaviour: TrimBehaviour { + var behaviour: TrimBehaviour = .nothing - if let leading = previousWhiteSpace { - if leading == .unspecified { - behaviour.leading = environment.trimBehaviour.trailing - } else { - behaviour.leading = leading == .trim ? .whitespaceAndNewLines : .nothing - } - } - if let trailing = peekWhitespace() { - if trailing == .unspecified { - behaviour.trailing = environment.trimBehaviour.leading - } else { - behaviour.trailing = trailing == .trim ? .whitespaceAndNewLines : .nothing - } - } + if let leading = previousWhiteSpace { + if leading == .unspecified { + behaviour.leading = environment.trimBehaviour.trailing + } else { + behaviour.leading = leading == .trim ? .whitespaceAndNewLines : .nothing + } + } + if let trailing = peekWhitespace() { + if trailing == .unspecified { + behaviour.trailing = environment.trimBehaviour.leading + } else { + behaviour.trailing = trailing == .trim ? .whitespaceAndNewLines : .nothing + } + } - return behaviour - } + return behaviour + } } extension Environment { - func findTag(name: String) throws -> Extension.TagParser { - for ext in extensions { - if let filter = ext.tags[name] { - return filter - } - } + func findTag(name: String) throws -> Extension.TagParser { + for ext in extensions { + if let filter = ext.tags[name] { + return filter + } + } - throw TemplateSyntaxError("Unknown template tag '\(name)'") - } + throw TemplateSyntaxError("Unknown template tag '\(name)'") + } - func findFilter(_ name: String) throws -> FilterType { - for ext in extensions { - if let filter = ext.filters[name] { - return filter - } - } + func findFilter(_ name: String) throws -> FilterType { + for ext in extensions { + if let filter = ext.filters[name] { + return filter + } + } - let suggestedFilters = self.suggestedFilters(for: name) - if suggestedFilters.isEmpty { - throw TemplateSyntaxError("Unknown filter '\(name)'.") - } else { - throw TemplateSyntaxError( - """ - Unknown filter '\(name)'. \ - Found similar filters: \(suggestedFilters.map { "'\($0)'" }.joined(separator: ", ")). - """ - ) - } - } + let suggestedFilters = self.suggestedFilters(for: name) + if suggestedFilters.isEmpty { + throw TemplateSyntaxError("Unknown filter '\(name)'.") + } else { + throw TemplateSyntaxError( + """ + Unknown filter '\(name)'. \ + Found similar filters: \(suggestedFilters.map { "'\($0)'" }.joined(separator: ", ")). + """ + ) + } + } - private func suggestedFilters(for name: String) -> [String] { - let allFilters = extensions.flatMap { $0.filters.keys } + private func suggestedFilters(for name: String) -> [String] { + let allFilters = extensions.flatMap { $0.filters.keys } - let filtersWithDistance = allFilters - .map { (filterName: $0, distance: $0.levenshteinDistance(name)) } - // do not suggest filters which names are shorter than the distance - .filter { $0.filterName.count > $0.distance } - guard let minDistance = filtersWithDistance.min(by: { $0.distance < $1.distance })?.distance else { - return [] - } - // suggest all filters with the same distance - return filtersWithDistance.filter { $0.distance == minDistance }.map { $0.filterName } - } + let filtersWithDistance = allFilters + .map { (filterName: $0, distance: $0.levenshteinDistance(name)) } + // do not suggest filters which names are shorter than the distance + .filter { $0.filterName.count > $0.distance } + guard let minDistance = filtersWithDistance.min(by: { $0.distance < $1.distance })?.distance else { + return [] + } + // suggest all filters with the same distance + return filtersWithDistance.filter { $0.distance == minDistance }.map { $0.filterName } + } - /// Create filter expression from a string - public func compileFilter(_ token: String) throws -> Resolvable { - try FilterExpression(token: token, environment: self) - } + /// Create filter expression from a string + public func compileFilter(_ token: String) throws -> Resolvable { + try FilterExpression(token: token, environment: self) + } - /// Create filter expression from a string contained in provided token - public func compileFilter(_ filterToken: String, containedIn containingToken: Token) throws -> Resolvable { - do { - return try FilterExpression(token: filterToken, environment: self) - } catch { - guard var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil else { - throw error - } - // find offset of filter in the containing token so that only filter is highligted, not the whole token - if let filterTokenRange = containingToken.contents.range(of: filterToken) { - var location = containingToken.sourceMap.location - location.lineOffset += containingToken.contents.distance( - from: containingToken.contents.startIndex, - to: filterTokenRange.lowerBound - ) - syntaxError.token = .variable( - value: filterToken, - at: SourceMap(filename: containingToken.sourceMap.filename, location: location) - ) - } else { - syntaxError.token = containingToken - } - throw syntaxError - } - } + /// Create filter expression from a string contained in provided token + public func compileFilter(_ filterToken: String, containedIn containingToken: Token) throws -> Resolvable { + do { + return try FilterExpression(token: filterToken, environment: self) + } catch { + guard var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil else { + throw error + } + // find offset of filter in the containing token so that only filter is highligted, not the whole token + if let filterTokenRange = containingToken.contents.range(of: filterToken) { + var location = containingToken.sourceMap.location + location.lineOffset += containingToken.contents.distance( + from: containingToken.contents.startIndex, + to: filterTokenRange.lowerBound + ) + syntaxError.token = .variable( + value: filterToken, + at: SourceMap(filename: containingToken.sourceMap.filename, location: location) + ) + } else { + syntaxError.token = containingToken + } + throw syntaxError + } + } - /// Create resolvable (i.e. range variable or filter expression) from a string - public func compileResolvable(_ token: String) throws -> Resolvable { - try RangeVariable(token, environment: self) - ?? compileFilter(token) - } + /// Create resolvable (i.e. range variable or filter expression) from a string + public func compileResolvable(_ token: String) throws -> Resolvable { + try RangeVariable(token, environment: self) + ?? compileFilter(token) + } - /// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token - public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable { - try RangeVariable(token, environment: self, containedIn: containingToken) - ?? compileFilter(token, containedIn: containingToken) - } + /// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token + public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable { + try RangeVariable(token, environment: self, containedIn: containingToken) + ?? compileFilter(token, containedIn: containingToken) + } - /// Create boolean expression from components contained in provided token - public func compileExpression(components: [String], containedIn token: Token) throws -> Expression { - try IfExpressionParser.parser(components: components, environment: self, token: token).parse() - } + /// Create boolean expression from components contained in provided token + public func compileExpression(components: [String], containedIn token: Token) throws -> Expression { + try IfExpressionParser.parser(components: components, environment: self, token: token).parse() + } } // https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows extension String { - subscript(_ index: Int) -> Character { - self[self.index(self.startIndex, offsetBy: index)] - } + subscript(_ index: Int) -> Character { + self[self.index(self.startIndex, offsetBy: index)] + } - func levenshteinDistance(_ target: String) -> Int { - // create two work vectors of integer distances - var last, current: [Int] + func levenshteinDistance(_ target: String) -> Int { + // create two work vectors of integer distances + var last, current: [Int] - // initialize v0 (the previous row of distances) - // this row is A[0][i]: edit distance for an empty s - // the distance is just the number of characters to delete from t - last = [Int](0...target.count) - current = [Int](repeating: 0, count: target.count + 1) + // initialize v0 (the previous row of distances) + // this row is A[0][i]: edit distance for an empty s + // the distance is just the number of characters to delete from t + last = [Int](0...target.count) + current = [Int](repeating: 0, count: target.count + 1) - for selfIndex in 0.. String { - let context = context - let parser = TokenParser(tokens: tokens, environment: context.environment) - let nodes = try parser.parse() - return try renderNodes(nodes, context) - } + /// Render the given template with a context + public func render(_ context: Context) throws -> String { + let context = context + let parser = TokenParser(tokens: tokens, environment: context.environment) + let nodes = try parser.parse() + return try renderNodes(nodes, context) + } - /// Render the given template - // swiftlint:disable:next discouraged_optional_collection - open func render(_ dictionary: [String: Any]? = nil) throws -> String { - try render(Context(dictionary: dictionary ?? [:], environment: environment)) - } + /// Render the given template + // swiftlint:disable:next discouraged_optional_collection + open func render(_ dictionary: [String: Any]? = nil) throws -> String { + try render(Context(dictionary: dictionary ?? [:], environment: environment)) + } } diff --git a/Sources/Stencil/Tokenizer.swift b/Sources/Stencil/Tokenizer.swift index ed0e63f..0ba033a 100644 --- a/Sources/Stencil/Tokenizer.swift +++ b/Sources/Stencil/Tokenizer.swift @@ -1,81 +1,81 @@ import Foundation extension String { - /// Split a string by a separator leaving quoted phrases together - func smartSplit(separator: Character = " ") -> [String] { - var word = "" - var components: [String] = [] - var separate: Character = separator - var singleQuoteCount = 0 - var doubleQuoteCount = 0 + /// Split a string by a separator leaving quoted phrases together + func smartSplit(separator: Character = " ") -> [String] { + var word = "" + var components: [String] = [] + var separate: Character = separator + var singleQuoteCount = 0 + var doubleQuoteCount = 0 - for character in self { - if character == "'" { - singleQuoteCount += 1 - } else if character == "\"" { - doubleQuoteCount += 1 - } + for character in self { + if character == "'" { + singleQuoteCount += 1 + } else if character == "\"" { + doubleQuoteCount += 1 + } - if character == separate { - if separate != separator { - word.append(separate) - } else if (singleQuoteCount.isMultiple(of: 2) || doubleQuoteCount.isMultiple(of: 2)) && !word.isEmpty { - appendWord(word, to: &components) - word = "" - } + if character == separate { + if separate != separator { + word.append(separate) + } else if (singleQuoteCount.isMultiple(of: 2) || doubleQuoteCount.isMultiple(of: 2)) && !word.isEmpty { + appendWord(word, to: &components) + word = "" + } - separate = separator - } else { - if separate == separator && (character == "'" || character == "\"") { - separate = character - } - word.append(character) - } - } + separate = separator + } else { + if separate == separator && (character == "'" || character == "\"") { + separate = character + } + word.append(character) + } + } - if !word.isEmpty { - appendWord(word, to: &components) - } + if !word.isEmpty { + appendWord(word, to: &components) + } - return components - } + return components + } - private func appendWord(_ word: String, to components: inout [String]) { - let specialCharacters = ",|:" + private func appendWord(_ word: String, to components: inout [String]) { + let specialCharacters = ",|:" - if !components.isEmpty { - if let precedingChar = components.last?.last, specialCharacters.contains(precedingChar) { - // special case for labeled for-loops - if components.count == 1 && word == "for" { - components.append(word) - } else { - components[components.count - 1] += word - } - } else if specialCharacters.contains(word) { - components[components.count - 1] += word - } else if word != "(" && word.first == "(" || word != ")" && word.first == ")" { - components.append(String(word.prefix(1))) - appendWord(String(word.dropFirst()), to: &components) - } else if word != "(" && word.last == "(" || word != ")" && word.last == ")" { - appendWord(String(word.dropLast()), to: &components) - components.append(String(word.suffix(1))) - } else { - components.append(word) - } - } else { - components.append(word) - } - } + if !components.isEmpty { + if let precedingChar = components.last?.last, specialCharacters.contains(precedingChar) { + // special case for labeled for-loops + if components.count == 1 && word == "for" { + components.append(word) + } else { + components[components.count - 1] += word + } + } else if specialCharacters.contains(word) { + components[components.count - 1] += word + } else if word != "(" && word.first == "(" || word != ")" && word.first == ")" { + components.append(String(word.prefix(1))) + appendWord(String(word.dropFirst()), to: &components) + } else if word != "(" && word.last == "(" || word != ")" && word.last == ")" { + appendWord(String(word.dropLast()), to: &components) + components.append(String(word.suffix(1))) + } else { + components.append(word) + } + } else { + components.append(word) + } + } } public struct SourceMap: Equatable { - public let filename: String? - public let location: ContentLocation + public let filename: String? + public let location: ContentLocation - init(filename: String? = nil, location: ContentLocation = ("", 0, 0)) { - self.filename = filename - self.location = location - } + init(filename: String? = nil, location: ContentLocation = ("", 0, 0)) { + self.filename = filename + self.location = location + } static let unknown = Self() @@ -85,70 +85,70 @@ public struct SourceMap: Equatable { } public struct WhitespaceBehaviour: Equatable { - public enum Behaviour { - case unspecified - case trim - case keep - } + public enum Behaviour { + case unspecified + case trim + case keep + } - let leading: Behaviour - let trailing: Behaviour + let leading: Behaviour + let trailing: Behaviour - public static let unspecified = Self(leading: .unspecified, trailing: .unspecified) + public static let unspecified = Self(leading: .unspecified, trailing: .unspecified) } public class Token: Equatable { - public enum Kind: Equatable { - /// A token representing a piece of text. - case text - /// A token representing a variable. - case variable - /// A token representing a comment. - case comment - /// A token representing a template block. - case block - } + public enum Kind: Equatable { + /// A token representing a piece of text. + case text + /// A token representing a variable. + case variable + /// A token representing a comment. + case comment + /// A token representing a template block. + case block + } - public let contents: String - public let kind: Kind - public let sourceMap: SourceMap - public var whitespace: WhitespaceBehaviour? + public let contents: String + public let kind: Kind + public let sourceMap: SourceMap + public var whitespace: WhitespaceBehaviour? - /// Returns the underlying value as an array seperated by spaces - public private(set) lazy var components: [String] = self.contents.smartSplit() + /// Returns the underlying value as an array seperated by spaces + public private(set) lazy var components: [String] = self.contents.smartSplit() - init(contents: String, kind: Kind, sourceMap: SourceMap, whitespace: WhitespaceBehaviour? = nil) { - self.contents = contents - self.kind = kind - self.sourceMap = sourceMap - self.whitespace = whitespace - } + init(contents: String, kind: Kind, sourceMap: SourceMap, whitespace: WhitespaceBehaviour? = nil) { + self.contents = contents + self.kind = kind + self.sourceMap = sourceMap + self.whitespace = whitespace + } - /// A token representing a piece of text. - public static func text(value: String, at sourceMap: SourceMap) -> Token { - Token(contents: value, kind: .text, sourceMap: sourceMap) - } + /// A token representing a piece of text. + public static func text(value: String, at sourceMap: SourceMap) -> Token { + Token(contents: value, kind: .text, sourceMap: sourceMap) + } - /// A token representing a variable. - public static func variable(value: String, at sourceMap: SourceMap) -> Token { - Token(contents: value, kind: .variable, sourceMap: sourceMap) - } + /// A token representing a variable. + public static func variable(value: String, at sourceMap: SourceMap) -> Token { + Token(contents: value, kind: .variable, sourceMap: sourceMap) + } - /// A token representing a comment. - public static func comment(value: String, at sourceMap: SourceMap) -> Token { - Token(contents: value, kind: .comment, sourceMap: sourceMap) - } + /// A token representing a comment. + public static func comment(value: String, at sourceMap: SourceMap) -> Token { + Token(contents: value, kind: .comment, sourceMap: sourceMap) + } - /// A token representing a template block. - public static func block( - value: String, - at sourceMap: SourceMap, - whitespace: WhitespaceBehaviour = .unspecified - ) -> Token { - Token(contents: value, kind: .block, sourceMap: sourceMap, whitespace: whitespace) - } + /// A token representing a template block. + public static func block( + value: String, + at sourceMap: SourceMap, + whitespace: WhitespaceBehaviour = .unspecified + ) -> Token { + Token(contents: value, kind: .block, sourceMap: sourceMap, whitespace: whitespace) + } - public static func == (lhs: Token, rhs: Token) -> Bool { - lhs.contents == rhs.contents && lhs.kind == rhs.kind && lhs.sourceMap == rhs.sourceMap - } + public static func == (lhs: Token, rhs: Token) -> Bool { + lhs.contents == rhs.contents && lhs.kind == rhs.kind && lhs.sourceMap == rhs.sourceMap + } } diff --git a/Sources/Stencil/TrimBehaviour.swift b/Sources/Stencil/TrimBehaviour.swift index 2374347..e06ed49 100644 --- a/Sources/Stencil/TrimBehaviour.swift +++ b/Sources/Stencil/TrimBehaviour.swift @@ -1,75 +1,75 @@ import Foundation public struct TrimBehaviour: Equatable { - var leading: Trim - var trailing: Trim + var leading: Trim + var trailing: Trim - public enum Trim { - /// nothing - case nothing + public enum Trim { + /// nothing + case nothing - /// tabs and spaces - case whitespace + /// tabs and spaces + case whitespace - /// tabs and spaces and a single new line - case whitespaceAndOneNewLine + /// tabs and spaces and a single new line + case whitespaceAndOneNewLine - /// all tabs spaces and newlines - case whitespaceAndNewLines - } + /// all tabs spaces and newlines + case whitespaceAndNewLines + } - public init(leading: Trim, trailing: Trim) { - self.leading = leading - self.trailing = trailing - } + public init(leading: Trim, trailing: Trim) { + self.leading = leading + self.trailing = trailing + } - /// doesn't touch newlines - public static let nothing = Self(leading: .nothing, trailing: .nothing) + /// doesn't touch newlines + public static let nothing = Self(leading: .nothing, trailing: .nothing) - /// removes whitespace before a block and whitespace and a single newline after a block - public static let smart = Self(leading: .whitespace, trailing: .whitespaceAndOneNewLine) + /// removes whitespace before a block and whitespace and a single newline after a block + public static let smart = Self(leading: .whitespace, trailing: .whitespaceAndOneNewLine) - /// removes all whitespace and newlines before and after a block - public static let all = Self(leading: .whitespaceAndNewLines, trailing: .whitespaceAndNewLines) + /// removes all whitespace and newlines before and after a block + public static let all = Self(leading: .whitespaceAndNewLines, trailing: .whitespaceAndNewLines) - static func leadingRegex(trim: Trim) -> NSRegularExpression { - switch trim { - case .nothing: - fatalError("No RegularExpression for none") - case .whitespace: - return Self.leadingWhitespace - case .whitespaceAndOneNewLine: - return Self.leadingWhitespaceAndOneNewLine - case .whitespaceAndNewLines: - return Self.leadingWhitespaceAndNewlines - } - } + static func leadingRegex(trim: Trim) -> NSRegularExpression { + switch trim { + case .nothing: + fatalError("No RegularExpression for none") + case .whitespace: + return Self.leadingWhitespace + case .whitespaceAndOneNewLine: + return Self.leadingWhitespaceAndOneNewLine + case .whitespaceAndNewLines: + return Self.leadingWhitespaceAndNewlines + } + } - static func trailingRegex(trim: Trim) -> NSRegularExpression { - switch trim { - case .nothing: - fatalError("No RegularExpression for none") - case .whitespace: - return Self.trailingWhitespace - case .whitespaceAndOneNewLine: - return Self.trailingWhitespaceAndOneNewLine - case .whitespaceAndNewLines: - return Self.trailingWhitespaceAndNewLines - } - } + static func trailingRegex(trim: Trim) -> NSRegularExpression { + switch trim { + case .nothing: + fatalError("No RegularExpression for none") + case .whitespace: + return Self.trailingWhitespace + case .whitespaceAndOneNewLine: + return Self.trailingWhitespaceAndOneNewLine + case .whitespaceAndNewLines: + return Self.trailingWhitespaceAndNewLines + } + } - // swiftlint:disable:next force_try - private static let leadingWhitespaceAndNewlines = try! NSRegularExpression(pattern: "^\\s+") - // swiftlint:disable:next force_try - private static let trailingWhitespaceAndNewLines = try! NSRegularExpression(pattern: "\\s+$") + // swiftlint:disable:next force_try + private static let leadingWhitespaceAndNewlines = try! NSRegularExpression(pattern: "^\\s+") + // swiftlint:disable:next force_try + private static let trailingWhitespaceAndNewLines = try! NSRegularExpression(pattern: "\\s+$") - // swiftlint:disable:next force_try - private static let leadingWhitespaceAndOneNewLine = try! NSRegularExpression(pattern: "^[ \t]*\n") - // swiftlint:disable:next force_try - private static let trailingWhitespaceAndOneNewLine = try! NSRegularExpression(pattern: "\n[ \t]*$") + // swiftlint:disable:next force_try + private static let leadingWhitespaceAndOneNewLine = try! NSRegularExpression(pattern: "^[ \t]*\n") + // swiftlint:disable:next force_try + private static let trailingWhitespaceAndOneNewLine = try! NSRegularExpression(pattern: "\n[ \t]*$") - // swiftlint:disable:next force_try - private static let leadingWhitespace = try! NSRegularExpression(pattern: "^[ \t]*") - // swiftlint:disable:next force_try - private static let trailingWhitespace = try! NSRegularExpression(pattern: "[ \t]*$") + // swiftlint:disable:next force_try + private static let leadingWhitespace = try! NSRegularExpression(pattern: "^[ \t]*") + // swiftlint:disable:next force_try + private static let trailingWhitespace = try! NSRegularExpression(pattern: "[ \t]*$") } diff --git a/Sources/Stencil/Variable.swift b/Sources/Stencil/Variable.swift index c9b20d5..60e66c5 100644 --- a/Sources/Stencil/Variable.swift +++ b/Sources/Stencil/Variable.swift @@ -3,151 +3,151 @@ import Foundation typealias Number = Float class FilterExpression: Resolvable { - let filters: [(FilterType, [Variable])] - let variable: Variable + let filters: [(FilterType, [Variable])] + let variable: Variable - init(token: String, environment: Environment) throws { - let bits = token.smartSplit(separator: "|").map { String($0).trim(character: " ") } - if bits.isEmpty { - throw TemplateSyntaxError("Variable tags must include at least 1 argument") - } + init(token: String, environment: Environment) throws { + let bits = token.smartSplit(separator: "|").map { String($0).trim(character: " ") } + if bits.isEmpty { + throw TemplateSyntaxError("Variable tags must include at least 1 argument") + } - variable = Variable(bits[0]) - let filterBits = bits[bits.indices.suffix(from: 1)] + variable = Variable(bits[0]) + let filterBits = bits[bits.indices.suffix(from: 1)] - do { - filters = try filterBits.map { bit in - let (name, arguments) = parseFilterComponents(token: bit) - let filter = try environment.findFilter(name) - return (filter, arguments) - } - } catch { - filters = [] - throw error - } - } + do { + filters = try filterBits.map { bit in + let (name, arguments) = parseFilterComponents(token: bit) + let filter = try environment.findFilter(name) + return (filter, arguments) + } + } catch { + filters = [] + throw error + } + } - func resolve(_ context: Context) throws -> Any? { - let result = try variable.resolve(context) + func resolve(_ context: Context) throws -> Any? { + let result = try variable.resolve(context) - return try filters.reduce(result) { value, filter in - let arguments = try filter.1.map { try $0.resolve(context) } - return try filter.0.invoke(value: value, arguments: arguments, context: context) - } - } + return try filters.reduce(result) { value, filter in + let arguments = try filter.1.map { try $0.resolve(context) } + return try filter.0.invoke(value: value, arguments: arguments, context: context) + } + } } /// A structure used to represent a template variable, and to resolve it in a given context. public struct Variable: Equatable, Resolvable { - public let variable: String + public let variable: String - /// Create a variable with a string representing the variable - public init(_ variable: String) { - self.variable = variable - } + /// Create a variable with a string representing the variable + public init(_ variable: String) { + self.variable = variable + } - /// Resolve the variable in the given context - public func resolve(_ context: Context) throws -> Any? { - if variable.count > 1 && - ((variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\""))) { - // String literal - return String(variable[variable.index(after: variable.startIndex) ..< variable.index(before: variable.endIndex)]) - } + /// Resolve the variable in the given context + public func resolve(_ context: Context) throws -> Any? { + if variable.count > 1 && + ((variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\""))) { + // String literal + return String(variable[variable.index(after: variable.startIndex) ..< variable.index(before: variable.endIndex)]) + } - // Number literal - if let int = Int(variable) { - return int - } - if let number = Number(variable) { - return number - } - // Boolean literal - if let bool = Bool(variable) { - return bool - } + // Number literal + if let int = Int(variable) { + return int + } + if let number = Number(variable) { + return number + } + // Boolean literal + if let bool = Bool(variable) { + return bool + } - var current: Any? = context - for bit in try lookup(context) { - current = resolve(bit: bit, context: current) + var current: Any? = context + for bit in try lookup(context) { + current = resolve(bit: bit, context: current) - if current == nil { - return nil - } else if let lazyCurrent = current as? LazyValueWrapper { - current = try lazyCurrent.value(context: context) - } - } + if current == nil { + return nil + } else if let lazyCurrent = current as? LazyValueWrapper { + current = try lazyCurrent.value(context: context) + } + } - if let resolvable = current as? Resolvable { - current = try resolvable.resolve(context) - } else if let node = current as? NodeType { - current = try node.render(context) - } + if let resolvable = current as? Resolvable { + current = try resolvable.resolve(context) + } else if let node = current as? NodeType { + current = try node.render(context) + } - return normalize(current) - } + return normalize(current) + } - // Split the lookup string and resolve references if possible - private func lookup(_ context: Context) throws -> [String] { - let keyPath = KeyPath(variable, in: context) - return try keyPath.parse() - } + // Split the lookup string and resolve references if possible + private func lookup(_ context: Context) throws -> [String] { + let keyPath = KeyPath(variable, in: context) + return try keyPath.parse() + } - // Try to resolve a partial keypath for the given context - private func resolve(bit: String, context: Any?) -> Any? { - let context = normalize(context) + // Try to resolve a partial keypath for the given context + private func resolve(bit: String, context: Any?) -> Any? { + let context = normalize(context) - if let context = context as? Context { - return context[bit] - } else if let dictionary = context as? [String: Any] { - return resolve(bit: bit, dictionary: dictionary) - } else if let array = context as? [Any] { - return resolve(bit: bit, collection: array) - } else if let string = context as? String { - return resolve(bit: bit, collection: string) - } else if let value = context as? DynamicMemberLookup { - return value[dynamicMember: bit] - } else if let object = context as? NSObject { // NSKeyValueCoding - #if canImport(ObjectiveC) - if object.responds(to: Selector(bit)) { - return object.value(forKey: bit) - } - #else - return nil - #endif - } else if let value = context { - return Mirror(reflecting: value).getValue(for: bit) - } + if let context = context as? Context { + return context[bit] + } else if let dictionary = context as? [String: Any] { + return resolve(bit: bit, dictionary: dictionary) + } else if let array = context as? [Any] { + return resolve(bit: bit, collection: array) + } else if let string = context as? String { + return resolve(bit: bit, collection: string) + } else if let value = context as? DynamicMemberLookup { + return value[dynamicMember: bit] + } else if let object = context as? NSObject { // NSKeyValueCoding + #if canImport(ObjectiveC) + if object.responds(to: Selector(bit)) { + return object.value(forKey: bit) + } + #else + return nil + #endif + } else if let value = context { + return Mirror(reflecting: value).getValue(for: bit) + } - return nil - } + return nil + } - // Try to resolve a partial keypath for the given dictionary - private func resolve(bit: String, dictionary: [String: Any]) -> Any? { - if bit == "count" { - return dictionary.count - } else { - return dictionary[bit] - } - } + // Try to resolve a partial keypath for the given dictionary + private func resolve(bit: String, dictionary: [String: Any]) -> Any? { + if bit == "count" { + return dictionary.count + } else { + return dictionary[bit] + } + } - // Try to resolve a partial keypath for the given collection - private func resolve(bit: String, collection: T) -> Any? { - if let index = Int(bit) { - if index >= 0 && index < collection.count { - return collection[collection.index(collection.startIndex, offsetBy: index)] - } else { - return nil - } - } else if bit == "first" { - return collection.first - } else if bit == "last" { - return collection[collection.index(collection.endIndex, offsetBy: -1)] - } else if bit == "count" { - return collection.count - } else { - return nil - } - } + // Try to resolve a partial keypath for the given collection + private func resolve(bit: String, collection: T) -> Any? { + if let index = Int(bit) { + if index >= 0 && index < collection.count { + return collection[collection.index(collection.startIndex, offsetBy: index)] + } else { + return nil + } + } else if bit == "first" { + return collection.first + } else if bit == "last" { + return collection[collection.index(collection.endIndex, offsetBy: -1)] + } else if bit == "count" { + return collection.count + } else { + return nil + } + } } /// A structure used to represet range of two integer values expressed as `from...to`. @@ -155,131 +155,131 @@ public struct Variable: Equatable, Resolvable { /// Rendering this variable produces array from range `from...to`. /// If `from` is more than `to` array will contain values of reversed range. public struct RangeVariable: Resolvable { - public let from: Resolvable - // swiftlint:disable:next identifier_name - public let to: Resolvable + public let from: Resolvable + // swiftlint:disable:next identifier_name + public let to: Resolvable - public init?(_ token: String, environment: Environment) throws { - let components = token.components(separatedBy: "...") - guard components.count == 2 else { - return nil - } + public init?(_ token: String, environment: Environment) throws { + let components = token.components(separatedBy: "...") + guard components.count == 2 else { + return nil + } - self.from = try environment.compileFilter(components[0]) - self.to = try environment.compileFilter(components[1]) - } + self.from = try environment.compileFilter(components[0]) + self.to = try environment.compileFilter(components[1]) + } - public init?(_ token: String, environment: Environment, containedIn containingToken: Token) throws { - let components = token.components(separatedBy: "...") - guard components.count == 2 else { - return nil - } + public init?(_ token: String, environment: Environment, containedIn containingToken: Token) throws { + let components = token.components(separatedBy: "...") + guard components.count == 2 else { + return nil + } - self.from = try environment.compileFilter(components[0], containedIn: containingToken) - self.to = try environment.compileFilter(components[1], containedIn: containingToken) - } + self.from = try environment.compileFilter(components[0], containedIn: containingToken) + self.to = try environment.compileFilter(components[1], containedIn: containingToken) + } - public func resolve(_ context: Context) throws -> Any? { - let lowerResolved = try from.resolve(context) - let upperResolved = try to.resolve(context) + public func resolve(_ context: Context) throws -> Any? { + let lowerResolved = try from.resolve(context) + let upperResolved = try to.resolve(context) - guard let lower = lowerResolved.flatMap(toNumber(value:)).flatMap(Int.init) else { - throw TemplateSyntaxError("'from' value is not an Integer (\(lowerResolved ?? "nil"))") - } + guard let lower = lowerResolved.flatMap(toNumber(value:)).flatMap(Int.init) else { + throw TemplateSyntaxError("'from' value is not an Integer (\(lowerResolved ?? "nil"))") + } - guard let upper = upperResolved.flatMap(toNumber(value:)).flatMap(Int.init) else { - throw TemplateSyntaxError("'to' value is not an Integer (\(upperResolved ?? "nil") )") - } + guard let upper = upperResolved.flatMap(toNumber(value:)).flatMap(Int.init) else { + throw TemplateSyntaxError("'to' value is not an Integer (\(upperResolved ?? "nil") )") + } - let range = min(lower, upper)...max(lower, upper) - return lower > upper ? Array(range.reversed()) : Array(range) - } + let range = min(lower, upper)...max(lower, upper) + return lower > upper ? Array(range.reversed()) : Array(range) + } } func normalize(_ current: Any?) -> Any? { - if let current = current as? Normalizable { - return current.normalize() - } + if let current = current as? Normalizable { + return current.normalize() + } - return current + return current } protocol Normalizable { - func normalize() -> Any? + func normalize() -> Any? } extension Array: Normalizable { - func normalize() -> Any? { - map { $0 as Any } - } + func normalize() -> Any? { + map { $0 as Any } + } } // swiftlint:disable:next legacy_objc_type extension NSArray: Normalizable { - func normalize() -> Any? { - map { $0 as Any } - } + func normalize() -> Any? { + map { $0 as Any } + } } extension Dictionary: Normalizable { - func normalize() -> Any? { - var dictionary: [String: Any] = [:] + func normalize() -> Any? { + var dictionary: [String: Any] = [:] - for (key, value) in self { - if let key = key as? String { - dictionary[key] = Stencil.normalize(value) - } else if let key = key as? CustomStringConvertible { - dictionary[key.description] = Stencil.normalize(value) - } - } + for (key, value) in self { + if let key = key as? String { + dictionary[key] = Stencil.normalize(value) + } else if let key = key as? CustomStringConvertible { + dictionary[key.description] = Stencil.normalize(value) + } + } - return dictionary - } + return dictionary + } } func parseFilterComponents(token: String) -> (String, [Variable]) { - var components = token.smartSplit(separator: ":") - let name = components.removeFirst().trim(character: " ") - let variables = components - .joined(separator: ":") - .smartSplit(separator: ",") - .map { Variable($0.trim(character: " ")) } - return (name, variables) + var components = token.smartSplit(separator: ":") + let name = components.removeFirst().trim(character: " ") + let variables = components + .joined(separator: ":") + .smartSplit(separator: ",") + .map { Variable($0.trim(character: " ")) } + return (name, variables) } extension Mirror { - func getValue(for key: String) -> Any? { - let result = descendant(key) ?? Int(key).flatMap { descendant($0) } - if result == nil { - // go through inheritance chain to reach superclass properties - return superclassMirror?.getValue(for: key) - } else if let result = result { - guard String(describing: result) != "nil" else { - // mirror returns non-nil value even for nil-containing properties - // so we have to check if its value is actually nil or not - return nil - } - if let result = (result as? AnyOptional)?.wrapped { - return result - } else { - return result - } - } - return result - } + func getValue(for key: String) -> Any? { + let result = descendant(key) ?? Int(key).flatMap { descendant($0) } + if result == nil { + // go through inheritance chain to reach superclass properties + return superclassMirror?.getValue(for: key) + } else if let result = result { + guard String(describing: result) != "nil" else { + // mirror returns non-nil value even for nil-containing properties + // so we have to check if its value is actually nil or not + return nil + } + if let result = (result as? AnyOptional)?.wrapped { + return result + } else { + return result + } + } + return result + } } protocol AnyOptional { - var wrapped: Any? { get } + var wrapped: Any? { get } } extension Optional: AnyOptional { - var wrapped: Any? { - switch self { - case let .some(value): - return value - case .none: - return nil - } - } + var wrapped: Any? { + switch self { + case let .some(value): + return value + case .none: + return nil + } + } } diff --git a/Tests/StencilTests/ContextSpec.swift b/Tests/StencilTests/ContextSpec.swift index 80b1640..a3bacbb 100644 --- a/Tests/StencilTests/ContextSpec.swift +++ b/Tests/StencilTests/ContextSpec.swift @@ -3,158 +3,158 @@ import Spectre import XCTest final class ContextTests: XCTestCase { - func testContextSubscripting() { - describe("Context Subscripting") { test in - var context = Context() - test.before { - context = Context(dictionary: ["name": "Kyle"]) - } + func testContextSubscripting() { + describe("Context Subscripting") { test in + var context = Context() + test.before { + context = Context(dictionary: ["name": "Kyle"]) + } - test.it("allows you to get a value via subscripting") { - try expect(context["name"] as? String) == "Kyle" - } + test.it("allows you to get a value via subscripting") { + try expect(context["name"] as? String) == "Kyle" + } - test.it("allows you to set a value via subscripting") { - context["name"] = "Katie" + test.it("allows you to set a value via subscripting") { + context["name"] = "Katie" - try expect(context["name"] as? String) == "Katie" - } + try expect(context["name"] as? String) == "Katie" + } - test.it("allows you to remove a value via subscripting") { - context["name"] = nil + test.it("allows you to remove a value via subscripting") { + context["name"] = nil - try expect(context["name"]).to.beNil() - } + try expect(context["name"]).to.beNil() + } - test.it("allows you to retrieve a value from a parent") { - try context.push { - try expect(context["name"] as? String) == "Kyle" - } - } + test.it("allows you to retrieve a value from a parent") { + try context.push { + try expect(context["name"] as? String) == "Kyle" + } + } - test.it("allows you to override a parent's value") { - try context.push { - context["name"] = "Katie" - try expect(context["name"] as? String) == "Katie" - } - } - } - } + test.it("allows you to override a parent's value") { + try context.push { + context["name"] = "Katie" + try expect(context["name"] as? String) == "Katie" + } + } + } + } - func testContextRestoration() { - describe("Context Restoration") { test in - var context = Context() - test.before { - context = Context(dictionary: ["name": "Kyle"]) - } + func testContextRestoration() { + describe("Context Restoration") { test in + var context = Context() + test.before { + context = Context(dictionary: ["name": "Kyle"]) + } - test.it("allows you to pop to restore previous state") { - context.push { - context["name"] = "Katie" - } + test.it("allows you to pop to restore previous state") { + context.push { + context["name"] = "Katie" + } - try expect(context["name"] as? String) == "Kyle" - } + try expect(context["name"] as? String) == "Kyle" + } - test.it("allows you to remove a parent's value in a level") { - try context.push { - context["name"] = nil - try expect(context["name"]).to.beNil() - } + test.it("allows you to remove a parent's value in a level") { + try context.push { + context["name"] = nil + try expect(context["name"]).to.beNil() + } - try expect(context["name"] as? String) == "Kyle" - } + try expect(context["name"] as? String) == "Kyle" + } - test.it("allows you to push a dictionary and run a closure then restoring previous state") { - var didRun = false + test.it("allows you to push a dictionary and run a closure then restoring previous state") { + var didRun = false - try context.push(dictionary: ["name": "Katie"]) { - didRun = true - try expect(context["name"] as? String) == "Katie" - } + try context.push(dictionary: ["name": "Katie"]) { + didRun = true + try expect(context["name"] as? String) == "Katie" + } - try expect(didRun).to.beTrue() - try expect(context["name"] as? String) == "Kyle" - } + try expect(didRun).to.beTrue() + try expect(context["name"] as? String) == "Kyle" + } - test.it("allows you to flatten the context contents") { - try context.push(dictionary: ["test": "abc"]) { - let flattened = context.flatten() + test.it("allows you to flatten the context contents") { + try context.push(dictionary: ["test": "abc"]) { + let flattened = context.flatten() - try expect(flattened.count) == 2 - try expect(flattened["name"] as? String) == "Kyle" - try expect(flattened["test"] as? String) == "abc" - } - } - } - } + try expect(flattened.count) == 2 + try expect(flattened["name"] as? String) == "Kyle" + try expect(flattened["test"] as? String) == "abc" + } + } + } + } - func testContextLazyEvaluation() { - let ticker = Ticker() - var context = Context() - var wrapper = LazyValueWrapper("") + func testContextLazyEvaluation() { + let ticker = Ticker() + var context = Context() + var wrapper = LazyValueWrapper("") - describe("Lazy evaluation") { test in - test.before { - ticker.count = 0 - wrapper = LazyValueWrapper(ticker.tick()) - context = Context(dictionary: ["name": wrapper]) - } + describe("Lazy evaluation") { test in + test.before { + ticker.count = 0 + wrapper = LazyValueWrapper(ticker.tick()) + context = Context(dictionary: ["name": wrapper]) + } - test.it("Evaluates lazy data") { - let template = Template(templateString: "{{ name }}") - let result = try template.render(context) - try expect(result) == "Kyle" - try expect(ticker.count) == 1 - } + test.it("Evaluates lazy data") { + let template = Template(templateString: "{{ name }}") + let result = try template.render(context) + try expect(result) == "Kyle" + try expect(ticker.count) == 1 + } - test.it("Evaluates lazy only once") { - let template = Template(templateString: "{{ name }}{{ name }}") - let result = try template.render(context) - try expect(result) == "KyleKyle" - try expect(ticker.count) == 1 - } + test.it("Evaluates lazy only once") { + let template = Template(templateString: "{{ name }}{{ name }}") + let result = try template.render(context) + try expect(result) == "KyleKyle" + try expect(ticker.count) == 1 + } - test.it("Does not evaluate lazy data when not used") { - let template = Template(templateString: "{{ 'Katie' }}") - let result = try template.render(context) - try expect(result) == "Katie" - try expect(ticker.count) == 0 - } - } - } + test.it("Does not evaluate lazy data when not used") { + let template = Template(templateString: "{{ 'Katie' }}") + let result = try template.render(context) + try expect(result) == "Katie" + try expect(ticker.count) == 0 + } + } + } - func testContextLazyAccessTypes() { - it("Supports evaluation via context reference") { - let context = Context(dictionary: ["name": "Kyle"]) - context["alias"] = LazyValueWrapper { $0["name"] ?? "" } - let template = Template(templateString: "{{ alias }}") + func testContextLazyAccessTypes() { + it("Supports evaluation via context reference") { + let context = Context(dictionary: ["name": "Kyle"]) + context["alias"] = LazyValueWrapper { $0["name"] ?? "" } + let template = Template(templateString: "{{ alias }}") - try context.push(dictionary: ["name": "Katie"]) { - let result = try template.render(context) - try expect(result) == "Katie" - } - } + try context.push(dictionary: ["name": "Katie"]) { + let result = try template.render(context) + try expect(result) == "Katie" + } + } - it("Supports evaluation via context copy") { - let context = Context(dictionary: ["name": "Kyle"]) - context["alias"] = LazyValueWrapper(copying: context) { $0["name"] ?? "" } - let template = Template(templateString: "{{ alias }}") + it("Supports evaluation via context copy") { + let context = Context(dictionary: ["name": "Kyle"]) + context["alias"] = LazyValueWrapper(copying: context) { $0["name"] ?? "" } + let template = Template(templateString: "{{ alias }}") - try context.push(dictionary: ["name": "Katie"]) { - let result = try template.render(context) - try expect(result) == "Kyle" - } - } - } + try context.push(dictionary: ["name": "Katie"]) { + let result = try template.render(context) + try expect(result) == "Kyle" + } + } + } } // MARK: - Helpers private final class Ticker { - var count: Int = 0 - func tick() -> String { - count += 1 - return "Kyle" - } + var count: Int = 0 + func tick() -> String { + count += 1 + return "Kyle" + } } diff --git a/Tests/StencilTests/EnvironmentBaseAndChildTemplateSpec.swift b/Tests/StencilTests/EnvironmentBaseAndChildTemplateSpec.swift index a4ca6e1..0681277 100644 --- a/Tests/StencilTests/EnvironmentBaseAndChildTemplateSpec.swift +++ b/Tests/StencilTests/EnvironmentBaseAndChildTemplateSpec.swift @@ -4,122 +4,122 @@ import Spectre import XCTest final class EnvironmentBaseAndChildTemplateTests: XCTestCase { - private var environment = Environment(loader: ExampleLoader()) - private var childTemplate: Template = "" - private var baseTemplate: Template = "" + private var environment = Environment(loader: ExampleLoader()) + private var childTemplate: Template = "" + private var baseTemplate: Template = "" - override func setUp() { - super.setUp() + override func setUp() { + super.setUp() - let path = Path(#file as String)! / ".." / "fixtures" - let loader = FileSystemLoader(paths: [path]) - environment = Environment(loader: loader) - childTemplate = "" - baseTemplate = "" - } + let path = Path(#file as String)! / ".." / "fixtures" + let loader = FileSystemLoader(paths: [path]) + environment = Environment(loader: loader) + childTemplate = "" + baseTemplate = "" + } - override func tearDown() { - super.tearDown() - } + override func tearDown() { + super.tearDown() + } - func testSyntaxErrorInBaseTemplate() throws { - childTemplate = try environment.loadTemplate(name: "invalid-child-super.html") - baseTemplate = try environment.loadTemplate(name: "invalid-base.html") + func testSyntaxErrorInBaseTemplate() throws { + childTemplate = try environment.loadTemplate(name: "invalid-child-super.html") + baseTemplate = try environment.loadTemplate(name: "invalid-base.html") - try expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", - childToken: "extends \"invalid-base.html\"", - baseToken: "target|unknown" - ) - } + try expectError( + reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + childToken: "extends \"invalid-base.html\"", + baseToken: "target|unknown" + ) + } - func testRuntimeErrorInBaseTemplate() throws { - let filterExtension = Extension() - filterExtension.registerFilter("unknown") { (_: Any?) in - throw TemplateSyntaxError("filter error") - } - environment.extensions += [filterExtension] + func testRuntimeErrorInBaseTemplate() throws { + let filterExtension = Extension() + filterExtension.registerFilter("unknown") { (_: Any?) in + throw TemplateSyntaxError("filter error") + } + environment.extensions += [filterExtension] - childTemplate = try environment.loadTemplate(name: "invalid-child-super.html") - baseTemplate = try environment.loadTemplate(name: "invalid-base.html") + childTemplate = try environment.loadTemplate(name: "invalid-child-super.html") + baseTemplate = try environment.loadTemplate(name: "invalid-base.html") - try expectError( - reason: "filter error", - childToken: "extends \"invalid-base.html\"", - baseToken: "target|unknown" - ) - } + try expectError( + reason: "filter error", + childToken: "extends \"invalid-base.html\"", + baseToken: "target|unknown" + ) + } - func testSyntaxErrorInChildTemplate() throws { - childTemplate = Template( - templateString: """ - {% extends "base.html" %} - {% block body %}Child {{ target|unknown }}{% endblock %} - """, - environment: environment, - name: nil - ) + func testSyntaxErrorInChildTemplate() throws { + childTemplate = Template( + templateString: """ + {% extends "base.html" %} + {% block body %}Child {{ target|unknown }}{% endblock %} + """, + environment: environment, + name: nil + ) - try expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", - childToken: "target|unknown", - baseToken: nil - ) - } + try expectError( + reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + childToken: "target|unknown", + baseToken: nil + ) + } - func testRuntimeErrorInChildTemplate() throws { - let filterExtension = Extension() - filterExtension.registerFilter("unknown") { (_: Any?) in - throw TemplateSyntaxError("filter error") - } - environment.extensions += [filterExtension] + func testRuntimeErrorInChildTemplate() throws { + let filterExtension = Extension() + filterExtension.registerFilter("unknown") { (_: Any?) in + throw TemplateSyntaxError("filter error") + } + environment.extensions += [filterExtension] - childTemplate = Template( - templateString: """ - {% extends "base.html" %} - {% block body %}Child {{ target|unknown }}{% endblock %} - """, - environment: environment, - name: nil - ) + childTemplate = Template( + templateString: """ + {% extends "base.html" %} + {% block body %}Child {{ target|unknown }}{% endblock %} + """, + environment: environment, + name: nil + ) - try expectError( - reason: "filter error", - childToken: "target|unknown", - baseToken: nil - ) - } + try expectError( + reason: "filter error", + childToken: "target|unknown", + baseToken: nil + ) + } - private func expectError( - reason: String, - childToken: String, - baseToken: String?, - file: String = #file, - line: Int = #line, - function: String = #function - ) throws { - var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason) - if let baseToken = baseToken { - expectedError.stackTrace = [ - expectedSyntaxError( - token: baseToken, - template: baseTemplate, - description: reason - ).token - ].compactMap { $0 } - } - let error = try expect( - self.environment.render(template: self.childTemplate, context: ["target": "World"]), - file: file, - line: line, - function: function - ).toThrow() as TemplateSyntaxError - let reporter = SimpleErrorReporter() - try expect( - reporter.renderError(error), - file: file, - line: line, - function: function - ) == reporter.renderError(expectedError) - } + private func expectError( + reason: String, + childToken: String, + baseToken: String?, + file: String = #file, + line: Int = #line, + function: String = #function + ) throws { + var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason) + if let baseToken = baseToken { + expectedError.stackTrace = [ + expectedSyntaxError( + token: baseToken, + template: baseTemplate, + description: reason + ).token + ].compactMap { $0 } + } + let error = try expect( + self.environment.render(template: self.childTemplate, context: ["target": "World"]), + file: file, + line: line, + function: function + ).toThrow() as TemplateSyntaxError + let reporter = SimpleErrorReporter() + try expect( + reporter.renderError(error), + file: file, + line: line, + function: function + ) == reporter.renderError(expectedError) + } } diff --git a/Tests/StencilTests/EnvironmentIncludeTemplateSpec.swift b/Tests/StencilTests/EnvironmentIncludeTemplateSpec.swift index 839b057..1dbf92b 100644 --- a/Tests/StencilTests/EnvironmentIncludeTemplateSpec.swift +++ b/Tests/StencilTests/EnvironmentIncludeTemplateSpec.swift @@ -4,85 +4,85 @@ import Spectre import XCTest final class EnvironmentIncludeTemplateTests: XCTestCase { - private var environment = Environment(loader: ExampleLoader()) - private var template: Template = "" - private var includedTemplate: Template = "" + private var environment = Environment(loader: ExampleLoader()) + private var template: Template = "" + private var includedTemplate: Template = "" - override func setUp() { - super.setUp() + override func setUp() { + super.setUp() - let path = Path(#file as String)! / ".." / "fixtures" - let loader = FileSystemLoader(paths: [path]) - environment = Environment(loader: loader) - template = "" - includedTemplate = "" - } + let path = Path(#file as String)! / ".." / "fixtures" + let loader = FileSystemLoader(paths: [path]) + environment = Environment(loader: loader) + template = "" + includedTemplate = "" + } - override func tearDown() { - super.tearDown() - } + override func tearDown() { + super.tearDown() + } - func testSyntaxError() throws { - template = Template(templateString: """ - {% include "invalid-include.html" %} - """, environment: environment) - includedTemplate = try environment.loadTemplate(name: "invalid-include.html") + func testSyntaxError() throws { + template = Template(templateString: """ + {% include "invalid-include.html" %} + """, environment: environment) + includedTemplate = try environment.loadTemplate(name: "invalid-include.html") - try expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", - token: #"include "invalid-include.html""#, - includedToken: "target|unknown" - ) - } + try expectError( + reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + token: #"include "invalid-include.html""#, + includedToken: "target|unknown" + ) + } - func testRuntimeError() throws { - let filterExtension = Extension() - filterExtension.registerFilter("unknown") { (_: Any?) in - throw TemplateSyntaxError("filter error") - } - environment.extensions += [filterExtension] + func testRuntimeError() throws { + let filterExtension = Extension() + filterExtension.registerFilter("unknown") { (_: Any?) in + throw TemplateSyntaxError("filter error") + } + environment.extensions += [filterExtension] - template = Template(templateString: """ - {% include "invalid-include.html" %} - """, environment: environment) - includedTemplate = try environment.loadTemplate(name: "invalid-include.html") + template = Template(templateString: """ + {% include "invalid-include.html" %} + """, environment: environment) + includedTemplate = try environment.loadTemplate(name: "invalid-include.html") - try expectError( - reason: "filter error", - token: "include \"invalid-include.html\"", - includedToken: "target|unknown" - ) - } + try expectError( + reason: "filter error", + token: "include \"invalid-include.html\"", + includedToken: "target|unknown" + ) + } - private func expectError( - reason: String, - token: String, - includedToken: String, - file: String = #file, - line: Int = #line, - function: String = #function - ) throws { - var expectedError = expectedSyntaxError(token: token, template: template, description: reason) - expectedError.stackTrace = [ - expectedSyntaxError( - token: includedToken, - template: includedTemplate, - description: reason - ).token - ].compactMap { $0 } + private func expectError( + reason: String, + token: String, + includedToken: String, + file: String = #file, + line: Int = #line, + function: String = #function + ) throws { + var expectedError = expectedSyntaxError(token: token, template: template, description: reason) + expectedError.stackTrace = [ + expectedSyntaxError( + token: includedToken, + template: includedTemplate, + description: reason + ).token + ].compactMap { $0 } - let error = try expect( - self.environment.render(template: self.template, context: ["target": "World"]), - file: file, - line: line, - function: function - ).toThrow() as TemplateSyntaxError - let reporter = SimpleErrorReporter() - try expect( - reporter.renderError(error), - file: file, - line: line, - function: function - ) == reporter.renderError(expectedError) - } + let error = try expect( + self.environment.render(template: self.template, context: ["target": "World"]), + file: file, + line: line, + function: function + ).toThrow() as TemplateSyntaxError + let reporter = SimpleErrorReporter() + try expect( + reporter.renderError(error), + file: file, + line: line, + function: function + ) == reporter.renderError(expectedError) + } } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index e41d58c..efe471b 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -3,218 +3,218 @@ import Spectre import XCTest final class EnvironmentTests: XCTestCase { - private var environment = Environment(loader: ExampleLoader()) - private var template: Template = "" + private var environment = Environment(loader: ExampleLoader()) + private var template: Template = "" - override func setUp() { - super.setUp() + override func setUp() { + super.setUp() - let errorExtension = Extension() - errorExtension.registerFilter("throw") { (_: Any?) in - throw TemplateSyntaxError("filter error") - } - errorExtension.registerSimpleTag("simpletag") { _ in - throw TemplateSyntaxError("simpletag error") - } - errorExtension.registerTag("customtag") { _, token in - ErrorNode(token: token) - } + let errorExtension = Extension() + errorExtension.registerFilter("throw") { (_: Any?) in + throw TemplateSyntaxError("filter error") + } + errorExtension.registerSimpleTag("simpletag") { _ in + throw TemplateSyntaxError("simpletag error") + } + errorExtension.registerTag("customtag") { _, token in + ErrorNode(token: token) + } - environment = Environment(loader: ExampleLoader()) - environment.extensions += [errorExtension] - template = "" - } + environment = Environment(loader: ExampleLoader()) + environment.extensions += [errorExtension] + template = "" + } - override func tearDown() { - super.tearDown() - } + override func tearDown() { + super.tearDown() + } - func testLoading() { - it("can load a template from a name") { - let template = try self.environment.loadTemplate(name: "example.html") - try expect(template.name) == "example.html" - } + func testLoading() { + it("can load a template from a name") { + let template = try self.environment.loadTemplate(name: "example.html") + try expect(template.name) == "example.html" + } - it("can load a template from a names") { - let template = try self.environment.loadTemplate(names: ["first.html", "example.html"]) - try expect(template.name) == "example.html" - } - } + it("can load a template from a names") { + let template = try self.environment.loadTemplate(names: ["first.html", "example.html"]) + try expect(template.name) == "example.html" + } + } - func testRendering() { - it("can render a template from a string") { - let result = try self.environment.renderTemplate(string: "Hello World") - try expect(result) == "Hello World" - } + func testRendering() { + it("can render a template from a string") { + let result = try self.environment.renderTemplate(string: "Hello World") + try expect(result) == "Hello World" + } - it("can render a template from a file") { - let result = try self.environment.renderTemplate(name: "example.html") - try expect(result) == "Hello World!" - } + it("can render a template from a file") { + let result = try self.environment.renderTemplate(name: "example.html") + try expect(result) == "Hello World!" + } - it("allows you to provide a custom template class") { - let environment = Environment(loader: ExampleLoader(), templateClass: CustomTemplate.self) - let result = try environment.renderTemplate(string: "Hello World") + it("allows you to provide a custom template class") { + let environment = Environment(loader: ExampleLoader(), templateClass: CustomTemplate.self) + let result = try environment.renderTemplate(string: "Hello World") - try expect(result) == "here" - } - } + try expect(result) == "here" + } + } - func testSyntaxError() { - it("reports syntax error on invalid for tag syntax") { - self.template = "Hello {% for name in %}{{ name }}, {% endfor %}!" - try self.expectError( - reason: "'for' statements should use the syntax: `for in [where ]`.", - token: "for name in" - ) - } + func testSyntaxError() { + it("reports syntax error on invalid for tag syntax") { + self.template = "Hello {% for name in %}{{ name }}, {% endfor %}!" + try self.expectError( + reason: "'for' statements should use the syntax: `for in [where ]`.", + token: "for name in" + ) + } - it("reports syntax error on missing endfor") { - self.template = "{% for name in names %}{{ name }}" - try self.expectError(reason: "`endfor` was not found.", token: "for name in names") - } + it("reports syntax error on missing endfor") { + self.template = "{% for name in names %}{{ name }}" + try self.expectError(reason: "`endfor` was not found.", token: "for name in names") + } - it("reports syntax error on unknown tag") { - self.template = "{% for name in names %}{{ name }}{% end %}" - try self.expectError(reason: "Unknown template tag 'end'", token: "end") - } - } + it("reports syntax error on unknown tag") { + self.template = "{% for name in names %}{{ name }}{% end %}" + try self.expectError(reason: "Unknown template tag 'end'", token: "end") + } + } - func testUnknownFilter() { - it("reports syntax error in for tag") { - self.template = "{% for name in names|unknown %}{{ name }}{% endfor %}" - try self.expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", - token: "names|unknown" - ) - } + func testUnknownFilter() { + it("reports syntax error in for tag") { + self.template = "{% for name in names|unknown %}{{ name }}{% endfor %}" + try self.expectError( + reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + token: "names|unknown" + ) + } - it("reports syntax error in for-where tag") { - self.template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}" - try self.expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", - token: "name|unknown" - ) - } + it("reports syntax error in for-where tag") { + self.template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}" + try self.expectError( + reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + token: "name|unknown" + ) + } - it("reports syntax error in if tag") { - self.template = "{% if name|unknown %}{{ name }}{% endif %}" - try self.expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", - token: "name|unknown" - ) - } + it("reports syntax error in if tag") { + self.template = "{% if name|unknown %}{{ name }}{% endif %}" + try self.expectError( + reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + token: "name|unknown" + ) + } - it("reports syntax error in elif tag") { - self.template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}" - try self.expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", - token: "name|unknown" - ) - } + it("reports syntax error in elif tag") { + self.template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}" + try self.expectError( + reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + token: "name|unknown" + ) + } - it("reports syntax error in ifnot tag") { - self.template = "{% ifnot name|unknown %}{{ name }}{% endif %}" - try self.expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", - token: "name|unknown" - ) - } + it("reports syntax error in ifnot tag") { + self.template = "{% ifnot name|unknown %}{{ name }}{% endif %}" + try self.expectError( + reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + token: "name|unknown" + ) + } - it("reports syntax error in filter tag") { - self.template = "{% filter unknown %}Text{% endfilter %}" - try self.expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", - token: "filter unknown" - ) - } + it("reports syntax error in filter tag") { + self.template = "{% filter unknown %}Text{% endfilter %}" + try self.expectError( + reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + token: "filter unknown" + ) + } - it("reports syntax error in variable tag") { - self.template = "{{ name|unknown }}" - try self.expectError( - reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", - token: "name|unknown" - ) - } + it("reports syntax error in variable tag") { + self.template = "{{ name|unknown }}" + try self.expectError( + reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + token: "name|unknown" + ) + } - it("reports error in variable tag") { - self.template = "{{ }}" - try self.expectError(reason: "Missing variable name", token: " ") - } - } + it("reports error in variable tag") { + self.template = "{{ }}" + try self.expectError(reason: "Missing variable name", token: " ") + } + } - func testRenderingError() { - it("reports rendering error in variable filter") { - self.template = Template(templateString: "{{ name|throw }}", environment: self.environment) - try self.expectError(reason: "filter error", token: "name|throw") - } + func testRenderingError() { + it("reports rendering error in variable filter") { + self.template = Template(templateString: "{{ name|throw }}", environment: self.environment) + try self.expectError(reason: "filter error", token: "name|throw") + } - it("reports rendering error in filter tag") { - self.template = Template(templateString: "{% filter throw %}Test{% endfilter %}", environment: self.environment) - try self.expectError(reason: "filter error", token: "filter throw") - } + it("reports rendering error in filter tag") { + self.template = Template(templateString: "{% filter throw %}Test{% endfilter %}", environment: self.environment) + try self.expectError(reason: "filter error", token: "filter throw") + } - it("reports rendering error in simple tag") { - self.template = Template(templateString: "{% simpletag %}", environment: self.environment) - try self.expectError(reason: "simpletag error", token: "simpletag") - } + it("reports rendering error in simple tag") { + self.template = Template(templateString: "{% simpletag %}", environment: self.environment) + try self.expectError(reason: "simpletag error", token: "simpletag") + } - it("reports passing argument to simple filter") { - self.template = "{{ name|uppercase:5 }}" - try self.expectError(reason: "Can't invoke filter with an argument", token: "name|uppercase:5") - } + it("reports passing argument to simple filter") { + self.template = "{{ name|uppercase:5 }}" + try self.expectError(reason: "Can't invoke filter with an argument", token: "name|uppercase:5") + } - it("reports rendering error in custom tag") { - self.template = Template(templateString: "{% customtag %}", environment: self.environment) - try self.expectError(reason: "Custom Error", token: "customtag") - } + it("reports rendering error in custom tag") { + self.template = Template(templateString: "{% customtag %}", environment: self.environment) + try self.expectError(reason: "Custom Error", token: "customtag") + } - it("reports rendering error in for body") { - self.template = Template(templateString: """ - {% for name in names %}{% customtag %}{% endfor %} - """, environment: self.environment) - try self.expectError(reason: "Custom Error", token: "customtag") - } + it("reports rendering error in for body") { + self.template = Template(templateString: """ + {% for name in names %}{% customtag %}{% endfor %} + """, environment: self.environment) + try self.expectError(reason: "Custom Error", token: "customtag") + } - it("reports rendering error in block") { - self.template = Template( - templateString: "{% block some %}{% customtag %}{% endblock %}", - environment: self.environment - ) - try self.expectError(reason: "Custom Error", token: "customtag") - } - } + it("reports rendering error in block") { + self.template = Template( + templateString: "{% block some %}{% customtag %}{% endblock %}", + environment: self.environment + ) + try self.expectError(reason: "Custom Error", token: "customtag") + } + } - private func expectError( - reason: String, - token: String, - file: String = #file, - line: Int = #line, - function: String = #function - ) throws { - let expectedError = expectedSyntaxError(token: token, template: template, description: reason) + private func expectError( + reason: String, + token: String, + file: String = #file, + line: Int = #line, + function: String = #function + ) throws { + let expectedError = expectedSyntaxError(token: token, template: template, description: reason) - let error = try expect( - self.environment.render(template: self.template, context: ["names": ["Bob", "Alice"], "name": "Bob"]), - file: file, - line: line, - function: function - ).toThrow() as TemplateSyntaxError - let reporter = SimpleErrorReporter() - try expect( - reporter.renderError(error), - file: file, - line: line, - function: function - ) == reporter.renderError(expectedError) - } + let error = try expect( + self.environment.render(template: self.template, context: ["names": ["Bob", "Alice"], "name": "Bob"]), + file: file, + line: line, + function: function + ).toThrow() as TemplateSyntaxError + let reporter = SimpleErrorReporter() + try expect( + reporter.renderError(error), + file: file, + line: line, + function: function + ) == reporter.renderError(expectedError) + } } // MARK: - Helpers private class CustomTemplate: Template { - // swiftlint:disable discouraged_optional_collection - override func render(_ dictionary: [String: Any]? = nil) throws -> String { - "here" - } + // swiftlint:disable discouraged_optional_collection + override func render(_ dictionary: [String: Any]? = nil) throws -> String { + "here" + } } diff --git a/Tests/StencilTests/ExpressionSpec.swift b/Tests/StencilTests/ExpressionSpec.swift index 866d928..750e4c2 100644 --- a/Tests/StencilTests/ExpressionSpec.swift +++ b/Tests/StencilTests/ExpressionSpec.swift @@ -3,353 +3,353 @@ import Spectre import XCTest final class ExpressionsTests: XCTestCase { - private let parser = TokenParser(tokens: [], environment: Environment()) - - private func makeExpression(_ components: [String]) -> Stencil.Expression { - do { - let parser = try IfExpressionParser.parser( - components: components, - environment: Environment(), - token: .text(value: "", at: .unknown) - ) - return try parser.parse() - } catch { - fatalError(error.localizedDescription) - } - } - - func testTrueExpressions() { - let expression = VariableExpression(variable: Variable("value")) - - it("evaluates to true when value is not nil") { - let context = Context(dictionary: ["value": "known"]) - try expect(try expression.evaluate(context: context)).to.beTrue() - } - - it("evaluates to true when array variable is not empty") { - let items: [[String: Any]] = [["key": "key1", "value": 42], ["key": "key2", "value": 1_337]] - let context = Context(dictionary: ["value": [items]]) - try expect(try expression.evaluate(context: context)).to.beTrue() - } - - it("evaluates to false when dictionary value is empty") { - let emptyItems = [String: Any]() - let context = Context(dictionary: ["value": emptyItems]) - try expect(try expression.evaluate(context: context)).to.beFalse() - } - - it("evaluates to true when integer value is above 0") { - let context = Context(dictionary: ["value": 1]) - try expect(try expression.evaluate(context: context)).to.beTrue() - } - - it("evaluates to true with string") { - let context = Context(dictionary: ["value": "test"]) - try expect(try expression.evaluate(context: context)).to.beTrue() - } - - it("evaluates to true when float value is above 0") { - let context = Context(dictionary: ["value": Float(0.5)]) - try expect(try expression.evaluate(context: context)).to.beTrue() - } - - it("evaluates to true when double value is above 0") { - let context = Context(dictionary: ["value": Double(0.5)]) - try expect(try expression.evaluate(context: context)).to.beTrue() - } - } - - func testFalseExpressions() { - let expression = VariableExpression(variable: Variable("value")) - - it("evaluates to false when value is unset") { - let context = Context() - try expect(try expression.evaluate(context: context)).to.beFalse() - } - - it("evaluates to false when array value is empty") { - let emptyItems = [[String: Any]]() - let context = Context(dictionary: ["value": emptyItems]) - try expect(try expression.evaluate(context: context)).to.beFalse() - } - - it("evaluates to false when dictionary value is empty") { - let emptyItems = [String: Any]() - let context = Context(dictionary: ["value": emptyItems]) - try expect(try expression.evaluate(context: context)).to.beFalse() - } - - it("evaluates to false when Array value is empty") { - let context = Context(dictionary: ["value": ([] as [Any])]) - try expect(try expression.evaluate(context: context)).to.beFalse() - } - - it("evaluates to false when empty string") { - let context = Context(dictionary: ["value": ""]) - try expect(try expression.evaluate(context: context)).to.beFalse() - } - - it("evaluates to false when integer value is below 0 or below") { - let context = Context(dictionary: ["value": 0]) - try expect(try expression.evaluate(context: context)).to.beFalse() - - let negativeContext = Context(dictionary: ["value": -1]) - try expect(try expression.evaluate(context: negativeContext)).to.beFalse() - } - - it("evaluates to false when float is 0 or below") { - let context = Context(dictionary: ["value": Float(0)]) - try expect(try expression.evaluate(context: context)).to.beFalse() - } - - it("evaluates to false when double is 0 or below") { - let context = Context(dictionary: ["value": Double(0)]) - try expect(try expression.evaluate(context: context)).to.beFalse() - } - - it("evaluates to false when uint is 0") { - let context = Context(dictionary: ["value": UInt(0)]) - try expect(try expression.evaluate(context: context)).to.beFalse() - } - } - - func testNotExpression() { - it("returns truthy for positive expressions") { - let expression = NotExpression(expression: VariableExpression(variable: Variable("true"))) - try expect(expression.evaluate(context: Context())).to.beFalse() - } - - it("returns falsy for negative expressions") { - let expression = NotExpression(expression: VariableExpression(variable: Variable("false"))) - try expect(expression.evaluate(context: Context())).to.beTrue() - } - } - - func testExpressionParsing() { - it("can parse a variable expression") { - let expression = self.makeExpression(["value"]) - try expect(expression.evaluate(context: Context())).to.beFalse() - try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue() - } - - it("can parse a not expression") { - let expression = self.makeExpression(["not", "value"]) - try expect(expression.evaluate(context: Context())).to.beTrue() - try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse() - } - } - - func testAndExpression() { - let expression = makeExpression(["lhs", "and", "rhs"]) - - it("evaluates to false with lhs false") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse() - } - - it("evaluates to false with rhs false") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse() - } - - it("evaluates to false with lhs and rhs false") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse() - } - - it("evaluates to true with lhs and rhs true") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() - } - } - - func testOrExpression() { - let expression = makeExpression(["lhs", "or", "rhs"]) - - it("evaluates to true with lhs true") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue() - } - - it("evaluates to true with rhs true") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beTrue() - } - - it("evaluates to true with lhs and rhs true") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() - } - - it("evaluates to false with lhs and rhs false") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse() - } - } - - func testEqualityExpression() { - let expression = makeExpression(["lhs", "==", "rhs"]) - - it("evaluates to true with equal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue() - } - - it("evaluates to false with non equal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beFalse() - } - - it("evaluates to true with nils") { - try expect(expression.evaluate(context: Context(dictionary: [:]))).to.beTrue() - } - - it("evaluates to true with numbers") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.0]))).to.beTrue() - } - - it("evaluates to false with non equal numbers") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.1]))).to.beFalse() - } - - it("evaluates to true with booleans") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() - } - - it("evaluates to false with falsy booleans") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse() - } - - it("evaluates to false with different types") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": 1]))).to.beFalse() - } - } - - func testInequalityExpression() { - let expression = makeExpression(["lhs", "!=", "rhs"]) - - it("evaluates to true with inequal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue() - } - - it("evaluates to false with equal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "b", "rhs": "b"]))).to.beFalse() - } - } - - func testMoreThanExpression() { - let expression = makeExpression(["lhs", ">", "rhs"]) - - it("evaluates to true with lhs > rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue() - } - - it("evaluates to false with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse() - } - } - - func testMoreThanEqualExpression() { - let expression = makeExpression(["lhs", ">=", "rhs"]) - - it("evaluates to true with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() - } - - it("evaluates to false with lhs < rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.1]))).to.beFalse() - } - } - - func testLessThanExpression() { - let expression = makeExpression(["lhs", "<", "rhs"]) - - it("evaluates to true with lhs < rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue() - } - - it("evaluates to false with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse() - } - } - - func testLessThanEqualExpression() { - let expression = makeExpression(["lhs", "<=", "rhs"]) - - it("evaluates to true with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() - } - - it("evaluates to false with lhs > rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.1, "rhs": 5.0]))).to.beFalse() - } - } - - func testMultipleExpressions() { - let expression = makeExpression(["one", "or", "two", "and", "not", "three"]) - - it("evaluates to true with one") { - try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue() - } - - it("evaluates to true with one and three") { - try expect(expression.evaluate(context: Context(dictionary: ["one": true, "three": true]))).to.beTrue() - } - - it("evaluates to true with two") { - try expect(expression.evaluate(context: Context(dictionary: ["two": true]))).to.beTrue() - } - - it("evaluates to false with two and three") { - try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse() - } - - it("evaluates to false with two and three") { - try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse() - } - - it("evaluates to false with nothing") { - try expect(expression.evaluate(context: Context())).to.beFalse() - } - } - - func testTrueInExpression() throws { - let expression = makeExpression(["lhs", "in", "rhs"]) - - try expect(expression.evaluate(context: Context(dictionary: [ - "lhs": 1, - "rhs": [1, 2, 3] - ]))).to.beTrue() - try expect(expression.evaluate(context: Context(dictionary: [ - "lhs": "a", - "rhs": ["a", "b", "c"] - ]))).to.beTrue() - try expect(expression.evaluate(context: Context(dictionary: [ - "lhs": "a", - "rhs": "abc" - ]))).to.beTrue() - try expect(expression.evaluate(context: Context(dictionary: [ - "lhs": 1, - "rhs": 1...3 - ]))).to.beTrue() - try expect(expression.evaluate(context: Context(dictionary: [ - "lhs": 1, - "rhs": 1..<3 - ]))).to.beTrue() - } - - func testFalseInExpression() throws { - let expression = makeExpression(["lhs", "in", "rhs"]) - - try expect(expression.evaluate(context: Context(dictionary: [ - "lhs": 1, - "rhs": [2, 3, 4] - ]))).to.beFalse() - try expect(expression.evaluate(context: Context(dictionary: [ - "lhs": "a", - "rhs": ["b", "c", "d"] - ]))).to.beFalse() - try expect(expression.evaluate(context: Context(dictionary: [ - "lhs": "a", - "rhs": "bcd" - ]))).to.beFalse() - try expect(expression.evaluate(context: Context(dictionary: [ - "lhs": 4, - "rhs": 1...3 - ]))).to.beFalse() - try expect(expression.evaluate(context: Context(dictionary: [ - "lhs": 3, - "rhs": 1..<3 - ]))).to.beFalse() - } + private let parser = TokenParser(tokens: [], environment: Environment()) + + private func makeExpression(_ components: [String]) -> Stencil.Expression { + do { + let parser = try IfExpressionParser.parser( + components: components, + environment: Environment(), + token: .text(value: "", at: .unknown) + ) + return try parser.parse() + } catch { + fatalError(error.localizedDescription) + } + } + + func testTrueExpressions() { + let expression = VariableExpression(variable: Variable("value")) + + it("evaluates to true when value is not nil") { + let context = Context(dictionary: ["value": "known"]) + try expect(try expression.evaluate(context: context)).to.beTrue() + } + + it("evaluates to true when array variable is not empty") { + let items: [[String: Any]] = [["key": "key1", "value": 42], ["key": "key2", "value": 1_337]] + let context = Context(dictionary: ["value": [items]]) + try expect(try expression.evaluate(context: context)).to.beTrue() + } + + it("evaluates to false when dictionary value is empty") { + let emptyItems = [String: Any]() + let context = Context(dictionary: ["value": emptyItems]) + try expect(try expression.evaluate(context: context)).to.beFalse() + } + + it("evaluates to true when integer value is above 0") { + let context = Context(dictionary: ["value": 1]) + try expect(try expression.evaluate(context: context)).to.beTrue() + } + + it("evaluates to true with string") { + let context = Context(dictionary: ["value": "test"]) + try expect(try expression.evaluate(context: context)).to.beTrue() + } + + it("evaluates to true when float value is above 0") { + let context = Context(dictionary: ["value": Float(0.5)]) + try expect(try expression.evaluate(context: context)).to.beTrue() + } + + it("evaluates to true when double value is above 0") { + let context = Context(dictionary: ["value": Double(0.5)]) + try expect(try expression.evaluate(context: context)).to.beTrue() + } + } + + func testFalseExpressions() { + let expression = VariableExpression(variable: Variable("value")) + + it("evaluates to false when value is unset") { + let context = Context() + try expect(try expression.evaluate(context: context)).to.beFalse() + } + + it("evaluates to false when array value is empty") { + let emptyItems = [[String: Any]]() + let context = Context(dictionary: ["value": emptyItems]) + try expect(try expression.evaluate(context: context)).to.beFalse() + } + + it("evaluates to false when dictionary value is empty") { + let emptyItems = [String: Any]() + let context = Context(dictionary: ["value": emptyItems]) + try expect(try expression.evaluate(context: context)).to.beFalse() + } + + it("evaluates to false when Array value is empty") { + let context = Context(dictionary: ["value": ([] as [Any])]) + try expect(try expression.evaluate(context: context)).to.beFalse() + } + + it("evaluates to false when empty string") { + let context = Context(dictionary: ["value": ""]) + try expect(try expression.evaluate(context: context)).to.beFalse() + } + + it("evaluates to false when integer value is below 0 or below") { + let context = Context(dictionary: ["value": 0]) + try expect(try expression.evaluate(context: context)).to.beFalse() + + let negativeContext = Context(dictionary: ["value": -1]) + try expect(try expression.evaluate(context: negativeContext)).to.beFalse() + } + + it("evaluates to false when float is 0 or below") { + let context = Context(dictionary: ["value": Float(0)]) + try expect(try expression.evaluate(context: context)).to.beFalse() + } + + it("evaluates to false when double is 0 or below") { + let context = Context(dictionary: ["value": Double(0)]) + try expect(try expression.evaluate(context: context)).to.beFalse() + } + + it("evaluates to false when uint is 0") { + let context = Context(dictionary: ["value": UInt(0)]) + try expect(try expression.evaluate(context: context)).to.beFalse() + } + } + + func testNotExpression() { + it("returns truthy for positive expressions") { + let expression = NotExpression(expression: VariableExpression(variable: Variable("true"))) + try expect(expression.evaluate(context: Context())).to.beFalse() + } + + it("returns falsy for negative expressions") { + let expression = NotExpression(expression: VariableExpression(variable: Variable("false"))) + try expect(expression.evaluate(context: Context())).to.beTrue() + } + } + + func testExpressionParsing() { + it("can parse a variable expression") { + let expression = self.makeExpression(["value"]) + try expect(expression.evaluate(context: Context())).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue() + } + + it("can parse a not expression") { + let expression = self.makeExpression(["not", "value"]) + try expect(expression.evaluate(context: Context())).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse() + } + } + + func testAndExpression() { + let expression = makeExpression(["lhs", "and", "rhs"]) + + it("evaluates to false with lhs false") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse() + } + + it("evaluates to false with rhs false") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse() + } + + it("evaluates to false with lhs and rhs false") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse() + } + + it("evaluates to true with lhs and rhs true") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() + } + } + + func testOrExpression() { + let expression = makeExpression(["lhs", "or", "rhs"]) + + it("evaluates to true with lhs true") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue() + } + + it("evaluates to true with rhs true") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beTrue() + } + + it("evaluates to true with lhs and rhs true") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() + } + + it("evaluates to false with lhs and rhs false") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse() + } + } + + func testEqualityExpression() { + let expression = makeExpression(["lhs", "==", "rhs"]) + + it("evaluates to true with equal lhs/rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue() + } + + it("evaluates to false with non equal lhs/rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beFalse() + } + + it("evaluates to true with nils") { + try expect(expression.evaluate(context: Context(dictionary: [:]))).to.beTrue() + } + + it("evaluates to true with numbers") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.0]))).to.beTrue() + } + + it("evaluates to false with non equal numbers") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.1]))).to.beFalse() + } + + it("evaluates to true with booleans") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() + } + + it("evaluates to false with falsy booleans") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse() + } + + it("evaluates to false with different types") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": 1]))).to.beFalse() + } + } + + func testInequalityExpression() { + let expression = makeExpression(["lhs", "!=", "rhs"]) + + it("evaluates to true with inequal lhs/rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue() + } + + it("evaluates to false with equal lhs/rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "b", "rhs": "b"]))).to.beFalse() + } + } + + func testMoreThanExpression() { + let expression = makeExpression(["lhs", ">", "rhs"]) + + it("evaluates to true with lhs > rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue() + } + + it("evaluates to false with lhs == rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse() + } + } + + func testMoreThanEqualExpression() { + let expression = makeExpression(["lhs", ">=", "rhs"]) + + it("evaluates to true with lhs == rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() + } + + it("evaluates to false with lhs < rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.1]))).to.beFalse() + } + } + + func testLessThanExpression() { + let expression = makeExpression(["lhs", "<", "rhs"]) + + it("evaluates to true with lhs < rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue() + } + + it("evaluates to false with lhs == rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse() + } + } + + func testLessThanEqualExpression() { + let expression = makeExpression(["lhs", "<=", "rhs"]) + + it("evaluates to true with lhs == rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() + } + + it("evaluates to false with lhs > rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.1, "rhs": 5.0]))).to.beFalse() + } + } + + func testMultipleExpressions() { + let expression = makeExpression(["one", "or", "two", "and", "not", "three"]) + + it("evaluates to true with one") { + try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue() + } + + it("evaluates to true with one and three") { + try expect(expression.evaluate(context: Context(dictionary: ["one": true, "three": true]))).to.beTrue() + } + + it("evaluates to true with two") { + try expect(expression.evaluate(context: Context(dictionary: ["two": true]))).to.beTrue() + } + + it("evaluates to false with two and three") { + try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse() + } + + it("evaluates to false with two and three") { + try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse() + } + + it("evaluates to false with nothing") { + try expect(expression.evaluate(context: Context())).to.beFalse() + } + } + + func testTrueInExpression() throws { + let expression = makeExpression(["lhs", "in", "rhs"]) + + try expect(expression.evaluate(context: Context(dictionary: [ + "lhs": 1, + "rhs": [1, 2, 3] + ]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: [ + "lhs": "a", + "rhs": ["a", "b", "c"] + ]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: [ + "lhs": "a", + "rhs": "abc" + ]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: [ + "lhs": 1, + "rhs": 1...3 + ]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: [ + "lhs": 1, + "rhs": 1..<3 + ]))).to.beTrue() + } + + func testFalseInExpression() throws { + let expression = makeExpression(["lhs", "in", "rhs"]) + + try expect(expression.evaluate(context: Context(dictionary: [ + "lhs": 1, + "rhs": [2, 3, 4] + ]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: [ + "lhs": "a", + "rhs": ["b", "c", "d"] + ]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: [ + "lhs": "a", + "rhs": "bcd" + ]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: [ + "lhs": 4, + "rhs": 1...3 + ]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: [ + "lhs": 3, + "rhs": 1..<3 + ]))).to.beFalse() + } } diff --git a/Tests/StencilTests/FilterSpec.swift b/Tests/StencilTests/FilterSpec.swift index 092c943..a02f1a6 100644 --- a/Tests/StencilTests/FilterSpec.swift +++ b/Tests/StencilTests/FilterSpec.swift @@ -3,380 +3,380 @@ import Spectre import XCTest final class FilterTests: XCTestCase { - func testRegistration() { - let context: [String: Any] = ["name": "Kyle"] + func testRegistration() { + let context: [String: Any] = ["name": "Kyle"] - it("allows you to register a custom filter") { - let template = Template(templateString: "{{ name|repeat }}") + it("allows you to register a custom filter") { + let template = Template(templateString: "{{ name|repeat }}") - let repeatExtension = Extension() - repeatExtension.registerFilter("repeat") { (value: Any?) in - if let value = value as? String { - return "\(value) \(value)" - } + let repeatExtension = Extension() + repeatExtension.registerFilter("repeat") { (value: Any?) in + if let value = value as? String { + return "\(value) \(value)" + } - return nil - } + return nil + } - let result = try template.render(Context( - dictionary: context, - environment: Environment(extensions: [repeatExtension]) - )) - try expect(result) == "Kyle Kyle" - } + let result = try template.render(Context( + dictionary: context, + environment: Environment(extensions: [repeatExtension]) + )) + try expect(result) == "Kyle Kyle" + } - it("allows you to register boolean filters") { - let repeatExtension = Extension() - repeatExtension.registerFilter(name: "isPositive", negativeFilterName: "isNotPositive") { (value: Any?) in - if let value = value as? Int { - return value > 0 - } - return nil - } + it("allows you to register boolean filters") { + let repeatExtension = Extension() + repeatExtension.registerFilter(name: "isPositive", negativeFilterName: "isNotPositive") { (value: Any?) in + if let value = value as? Int { + return value > 0 + } + return nil + } - let result = try Template(templateString: "{{ value|isPositive }}") - .render(Context(dictionary: ["value": 1], environment: Environment(extensions: [repeatExtension]))) - try expect(result) == "true" + let result = try Template(templateString: "{{ value|isPositive }}") + .render(Context(dictionary: ["value": 1], environment: Environment(extensions: [repeatExtension]))) + try expect(result) == "true" - let negativeResult = try Template(templateString: "{{ value|isNotPositive }}") - .render(Context(dictionary: ["value": -1], environment: Environment(extensions: [repeatExtension]))) - try expect(negativeResult) == "true" - } + let negativeResult = try Template(templateString: "{{ value|isNotPositive }}") + .render(Context(dictionary: ["value": -1], environment: Environment(extensions: [repeatExtension]))) + try expect(negativeResult) == "true" + } - it("allows you to register a custom which throws") { - let template = Template(templateString: "{{ name|repeat }}") - let repeatExtension = Extension() - repeatExtension.registerFilter("repeat") { (_: Any?) in - throw TemplateSyntaxError("No Repeat") - } + it("allows you to register a custom which throws") { + let template = Template(templateString: "{{ name|repeat }}") + let repeatExtension = Extension() + repeatExtension.registerFilter("repeat") { (_: Any?) in + throw TemplateSyntaxError("No Repeat") + } - let context = Context(dictionary: context, environment: Environment(extensions: [repeatExtension])) - try expect(try template.render(context)) - .toThrow(TemplateSyntaxError(reason: "No Repeat", token: template.tokens.first)) - } + let context = Context(dictionary: context, environment: Environment(extensions: [repeatExtension])) + try expect(try template.render(context)) + .toThrow(TemplateSyntaxError(reason: "No Repeat", token: template.tokens.first)) + } - it("throws when you pass arguments to simple filter") { - let template = Template(templateString: "{{ name|uppercase:5 }}") - try expect(try template.render(Context(dictionary: ["name": "kyle"]))).toThrow() - } - } + it("throws when you pass arguments to simple filter") { + let template = Template(templateString: "{{ name|uppercase:5 }}") + try expect(try template.render(Context(dictionary: ["name": "kyle"]))).toThrow() + } + } - func testRegistrationOverrideDefault() throws { - let template = Template(templateString: "{{ name|join }}") - let context: [String: Any] = ["name": "Kyle"] + func testRegistrationOverrideDefault() throws { + let template = Template(templateString: "{{ name|join }}") + let context: [String: Any] = ["name": "Kyle"] - let repeatExtension = Extension() - repeatExtension.registerFilter("join") { (_: Any?) in - "joined" - } + let repeatExtension = Extension() + repeatExtension.registerFilter("join") { (_: Any?) in + "joined" + } - let result = try template.render(Context( - dictionary: context, - environment: Environment(extensions: [repeatExtension]) - )) - try expect(result) == "joined" - } + let result = try template.render(Context( + dictionary: context, + environment: Environment(extensions: [repeatExtension]) + )) + try expect(result) == "joined" + } - func testRegistrationWithArguments() { - let context: [String: Any] = ["name": "Kyle"] + func testRegistrationWithArguments() { + let context: [String: Any] = ["name": "Kyle"] - it("allows you to register a custom filter which accepts single argument") { - let template = Template(templateString: """ - {{ name|repeat:'value1, "value2"' }} - """) + it("allows you to register a custom filter which accepts single argument") { + let template = Template(templateString: """ + {{ name|repeat:'value1, "value2"' }} + """) - let repeatExtension = Extension() - repeatExtension.registerFilter("repeat") { value, arguments in - guard let value = value, - let argument = arguments.first else { return nil } + let repeatExtension = Extension() + repeatExtension.registerFilter("repeat") { value, arguments in + guard let value = value, + let argument = arguments.first else { return nil } - return "\(value) \(value) with args \(argument ?? "")" - } + return "\(value) \(value) with args \(argument ?? "")" + } - let result = try template.render(Context( - dictionary: context, - environment: Environment(extensions: [repeatExtension]) - )) - try expect(result) == """ - Kyle Kyle with args value1, "value2" - """ - } + let result = try template.render(Context( + dictionary: context, + environment: Environment(extensions: [repeatExtension]) + )) + try expect(result) == """ + Kyle Kyle with args value1, "value2" + """ + } - it("allows you to register a custom filter which accepts several arguments") { - let template = Template(templateString: """ - {{ name|repeat:'value"1"',"value'2'",'(key, value)' }} - """) + it("allows you to register a custom filter which accepts several arguments") { + let template = Template(templateString: """ + {{ name|repeat:'value"1"',"value'2'",'(key, value)' }} + """) - let repeatExtension = Extension() - repeatExtension.registerFilter("repeat") { value, arguments in - guard let value = value else { return nil } - let args = arguments.compactMap { $0 } - return "\(value) \(value) with args 0: \(args[0]), 1: \(args[1]), 2: \(args[2])" - } + let repeatExtension = Extension() + repeatExtension.registerFilter("repeat") { value, arguments in + guard let value = value else { return nil } + let args = arguments.compactMap { $0 } + return "\(value) \(value) with args 0: \(args[0]), 1: \(args[1]), 2: \(args[2])" + } - let result = try template.render(Context( - dictionary: context, - environment: Environment(extensions: [repeatExtension]) - )) - try expect(result) == """ - Kyle Kyle with args 0: value"1", 1: value'2', 2: (key, value) - """ - } + let result = try template.render(Context( + dictionary: context, + environment: Environment(extensions: [repeatExtension]) + )) + try expect(result) == """ + Kyle Kyle with args 0: value"1", 1: value'2', 2: (key, value) + """ + } - it("allows whitespace in expression") { - let template = Template(templateString: """ - {{ value | join : ", " }} - """) - let result = try template.render(Context(dictionary: ["value": ["One", "Two"]])) - try expect(result) == "One, Two" - } - } + it("allows whitespace in expression") { + let template = Template(templateString: """ + {{ value | join : ", " }} + """) + let result = try template.render(Context(dictionary: ["value": ["One", "Two"]])) + try expect(result) == "One, Two" + } + } - func testStringFilters() { - it("transforms a string to be capitalized") { - let template = Template(templateString: "{{ name|capitalize }}") - let result = try template.render(Context(dictionary: ["name": "kyle"])) - try expect(result) == "Kyle" - } + func testStringFilters() { + it("transforms a string to be capitalized") { + let template = Template(templateString: "{{ name|capitalize }}") + let result = try template.render(Context(dictionary: ["name": "kyle"])) + try expect(result) == "Kyle" + } - it("transforms a string to be uppercase") { - let template = Template(templateString: "{{ name|uppercase }}") - let result = try template.render(Context(dictionary: ["name": "kyle"])) - try expect(result) == "KYLE" - } + it("transforms a string to be uppercase") { + let template = Template(templateString: "{{ name|uppercase }}") + let result = try template.render(Context(dictionary: ["name": "kyle"])) + try expect(result) == "KYLE" + } - it("transforms a string to be lowercase") { - let template = Template(templateString: "{{ name|lowercase }}") - let result = try template.render(Context(dictionary: ["name": "Kyle"])) - try expect(result) == "kyle" - } - } + it("transforms a string to be lowercase") { + let template = Template(templateString: "{{ name|lowercase }}") + let result = try template.render(Context(dictionary: ["name": "Kyle"])) + try expect(result) == "kyle" + } + } - func testStringFiltersWithArrays() { - it("transforms a string to be capitalized") { - let template = Template(templateString: "{{ names|capitalize }}") - let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]])) - try expect(result) == """ - ["Kyle", "Kyle"] - """ - } + func testStringFiltersWithArrays() { + it("transforms a string to be capitalized") { + let template = Template(templateString: "{{ names|capitalize }}") + let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]])) + try expect(result) == """ + ["Kyle", "Kyle"] + """ + } - it("transforms a string to be uppercase") { - let template = Template(templateString: "{{ names|uppercase }}") - let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]])) - try expect(result) == """ - ["KYLE", "KYLE"] - """ - } + it("transforms a string to be uppercase") { + let template = Template(templateString: "{{ names|uppercase }}") + let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]])) + try expect(result) == """ + ["KYLE", "KYLE"] + """ + } - it("transforms a string to be lowercase") { - let template = Template(templateString: "{{ names|lowercase }}") - let result = try template.render(Context(dictionary: ["names": ["Kyle", "Kyle"]])) - try expect(result) == """ - ["kyle", "kyle"] - """ - } - } + it("transforms a string to be lowercase") { + let template = Template(templateString: "{{ names|lowercase }}") + let result = try template.render(Context(dictionary: ["names": ["Kyle", "Kyle"]])) + try expect(result) == """ + ["kyle", "kyle"] + """ + } + } - func testDefaultFilter() { - let template = Template(templateString: """ - Hello {{ name|default:"World" }} - """) + func testDefaultFilter() { + let template = Template(templateString: """ + Hello {{ name|default:"World" }} + """) - it("shows the variable value") { - let result = try template.render(Context(dictionary: ["name": "Kyle"])) - try expect(result) == "Hello Kyle" - } + it("shows the variable value") { + let result = try template.render(Context(dictionary: ["name": "Kyle"])) + try expect(result) == "Hello Kyle" + } - it("shows the default value") { - let result = try template.render(Context(dictionary: [:])) - try expect(result) == "Hello World" - } + it("shows the default value") { + let result = try template.render(Context(dictionary: [:])) + try expect(result) == "Hello World" + } - it("supports multiple defaults") { - let template = Template(templateString: """ - Hello {{ name|default:a,b,c,"World" }} - """) - let result = try template.render(Context(dictionary: [:])) - try expect(result) == "Hello World" - } + it("supports multiple defaults") { + let template = Template(templateString: """ + Hello {{ name|default:a,b,c,"World" }} + """) + let result = try template.render(Context(dictionary: [:])) + try expect(result) == "Hello World" + } - it("can use int as default") { - let template = Template(templateString: "{{ value|default:1 }}") - let result = try template.render(Context(dictionary: [:])) - try expect(result) == "1" - } + it("can use int as default") { + let template = Template(templateString: "{{ value|default:1 }}") + let result = try template.render(Context(dictionary: [:])) + try expect(result) == "1" + } - it("can use float as default") { - let template = Template(templateString: "{{ value|default:1.5 }}") - let result = try template.render(Context(dictionary: [:])) - try expect(result) == "1.5" - } + it("can use float as default") { + let template = Template(templateString: "{{ value|default:1.5 }}") + let result = try template.render(Context(dictionary: [:])) + try expect(result) == "1.5" + } - it("checks for underlying nil value correctly") { - let template = Template(templateString: """ - Hello {{ user.name|default:"anonymous" }} - """) - let nilName: String? = nil - let user: [String: Any?] = ["name": nilName] - let result = try template.render(Context(dictionary: ["user": user])) - try expect(result) == "Hello anonymous" - } - } + it("checks for underlying nil value correctly") { + let template = Template(templateString: """ + Hello {{ user.name|default:"anonymous" }} + """) + let nilName: String? = nil + let user: [String: Any?] = ["name": nilName] + let result = try template.render(Context(dictionary: ["user": user])) + try expect(result) == "Hello anonymous" + } + } - func testJoinFilter() { - let template = Template(templateString: """ - {{ value|join:", " }} - """) + func testJoinFilter() { + let template = Template(templateString: """ + {{ value|join:", " }} + """) - it("joins a collection of strings") { - let result = try template.render(Context(dictionary: ["value": ["One", "Two"]])) - try expect(result) == "One, Two" - } + it("joins a collection of strings") { + let result = try template.render(Context(dictionary: ["value": ["One", "Two"]])) + try expect(result) == "One, Two" + } - it("joins a mixed-type collection") { - let result = try template.render(Context(dictionary: ["value": ["One", 2, true, 10.5, "Five"]])) - try expect(result) == "One, 2, true, 10.5, Five" - } + it("joins a mixed-type collection") { + let result = try template.render(Context(dictionary: ["value": ["One", 2, true, 10.5, "Five"]])) + try expect(result) == "One, 2, true, 10.5, Five" + } - it("can join by non string") { - let template = Template(templateString: """ - {{ value|join:separator }} - """) - let result = try template.render(Context(dictionary: ["value": ["One", "Two"], "separator": true])) - try expect(result) == "OnetrueTwo" - } + it("can join by non string") { + let template = Template(templateString: """ + {{ value|join:separator }} + """) + let result = try template.render(Context(dictionary: ["value": ["One", "Two"], "separator": true])) + try expect(result) == "OnetrueTwo" + } - it("can join without arguments") { - let template = Template(templateString: """ - {{ value|join }} - """) - let result = try template.render(Context(dictionary: ["value": ["One", "Two"]])) - try expect(result) == "OneTwo" - } - } + it("can join without arguments") { + let template = Template(templateString: """ + {{ value|join }} + """) + let result = try template.render(Context(dictionary: ["value": ["One", "Two"]])) + try expect(result) == "OneTwo" + } + } - func testSplitFilter() { - let template = Template(templateString: """ - {{ value|split:", " }} - """) + func testSplitFilter() { + let template = Template(templateString: """ + {{ value|split:", " }} + """) - it("split a string into array") { - let result = try template.render(Context(dictionary: ["value": "One, Two"])) - try expect(result) == """ - ["One", "Two"] - """ - } + it("split a string into array") { + let result = try template.render(Context(dictionary: ["value": "One, Two"])) + try expect(result) == """ + ["One", "Two"] + """ + } - it("can split without arguments") { - let template = Template(templateString: """ - {{ value|split }} - """) - let result = try template.render(Context(dictionary: ["value": "One, Two"])) - try expect(result) == """ - ["One,", "Two"] - """ - } - } + it("can split without arguments") { + let template = Template(templateString: """ + {{ value|split }} + """) + let result = try template.render(Context(dictionary: ["value": "One, Two"])) + try expect(result) == """ + ["One,", "Two"] + """ + } + } - func testFilterSuggestion() { - it("made for unknown filter") { - let template = Template(templateString: "{{ value|unknownFilter }}") - let filterExtension = Extension() - filterExtension.registerFilter("knownFilter") { value, _ in value } + func testFilterSuggestion() { + it("made for unknown filter") { + let template = Template(templateString: "{{ value|unknownFilter }}") + let filterExtension = Extension() + filterExtension.registerFilter("knownFilter") { value, _ in value } - try self.expectError( - reason: "Unknown filter 'unknownFilter'. Found similar filters: 'knownFilter'.", - token: "value|unknownFilter", - template: template, - extension: filterExtension - ) - } + try self.expectError( + reason: "Unknown filter 'unknownFilter'. Found similar filters: 'knownFilter'.", + token: "value|unknownFilter", + template: template, + extension: filterExtension + ) + } - it("made for multiple similar filters") { - let template = Template(templateString: "{{ value|lowerFirst }}") - let filterExtension = Extension() - filterExtension.registerFilter("lowerFirstWord") { value, _ in value } - filterExtension.registerFilter("lowerFirstLetter") { value, _ in value } + it("made for multiple similar filters") { + let template = Template(templateString: "{{ value|lowerFirst }}") + let filterExtension = Extension() + filterExtension.registerFilter("lowerFirstWord") { value, _ in value } + filterExtension.registerFilter("lowerFirstLetter") { value, _ in value } - try self.expectError( - reason: "Unknown filter 'lowerFirst'. Found similar filters: 'lowerFirstWord', 'lowercase'.", - token: "value|lowerFirst", - template: template, - extension: filterExtension - ) - } + try self.expectError( + reason: "Unknown filter 'lowerFirst'. Found similar filters: 'lowerFirstWord', 'lowercase'.", + token: "value|lowerFirst", + template: template, + extension: filterExtension + ) + } - it("not made when can't find similar filter") { - let template = Template(templateString: "{{ value|unknownFilter }}") - let filterExtension = Extension() - filterExtension.registerFilter("lowerFirstWord") { value, _ in value } + it("not made when can't find similar filter") { + let template = Template(templateString: "{{ value|unknownFilter }}") + let filterExtension = Extension() + filterExtension.registerFilter("lowerFirstWord") { value, _ in value } - try self.expectError( - reason: "Unknown filter 'unknownFilter'. Found similar filters: 'lowerFirstWord'.", - token: "value|unknownFilter", - template: template, - extension: filterExtension - ) - } - } + try self.expectError( + reason: "Unknown filter 'unknownFilter'. Found similar filters: 'lowerFirstWord'.", + token: "value|unknownFilter", + template: template, + extension: filterExtension + ) + } + } - func testIndentContent() throws { - let template = Template(templateString: """ + func testIndentContent() throws { + let template = Template(templateString: """ {{ value|indent:2 }} """) - let result = try template.render(Context(dictionary: [ - "value": """ + let result = try template.render(Context(dictionary: [ + "value": """ One Two """ - ])) - try expect(result) == """ + ])) + try expect(result) == """ One Two """ - } + } - func testIndentWithArbitraryCharacter() throws { - let template = Template(templateString: """ + func testIndentWithArbitraryCharacter() throws { + let template = Template(templateString: """ {{ value|indent:2,"\t" }} """) - let result = try template.render(Context(dictionary: [ - "value": """ + let result = try template.render(Context(dictionary: [ + "value": """ One Two """ - ])) - try expect(result) == """ - One - \t\tTwo - """ - } + ])) + try expect(result) == """ + One + \t\tTwo + """ + } - func testIndentFirstLine() throws { - let template = Template(templateString: """ - {{ value|indent:2," ",true }} - """) - let result = try template.render(Context(dictionary: [ - "value": """ + func testIndentFirstLine() throws { + let template = Template(templateString: """ + {{ value|indent:2," ",true }} + """) + let result = try template.render(Context(dictionary: [ + "value": """ One Two """ - ])) - // swiftlint:disable indentation_width - try expect(result) == """ + ])) + // swiftlint:disable indentation_width + try expect(result) == """ One Two """ - // swiftlint:enable indentation_width - } + // swiftlint:enable indentation_width + } - func testIndentNotEmptyLines() throws { - let template = Template(templateString: """ - {{ value|indent }} - """) - let result = try template.render(Context(dictionary: [ - "value": """ + func testIndentNotEmptyLines() throws { + let template = Template(templateString: """ + {{ value|indent }} + """) + let result = try template.render(Context(dictionary: [ + "value": """ One @@ -384,9 +384,9 @@ final class FilterTests: XCTestCase { """ - ])) - // swiftlint:disable indentation_width - try expect(result) == """ + ])) + // swiftlint:disable indentation_width + try expect(result) == """ One @@ -394,64 +394,64 @@ final class FilterTests: XCTestCase { """ - // swiftlint:enable indentation_width - } + // swiftlint:enable indentation_width + } - func testDynamicFilters() throws { - it("can apply dynamic filter") { - let template = Template(templateString: "{{ name|filter:somefilter }}") - let result = try template.render(Context(dictionary: ["name": "Jhon", "somefilter": "uppercase"])) - try expect(result) == "JHON" - } + func testDynamicFilters() throws { + it("can apply dynamic filter") { + let template = Template(templateString: "{{ name|filter:somefilter }}") + let result = try template.render(Context(dictionary: ["name": "Jhon", "somefilter": "uppercase"])) + try expect(result) == "JHON" + } - it("can apply dynamic filter on array") { - let template = Template(templateString: "{{ values|filter:joinfilter }}") - let result = try template.render(Context(dictionary: ["values": [1, 2, 3], "joinfilter": "join:\", \""])) - try expect(result) == "1, 2, 3" - } + it("can apply dynamic filter on array") { + let template = Template(templateString: "{{ values|filter:joinfilter }}") + let result = try template.render(Context(dictionary: ["values": [1, 2, 3], "joinfilter": "join:\", \""])) + try expect(result) == "1, 2, 3" + } - it("throws on unknown dynamic filter") { - let template = Template(templateString: "{{ values|filter:unknown }}") - let context = Context(dictionary: ["values": [1, 2, 3], "unknown": "absurd"]) - try expect(try template.render(context)).toThrow() - } - } + it("throws on unknown dynamic filter") { + let template = Template(templateString: "{{ values|filter:unknown }}") + let context = Context(dictionary: ["values": [1, 2, 3], "unknown": "absurd"]) + try expect(try template.render(context)).toThrow() + } + } - private func expectError( - reason: String, - token: String, - template: Template, - extension: Extension, - file: String = #file, - line: Int = #line, - function: String = #function - ) throws { - guard let range = template.templateString.range(of: token) else { - fatalError("Can't find '\(token)' in '\(template)'") - } + private func expectError( + reason: String, + token: String, + template: Template, + extension: Extension, + file: String = #file, + line: Int = #line, + function: String = #function + ) throws { + guard let range = template.templateString.range(of: token) else { + fatalError("Can't find '\(token)' in '\(template)'") + } - let environment = Environment(extensions: [`extension`]) - let expectedError: Error = { - let lexer = Lexer(templateString: template.templateString) - let location = lexer.rangeLocation(range) - let sourceMap = SourceMap(filename: template.name, location: location) - let token = Token.block(value: token, at: sourceMap) - return TemplateSyntaxError(reason: reason, token: token, stackTrace: []) - }() + let environment = Environment(extensions: [`extension`]) + let expectedError: Error = { + let lexer = Lexer(templateString: template.templateString) + let location = lexer.rangeLocation(range) + let sourceMap = SourceMap(filename: template.name, location: location) + let token = Token.block(value: token, at: sourceMap) + return TemplateSyntaxError(reason: reason, token: token, stackTrace: []) + }() - let error = try expect( - environment.render(template: template, context: [:]), - file: file, - line: line, - function: function - ).toThrow() as TemplateSyntaxError - let reporter = SimpleErrorReporter() + let error = try expect( + environment.render(template: template, context: [:]), + file: file, + line: line, + function: function + ).toThrow() as TemplateSyntaxError + let reporter = SimpleErrorReporter() - try expect( - reporter.renderError(error), - file: file, - line: line, - function: function - ) == reporter.renderError(expectedError) - } + try expect( + reporter.renderError(error), + file: file, + line: line, + function: function + ) == reporter.renderError(expectedError) + } } diff --git a/Tests/StencilTests/FilterTagSpec.swift b/Tests/StencilTests/FilterTagSpec.swift index d73cf40..3906f3f 100644 --- a/Tests/StencilTests/FilterTagSpec.swift +++ b/Tests/StencilTests/FilterTagSpec.swift @@ -3,52 +3,52 @@ import Stencil import XCTest final class FilterTagTests: XCTestCase { - func testFilterTag() { - it("allows you to use a filter") { - let template = Template(templateString: "{% filter uppercase %}Test{% endfilter %}") - let result = try template.render() - try expect(result) == "TEST" - } + func testFilterTag() { + it("allows you to use a filter") { + let template = Template(templateString: "{% filter uppercase %}Test{% endfilter %}") + let result = try template.render() + try expect(result) == "TEST" + } - it("allows you to chain filters") { - let template = Template(templateString: "{% filter lowercase|capitalize %}TEST{% endfilter %}") - let result = try template.render() - try expect(result) == "Test" - } + it("allows you to chain filters") { + let template = Template(templateString: "{% filter lowercase|capitalize %}TEST{% endfilter %}") + let result = try template.render() + try expect(result) == "Test" + } - it("errors without a filter") { - let template = Template(templateString: "Some {% filter %}Test{% endfilter %}") - try expect(try template.render()).toThrow() - } + it("errors without a filter") { + let template = Template(templateString: "Some {% filter %}Test{% endfilter %}") + try expect(try template.render()).toThrow() + } - it("can render filters with arguments") { - let ext = Extension() - ext.registerFilter("split") { value, args in - guard let value = value as? String, - let argument = args.first as? String else { return value } - return value.components(separatedBy: argument) - } - let env = Environment(extensions: [ext]) - let result = try env.renderTemplate(string: """ - {% filter split:","|join:";" %}{{ items|join:"," }}{% endfilter %} - """, context: ["items": [1, 2]]) - try expect(result) == "1;2" - } + it("can render filters with arguments") { + let ext = Extension() + ext.registerFilter("split") { value, args in + guard let value = value as? String, + let argument = args.first as? String else { return value } + return value.components(separatedBy: argument) + } + let env = Environment(extensions: [ext]) + let result = try env.renderTemplate(string: """ + {% filter split:","|join:";" %}{{ items|join:"," }}{% endfilter %} + """, context: ["items": [1, 2]]) + try expect(result) == "1;2" + } - it("can render filters with quote as an argument") { - let ext = Extension() - ext.registerFilter("replace") { value, args in - guard let value = value as? String, - args.count == 2, - let search = args.first as? String, - let replacement = args.last as? String else { return value } - return value.replacingOccurrences(of: search, with: replacement) - } - let env = Environment(extensions: [ext]) - let result = try env.renderTemplate(string: """ - {% filter replace:'"',"" %}{{ items|join:"," }}{% endfilter %} - """, context: ["items": ["\"1\"", "\"2\""]]) - try expect(result) == "1,2" - } - } + it("can render filters with quote as an argument") { + let ext = Extension() + ext.registerFilter("replace") { value, args in + guard let value = value as? String, + args.count == 2, + let search = args.first as? String, + let replacement = args.last as? String else { return value } + return value.replacingOccurrences(of: search, with: replacement) + } + let env = Environment(extensions: [ext]) + let result = try env.renderTemplate(string: """ + {% filter replace:'"',"" %}{{ items|join:"," }}{% endfilter %} + """, context: ["items": ["\"1\"", "\"2\""]]) + try expect(result) == "1,2" + } + } } diff --git a/Tests/StencilTests/ForNodeSpec.swift b/Tests/StencilTests/ForNodeSpec.swift index 9d31b58..103c87e 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -3,589 +3,589 @@ import Spectre import XCTest final class ForNodeTests: XCTestCase { - private let context = Context(dictionary: [ - "items": [1, 2, 3], - "anyItems": [1, 2, 3] as [Any], - // swiftlint:disable:next legacy_objc_type - "nsItems": NSArray(array: [1, 2, 3]), - "emptyItems": [Int](), - "dict": [ - "one": "I", - "two": "II" - ], - "tuples": [(1, 2, 3), (4, 5, 6)] - ]) + private let context = Context(dictionary: [ + "items": [1, 2, 3], + "anyItems": [1, 2, 3] as [Any], + // swiftlint:disable:next legacy_objc_type + "nsItems": NSArray(array: [1, 2, 3]), + "emptyItems": [Int](), + "dict": [ + "one": "I", + "two": "II" + ], + "tuples": [(1, 2, 3), (4, 5, 6)] + ]) - func testForNode() { - it("renders the given nodes for each item") { - let nodes: [NodeType] = [VariableNode(variable: "item")] - let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(self.context)) == "123" - } + func testForNode() { + it("renders the given nodes for each item") { + let nodes: [NodeType] = [VariableNode(variable: "item")] + let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + try expect(try node.render(self.context)) == "123" + } - it("renders the given empty nodes when no items found item") { - let node = ForNode( - resolvable: Variable("emptyItems"), - loopVariables: ["item"], - nodes: [VariableNode(variable: "item")], - emptyNodes: [TextNode(text: "empty")] - ) - try expect(try node.render(self.context)) == "empty" - } + it("renders the given empty nodes when no items found item") { + let node = ForNode( + resolvable: Variable("emptyItems"), + loopVariables: ["item"], + nodes: [VariableNode(variable: "item")], + emptyNodes: [TextNode(text: "empty")] + ) + try expect(try node.render(self.context)) == "empty" + } - it("renders a context variable of type Array") { - let nodes: [NodeType] = [VariableNode(variable: "item")] - let node = ForNode(resolvable: Variable("anyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(self.context)) == "123" - } + it("renders a context variable of type Array") { + let nodes: [NodeType] = [VariableNode(variable: "item")] + let node = ForNode(resolvable: Variable("anyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + try expect(try node.render(self.context)) == "123" + } - #if os(OSX) - it("renders a context variable of type NSArray") { - let nodes: [NodeType] = [VariableNode(variable: "item")] - let node = ForNode(resolvable: Variable("nsItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(self.context)) == "123" - } - #endif + #if os(OSX) + it("renders a context variable of type NSArray") { + let nodes: [NodeType] = [VariableNode(variable: "item")] + let node = ForNode(resolvable: Variable("nsItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + try expect(try node.render(self.context)) == "123" + } + #endif - it("can render a filter with spaces") { - let template = Template(templateString: """ - {% for article in ars | default: a, b , articles %}\ - - {{ article.title }} by {{ article.author }}. - {% endfor %} - """) - let context = Context(dictionary: [ - "articles": [ - Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"), - Article(title: "Memory Management with ARC", author: "Kyle Fuller") - ] - ]) - let result = try template.render(context) + it("can render a filter with spaces") { + let template = Template(templateString: """ + {% for article in ars | default: a, b , articles %}\ + - {{ article.title }} by {{ article.author }}. + {% endfor %} + """) + let context = Context(dictionary: [ + "articles": [ + Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"), + Article(title: "Memory Management with ARC", author: "Kyle Fuller") + ] + ]) + let result = try template.render(context) - try expect(result) == """ - - Migrating from OCUnit to XCTest by Kyle Fuller. - - Memory Management with ARC by Kyle Fuller. + try expect(result) == """ + - Migrating from OCUnit to XCTest by Kyle Fuller. + - Memory Management with ARC by Kyle Fuller. - """ - } - } + """ + } + } - func testLoopMetadata() { - it("renders the given nodes while providing if the item is first in the context") { - let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.first")] - let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(self.context)) == "1true2false3false" - } + func testLoopMetadata() { + it("renders the given nodes while providing if the item is first in the context") { + let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.first")] + let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + try expect(try node.render(self.context)) == "1true2false3false" + } - it("renders the given nodes while providing if the item is last in the context") { - let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.last")] - let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(self.context)) == "1false2false3true" - } + it("renders the given nodes while providing if the item is last in the context") { + let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.last")] + let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + try expect(try node.render(self.context)) == "1false2false3true" + } - it("renders the given nodes while providing item counter") { - let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")] - let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(self.context)) == "112233" - } + it("renders the given nodes while providing item counter") { + let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")] + let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + try expect(try node.render(self.context)) == "112233" + } - it("renders the given nodes while providing item counter") { - let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter0")] - let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(self.context)) == "102132" - } + it("renders the given nodes while providing item counter") { + let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter0")] + let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + try expect(try node.render(self.context)) == "102132" + } - it("renders the given nodes while providing loop length") { - let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.length")] - let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(self.context)) == "132333" - } - } + it("renders the given nodes while providing loop length") { + let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.length")] + let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + try expect(try node.render(self.context)) == "132333" + } + } - func testWhereExpression() { - it("renders the given nodes while filtering items using where expression") { - let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")] - let parser = TokenParser(tokens: [], environment: Environment()) - let `where` = try parser.compileExpression(components: ["item", ">", "1"], token: .text(value: "", at: .unknown)) - let node = ForNode( - resolvable: Variable("items"), - loopVariables: ["item"], - nodes: nodes, - emptyNodes: [], - where: `where` - ) - try expect(try node.render(self.context)) == "2132" - } + func testWhereExpression() { + it("renders the given nodes while filtering items using where expression") { + let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")] + let parser = TokenParser(tokens: [], environment: Environment()) + let `where` = try parser.compileExpression(components: ["item", ">", "1"], token: .text(value: "", at: .unknown)) + let node = ForNode( + resolvable: Variable("items"), + loopVariables: ["item"], + nodes: nodes, + emptyNodes: [], + where: `where` + ) + try expect(try node.render(self.context)) == "2132" + } - it("renders the given empty nodes when all items filtered out with where expression") { - let nodes: [NodeType] = [VariableNode(variable: "item")] - let emptyNodes: [NodeType] = [TextNode(text: "empty")] - let parser = TokenParser(tokens: [], environment: Environment()) - let `where` = try parser.compileExpression(components: ["item", "==", "0"], token: .text(value: "", at: .unknown)) - let node = ForNode( - resolvable: Variable("emptyItems"), - loopVariables: ["item"], - nodes: nodes, - emptyNodes: emptyNodes, - where: `where` - ) - try expect(try node.render(self.context)) == "empty" - } - } + it("renders the given empty nodes when all items filtered out with where expression") { + let nodes: [NodeType] = [VariableNode(variable: "item")] + let emptyNodes: [NodeType] = [TextNode(text: "empty")] + let parser = TokenParser(tokens: [], environment: Environment()) + let `where` = try parser.compileExpression(components: ["item", "==", "0"], token: .text(value: "", at: .unknown)) + let node = ForNode( + resolvable: Variable("emptyItems"), + loopVariables: ["item"], + nodes: nodes, + emptyNodes: emptyNodes, + where: `where` + ) + try expect(try node.render(self.context)) == "empty" + } + } - func testArrayOfTuples() { - it("can iterate over all tuple values") { - let template = Template(templateString: """ - {% for first,second,third in tuples %}\ - {{ first }}, {{ second }}, {{ third }} - {% endfor %} - """) - try expect(template.render(self.context)) == """ - 1, 2, 3 - 4, 5, 6 + func testArrayOfTuples() { + it("can iterate over all tuple values") { + let template = Template(templateString: """ + {% for first,second,third in tuples %}\ + {{ first }}, {{ second }}, {{ third }} + {% endfor %} + """) + try expect(template.render(self.context)) == """ + 1, 2, 3 + 4, 5, 6 - """ - } + """ + } - it("can iterate with less number of variables") { - let template = Template(templateString: """ - {% for first,second in tuples %}\ - {{ first }}, {{ second }} - {% endfor %} - """) - try expect(template.render(self.context)) == """ - 1, 2 - 4, 5 + it("can iterate with less number of variables") { + let template = Template(templateString: """ + {% for first,second in tuples %}\ + {{ first }}, {{ second }} + {% endfor %} + """) + try expect(template.render(self.context)) == """ + 1, 2 + 4, 5 - """ - } + """ + } - it("can use _ to skip variables") { - let template = Template(templateString: """ - {% for first,_,third in tuples %}\ - {{ first }}, {{ third }} - {% endfor %} - """) - try expect(template.render(self.context)) == """ - 1, 3 - 4, 6 + it("can use _ to skip variables") { + let template = Template(templateString: """ + {% for first,_,third in tuples %}\ + {{ first }}, {{ third }} + {% endfor %} + """) + try expect(template.render(self.context)) == """ + 1, 3 + 4, 6 - """ - } + """ + } - it("throws when number of variables is more than number of tuple values") { - let template = Template(templateString: """ - {% for key,value,smth in dict %}{% endfor %} - """) - try expect(template.render(self.context)).toThrow() - } - } + it("throws when number of variables is more than number of tuple values") { + let template = Template(templateString: """ + {% for key,value,smth in dict %}{% endfor %} + """) + try expect(template.render(self.context)).toThrow() + } + } - func testIterateDictionary() { - it("can iterate over dictionary") { - let template = Template(templateString: """ - {% for key, value in dict %}\ - {{ key }}: {{ value }},\ - {% endfor %} - """) - try expect(template.render(self.context)) == """ - one: I,two: II, - """ - } + func testIterateDictionary() { + it("can iterate over dictionary") { + let template = Template(templateString: """ + {% for key, value in dict %}\ + {{ key }}: {{ value }},\ + {% endfor %} + """) + try expect(template.render(self.context)) == """ + one: I,two: II, + """ + } - it("renders supports iterating over dictionary") { - let nodes: [NodeType] = [ - VariableNode(variable: "key"), - TextNode(text: ",") - ] - let emptyNodes: [NodeType] = [TextNode(text: "empty")] - let node = ForNode( - resolvable: Variable("dict"), - loopVariables: ["key"], - nodes: nodes, - emptyNodes: emptyNodes - ) + it("renders supports iterating over dictionary") { + let nodes: [NodeType] = [ + VariableNode(variable: "key"), + TextNode(text: ",") + ] + let emptyNodes: [NodeType] = [TextNode(text: "empty")] + let node = ForNode( + resolvable: Variable("dict"), + loopVariables: ["key"], + nodes: nodes, + emptyNodes: emptyNodes + ) - try expect(node.render(self.context)) == """ - one,two, - """ - } + try expect(node.render(self.context)) == """ + one,two, + """ + } - it("renders supports iterating over dictionary with values") { - let nodes: [NodeType] = [ - VariableNode(variable: "key"), - TextNode(text: "="), - VariableNode(variable: "value"), - TextNode(text: ",") - ] - let emptyNodes: [NodeType] = [TextNode(text: "empty")] - let node = ForNode( - resolvable: Variable("dict"), - loopVariables: ["key", "value"], - nodes: nodes, - emptyNodes: emptyNodes - ) + it("renders supports iterating over dictionary with values") { + let nodes: [NodeType] = [ + VariableNode(variable: "key"), + TextNode(text: "="), + VariableNode(variable: "value"), + TextNode(text: ",") + ] + let emptyNodes: [NodeType] = [TextNode(text: "empty")] + let node = ForNode( + resolvable: Variable("dict"), + loopVariables: ["key", "value"], + nodes: nodes, + emptyNodes: emptyNodes + ) - try expect(node.render(self.context)) == """ - one=I,two=II, - """ - } - } + try expect(node.render(self.context)) == """ + one=I,two=II, + """ + } + } - func testIterateUsingMirroring() { - let nodes: [NodeType] = [ - VariableNode(variable: "label"), - TextNode(text: "="), - VariableNode(variable: "value"), - TextNode(text: "\n") - ] - let node = ForNode( - resolvable: Variable("item"), - loopVariables: ["label", "value"], - nodes: nodes, - emptyNodes: [] - ) + func testIterateUsingMirroring() { + let nodes: [NodeType] = [ + VariableNode(variable: "label"), + TextNode(text: "="), + VariableNode(variable: "value"), + TextNode(text: "\n") + ] + let node = ForNode( + resolvable: Variable("item"), + loopVariables: ["label", "value"], + nodes: nodes, + emptyNodes: [] + ) - it("can iterate over struct properties") { - let context = Context(dictionary: [ - "item": MyStruct(string: "abc", number: 123) - ]) - try expect(node.render(context)) == """ - string=abc - number=123 + it("can iterate over struct properties") { + let context = Context(dictionary: [ + "item": MyStruct(string: "abc", number: 123) + ]) + try expect(node.render(context)) == """ + string=abc + number=123 - """ - } + """ + } - it("can iterate tuple items") { - let context = Context(dictionary: [ - "item": (one: 1, two: "dva") - ]) - try expect(node.render(context)) == """ - one=1 - two=dva + it("can iterate tuple items") { + let context = Context(dictionary: [ + "item": (one: 1, two: "dva") + ]) + try expect(node.render(context)) == """ + one=1 + two=dva - """ - } + """ + } - it("can iterate over class properties") { - let context = Context(dictionary: [ - "item": MySubclass("child", "base", 1) - ]) - try expect(node.render(context)) == """ - childString=child - baseString=base - baseInt=1 + it("can iterate over class properties") { + let context = Context(dictionary: [ + "item": MySubclass("child", "base", 1) + ]) + try expect(node.render(context)) == """ + childString=child + baseString=base + baseInt=1 - """ - } - } + """ + } + } - func testIterateRange() { - it("renders a context variable of type CountableClosedRange") { - let context = Context(dictionary: ["range": 1...3]) - let nodes: [NodeType] = [VariableNode(variable: "item")] - let node = ForNode(resolvable: Variable("range"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + func testIterateRange() { + it("renders a context variable of type CountableClosedRange") { + let context = Context(dictionary: ["range": 1...3]) + let nodes: [NodeType] = [VariableNode(variable: "item")] + let node = ForNode(resolvable: Variable("range"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(context)) == "123" - } + try expect(try node.render(context)) == "123" + } - it("renders a context variable of type CountableRange") { - let context = Context(dictionary: ["range": 1..<4]) - let nodes: [NodeType] = [VariableNode(variable: "item")] - let node = ForNode(resolvable: Variable("range"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + it("renders a context variable of type CountableRange") { + let context = Context(dictionary: ["range": 1..<4]) + let nodes: [NodeType] = [VariableNode(variable: "item")] + let node = ForNode(resolvable: Variable("range"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(context)) == "123" - } + try expect(try node.render(context)) == "123" + } - it("can iterate in range of variables") { - let template: Template = "{% for i in 1...j %}{{ i }}{% endfor %}" - try expect(try template.render(Context(dictionary: ["j": 3]))) == "123" - } - } + it("can iterate in range of variables") { + let template: Template = "{% for i in 1...j %}{{ i }}{% endfor %}" + try expect(try template.render(Context(dictionary: ["j": 3]))) == "123" + } + } - func testHandleInvalidInput() throws { - let token = Token.block(value: "for i", at: .unknown) - let parser = TokenParser(tokens: [token], environment: Environment()) - let error = TemplateSyntaxError( - reason: "'for' statements should use the syntax: `for in [where ]`.", - token: token - ) - try expect(try parser.parse()).toThrow(error) - } + func testHandleInvalidInput() throws { + let token = Token.block(value: "for i", at: .unknown) + let parser = TokenParser(tokens: [token], environment: Environment()) + let error = TemplateSyntaxError( + reason: "'for' statements should use the syntax: `for in [where ]`.", + token: token + ) + try expect(try parser.parse()).toThrow(error) + } - func testBreak() { - it("can break from loop") { - let template = Template(templateString: """ - {% for item in items %}\ - {{ item }}{% break %}\ - {% endfor %} - """) - try expect(template.render(self.context)) == """ - 1 - """ - } + func testBreak() { + it("can break from loop") { + let template = Template(templateString: """ + {% for item in items %}\ + {{ item }}{% break %}\ + {% endfor %} + """) + try expect(template.render(self.context)) == """ + 1 + """ + } - it("can break from inner node") { - let template = Template(templateString: """ - {% for item in items %}\ - {{ item }}\ - {% if forloop.first %}<{% break %}>{% endif %}!\ - {% endfor %} - """) - try expect(template.render(self.context)) == """ - 1< - """ - } + it("can break from inner node") { + let template = Template(templateString: """ + {% for item in items %}\ + {{ item }}\ + {% if forloop.first %}<{% break %}>{% endif %}!\ + {% endfor %} + """) + try expect(template.render(self.context)) == """ + 1< + """ + } - it("does not allow break outside loop") { - let template = Template(templateString: "{% for item in items %}{% endfor %}{% break %}") - let error = self.expectedSyntaxError( - token: "break", - template: template, - description: "'break' can be used only inside loop body" - ) - try expect(template.render(self.context)).toThrow(error) - } - } + it("does not allow break outside loop") { + let template = Template(templateString: "{% for item in items %}{% endfor %}{% break %}") + let error = self.expectedSyntaxError( + token: "break", + template: template, + description: "'break' can be used only inside loop body" + ) + try expect(template.render(self.context)).toThrow(error) + } + } - func testBreakNested() { - it("breaks outer loop") { - let template = Template(templateString: """ - {% for item in items %}\ - outer: {{ item }} - {% for item in items %}\ - inner: {{ item }} - {% endfor %}\ - {% break %}\ - {% endfor %} - """) - try expect(template.render(self.context)) == """ - outer: 1 - inner: 1 - inner: 2 - inner: 3 + func testBreakNested() { + it("breaks outer loop") { + let template = Template(templateString: """ + {% for item in items %}\ + outer: {{ item }} + {% for item in items %}\ + inner: {{ item }} + {% endfor %}\ + {% break %}\ + {% endfor %} + """) + try expect(template.render(self.context)) == """ + outer: 1 + inner: 1 + inner: 2 + inner: 3 - """ - } + """ + } - it("breaks inner loop") { - let template = Template(templateString: """ - {% for item in items %}\ - outer: {{ item }} - {% for item in items %}\ - inner: {{ item }} - {% break %}\ - {% endfor %}\ - {% endfor %} - """) - try expect(template.render(self.context)) == """ - outer: 1 - inner: 1 - outer: 2 - inner: 1 - outer: 3 - inner: 1 + it("breaks inner loop") { + let template = Template(templateString: """ + {% for item in items %}\ + outer: {{ item }} + {% for item in items %}\ + inner: {{ item }} + {% break %}\ + {% endfor %}\ + {% endfor %} + """) + try expect(template.render(self.context)) == """ + outer: 1 + inner: 1 + outer: 2 + inner: 1 + outer: 3 + inner: 1 - """ - } - } + """ + } + } - func testBreakLabeled() { - it("breaks labeled loop") { - let template = Template(templateString: """ - {% outer: for item in items %}\ - outer: {{ item }} - {% for item in items %}\ - {% break outer %}\ - inner: {{ item }} - {% endfor %}\ - {% endfor %} - """) - try expect(template.render(self.context)) == """ - outer: 1 + func testBreakLabeled() { + it("breaks labeled loop") { + let template = Template(templateString: """ + {% outer: for item in items %}\ + outer: {{ item }} + {% for item in items %}\ + {% break outer %}\ + inner: {{ item }} + {% endfor %}\ + {% endfor %} + """) + try expect(template.render(self.context)) == """ + outer: 1 - """ - } + """ + } - it("throws when breaking with unknown label") { - let template = Template(templateString: """ - {% outer: for item in items %} - {% break inner %} - {% endfor %} - """) - try expect(template.render(self.context)).toThrow() - } - } + it("throws when breaking with unknown label") { + let template = Template(templateString: """ + {% outer: for item in items %} + {% break inner %} + {% endfor %} + """) + try expect(template.render(self.context)).toThrow() + } + } - func testContinue() { - it("can continue loop") { - let template = Template(templateString: """ - {% for item in items %}\ - {{ item }}{% continue %}!\ - {% endfor %} - """) - try expect(template.render(self.context)) == "123" - } + func testContinue() { + it("can continue loop") { + let template = Template(templateString: """ + {% for item in items %}\ + {{ item }}{% continue %}!\ + {% endfor %} + """) + try expect(template.render(self.context)) == "123" + } - it("can continue from inner node") { - let template = Template(templateString: """ - {% for item in items %}\ - {% if forloop.last %}<{% continue %}>{% endif %}!\ - {{ item }}\ - {% endfor %} - """) - try expect(template.render(self.context)) == "!1!2<" - } + it("can continue from inner node") { + let template = Template(templateString: """ + {% for item in items %}\ + {% if forloop.last %}<{% continue %}>{% endif %}!\ + {{ item }}\ + {% endfor %} + """) + try expect(template.render(self.context)) == "!1!2<" + } - it("does not allow continue outside loop") { - let template = Template(templateString: "{% for item in items %}{% endfor %}{% continue %}") - let error = self.expectedSyntaxError( - token: "continue", - template: template, - description: "'continue' can be used only inside loop body" - ) - try expect(template.render(self.context)).toThrow(error) - } - } + it("does not allow continue outside loop") { + let template = Template(templateString: "{% for item in items %}{% endfor %}{% continue %}") + let error = self.expectedSyntaxError( + token: "continue", + template: template, + description: "'continue' can be used only inside loop body" + ) + try expect(template.render(self.context)).toThrow(error) + } + } - func testContinueNested() { - it("breaks outer loop") { - let template = Template(templateString: """ - {% for item in items %}\ - {% for item in items %}\ - inner: {{ item }}\ - {% endfor %} - {% continue %} - outer: {{ item }} - {% endfor %} - """) - try expect(template.render(self.context)) == """ - inner: 1inner: 2inner: 3 - inner: 1inner: 2inner: 3 - inner: 1inner: 2inner: 3 + func testContinueNested() { + it("breaks outer loop") { + let template = Template(templateString: """ + {% for item in items %}\ + {% for item in items %}\ + inner: {{ item }}\ + {% endfor %} + {% continue %} + outer: {{ item }} + {% endfor %} + """) + try expect(template.render(self.context)) == """ + inner: 1inner: 2inner: 3 + inner: 1inner: 2inner: 3 + inner: 1inner: 2inner: 3 - """ - } + """ + } - it("breaks inner loop") { - let template = Template(templateString: """ - {% for item in items %}\ - {% for item in items %}\ - {% continue %}\ - inner: {{ item }} - {% endfor %}\ - outer: {{ item }} - {% endfor %} - """) - try expect(template.render(self.context)) == """ - outer: 1 - outer: 2 - outer: 3 + it("breaks inner loop") { + let template = Template(templateString: """ + {% for item in items %}\ + {% for item in items %}\ + {% continue %}\ + inner: {{ item }} + {% endfor %}\ + outer: {{ item }} + {% endfor %} + """) + try expect(template.render(self.context)) == """ + outer: 1 + outer: 2 + outer: 3 - """ - } - } + """ + } + } - func testContinueLabeled() { - it("continues labeled loop") { - let template = Template(templateString: """ - {% outer: for item in items %}\ - {% for item in items %}\ - inner: {{ item }} - {% continue outer %}\ - {% endfor %}\ - outer: {{ item }} - {% endfor %} - """) - try expect(template.render(self.context)) == """ - inner: 1 - inner: 1 - inner: 1 + func testContinueLabeled() { + it("continues labeled loop") { + let template = Template(templateString: """ + {% outer: for item in items %}\ + {% for item in items %}\ + inner: {{ item }} + {% continue outer %}\ + {% endfor %}\ + outer: {{ item }} + {% endfor %} + """) + try expect(template.render(self.context)) == """ + inner: 1 + inner: 1 + inner: 1 - """ - } + """ + } - it("throws when continuing with unknown label") { - let template = Template(templateString: """ - {% outer: for item in items %} - {% continue inner %} - {% endfor %} - """) - try expect(template.render(self.context)).toThrow() - } - } + it("throws when continuing with unknown label") { + let template = Template(templateString: """ + {% outer: for item in items %} + {% continue inner %} + {% endfor %} + """) + try expect(template.render(self.context)).toThrow() + } + } - func testAccessLabeled() { - it("can access labeled outer loop context from inner loop") { - let template = Template(templateString: """ - {% outer: for item in 1...2 %}\ - {% for item in items %}\ - {{ forloop.counter }}-{{ forloop.outer.counter }},\ - {% endfor %}---\ - {% endfor %} - """) - try expect(template.render(self.context)) == """ - 1-1,2-1,3-1,---1-2,2-2,3-2,--- - """ - } + func testAccessLabeled() { + it("can access labeled outer loop context from inner loop") { + let template = Template(templateString: """ + {% outer: for item in 1...2 %}\ + {% for item in items %}\ + {{ forloop.counter }}-{{ forloop.outer.counter }},\ + {% endfor %}---\ + {% endfor %} + """) + try expect(template.render(self.context)) == """ + 1-1,2-1,3-1,---1-2,2-2,3-2,--- + """ + } - it("can access labeled outer loop from double inner loop") { - let template = Template(templateString: """ - {% outer: for item in 1...2 %}{% for item in 1...2 %}\ - {% for item in items %}\ - {{ forloop.counter }}-{{ forloop.outer.counter }},\ - {% endfor %}---{% endfor %} - {% endfor %} - """) - try expect(template.render(self.context)) == """ - 1-1,2-1,3-1,---1-1,2-1,3-1,--- - 1-2,2-2,3-2,---1-2,2-2,3-2,--- + it("can access labeled outer loop from double inner loop") { + let template = Template(templateString: """ + {% outer: for item in 1...2 %}{% for item in 1...2 %}\ + {% for item in items %}\ + {{ forloop.counter }}-{{ forloop.outer.counter }},\ + {% endfor %}---{% endfor %} + {% endfor %} + """) + try expect(template.render(self.context)) == """ + 1-1,2-1,3-1,---1-1,2-1,3-1,--- + 1-2,2-2,3-2,---1-2,2-2,3-2,--- - """ - } + """ + } - it("can access two labeled outer loop contexts from inner loop") { - let template = Template(templateString: """ - {% outer1: for item in 1...2 %}{% outer2: for item in 1...2 %}\ - {% for item in items %}\ - {{ forloop.counter }}-{{ forloop.outer2.counter }}-{{ forloop.outer1.counter }},\ - {% endfor %}---{% endfor %} - {% endfor %} - """) - try expect(template.render(self.context)) == """ - 1-1-1,2-1-1,3-1-1,---1-2-1,2-2-1,3-2-1,--- - 1-1-2,2-1-2,3-1-2,---1-2-2,2-2-2,3-2-2,--- + it("can access two labeled outer loop contexts from inner loop") { + let template = Template(templateString: """ + {% outer1: for item in 1...2 %}{% outer2: for item in 1...2 %}\ + {% for item in items %}\ + {{ forloop.counter }}-{{ forloop.outer2.counter }}-{{ forloop.outer1.counter }},\ + {% endfor %}---{% endfor %} + {% endfor %} + """) + try expect(template.render(self.context)) == """ + 1-1-1,2-1-1,3-1-1,---1-2-1,2-2-1,3-2-1,--- + 1-1-2,2-1-2,3-1-2,---1-2-2,2-2-2,3-2-2,--- - """ - } - } + """ + } + } } // MARK: - Helpers private struct MyStruct { - let string: String - let number: Int + let string: String + let number: Int } private struct Article { - let title: String - let author: String + let title: String + let author: String } private class MyClass { - var baseString: String - var baseInt: Int - init(_ string: String, _ int: Int) { - baseString = string - baseInt = int - } + var baseString: String + var baseInt: Int + init(_ string: String, _ int: Int) { + baseString = string + baseInt = int + } } private class MySubclass: MyClass { - var childString: String - init(_ childString: String, _ string: String, _ int: Int) { - self.childString = childString - super.init(string, int) - } + var childString: String + init(_ childString: String, _ string: String, _ int: Int) { + self.childString = childString + super.init(string, int) + } } diff --git a/Tests/StencilTests/Helpers.swift b/Tests/StencilTests/Helpers.swift index cf6dd2e..f72e54c 100644 --- a/Tests/StencilTests/Helpers.swift +++ b/Tests/StencilTests/Helpers.swift @@ -4,60 +4,60 @@ import Spectre import XCTest extension Expectation { - @discardableResult - func toThrow() throws -> E { - var thrownError: Error? + @discardableResult + func toThrow() throws -> E { + var thrownError: Error? - do { - _ = try expression() - } catch { - thrownError = error - } + do { + _ = try expression() + } catch { + thrownError = error + } - if let thrownError = thrownError { - if let thrownError = thrownError as? E { - return thrownError - } else { - throw failure("\(thrownError) is not \(T.self)") - } - } else { - throw failure("expression did not throw an error") - } - } + if let thrownError = thrownError { + if let thrownError = thrownError as? E { + return thrownError + } else { + throw failure("\(thrownError) is not \(T.self)") + } + } else { + throw failure("expression did not throw an error") + } + } } extension XCTestCase { - func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { - guard let range = template.templateString.range(of: token) else { - fatalError("Can't find '\(token)' in '\(template)'") - } - let lexer = Lexer(templateString: template.templateString) - let location = lexer.rangeLocation(range) - let sourceMap = SourceMap(filename: template.name, location: location) - let token = Token.block(value: token, at: sourceMap) - return TemplateSyntaxError(reason: description, token: token, stackTrace: []) - } + func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { + guard let range = template.templateString.range(of: token) else { + fatalError("Can't find '\(token)' in '\(template)'") + } + let lexer = Lexer(templateString: template.templateString) + let location = lexer.rangeLocation(range) + let sourceMap = SourceMap(filename: template.name, location: location) + let token = Token.block(value: token, at: sourceMap) + return TemplateSyntaxError(reason: description, token: token, stackTrace: []) + } } // MARK: - Test Types class ExampleLoader: Loader { - func loadTemplate(name: String, environment: Environment) throws -> Template { - if name == "example.html" { - return Template(templateString: "Hello World!", environment: environment, name: name) - } + func loadTemplate(name: String, environment: Environment) throws -> Template { + if name == "example.html" { + return Template(templateString: "Hello World!", environment: environment, name: name) + } - throw TemplateDoesNotExist(templateNames: [name], loader: self) - } + throw TemplateDoesNotExist(templateNames: [name], loader: self) + } } class ErrorNode: NodeType { - let token: Token? - init(token: Token? = nil) { - self.token = token - } + let token: Token? + init(token: Token? = nil) { + self.token = token + } - func render(_ context: Context) throws -> String { - throw TemplateSyntaxError("Custom Error") - } + func render(_ context: Context) throws -> String { + throw TemplateSyntaxError("Custom Error") + } } diff --git a/Tests/StencilTests/IfNodeSpec.swift b/Tests/StencilTests/IfNodeSpec.swift index 8107b9c..32ad579 100644 --- a/Tests/StencilTests/IfNodeSpec.swift +++ b/Tests/StencilTests/IfNodeSpec.swift @@ -3,288 +3,288 @@ import Spectre import XCTest final class IfNodeTests: XCTestCase { - func testParseIf() { - it("can parse an if block") { - let tokens: [Token] = [ - .block(value: "if value", at: .unknown), - .text(value: "true", at: .unknown), - .block(value: "endif", at: .unknown) - ] + func testParseIf() { + it("can parse an if block") { + let tokens: [Token] = [ + .block(value: "if value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "endif", at: .unknown) + ] - let parser = TokenParser(tokens: tokens, environment: Environment()) - let nodes = try parser.parse() - let node = nodes.first as? IfNode + let parser = TokenParser(tokens: tokens, environment: Environment()) + let nodes = try parser.parse() + let node = nodes.first as? IfNode - let conditions = node?.conditions - try expect(conditions?.count) == 1 - try expect(conditions?[0].nodes.count) == 1 - let trueNode = conditions?[0].nodes.first as? TextNode - try expect(trueNode?.text) == "true" - } + let conditions = node?.conditions + try expect(conditions?.count) == 1 + try expect(conditions?[0].nodes.count) == 1 + let trueNode = conditions?[0].nodes.first as? TextNode + try expect(trueNode?.text) == "true" + } - it("can parse an if with complex expression") { - let tokens: [Token] = [ - .block(value: """ - if value == \"test\" and (not name or not (name and surname) or( some )and other ) - """, at: .unknown), - .text(value: "true", at: .unknown), - .block(value: "endif", at: .unknown) - ] + it("can parse an if with complex expression") { + let tokens: [Token] = [ + .block(value: """ + if value == \"test\" and (not name or not (name and surname) or( some )and other ) + """, at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "endif", at: .unknown) + ] - let parser = TokenParser(tokens: tokens, environment: Environment()) - let nodes = try parser.parse() - try expect(nodes.first is IfNode).beTrue() - } - } + let parser = TokenParser(tokens: tokens, environment: Environment()) + let nodes = try parser.parse() + try expect(nodes.first is IfNode).beTrue() + } + } - func testParseIfWithElse() throws { - let tokens: [Token] = [ - .block(value: "if value", at: .unknown), - .text(value: "true", at: .unknown), - .block(value: "else", at: .unknown), - .text(value: "false", at: .unknown), - .block(value: "endif", at: .unknown) - ] + func testParseIfWithElse() throws { + let tokens: [Token] = [ + .block(value: "if value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "else", at: .unknown), + .text(value: "false", at: .unknown), + .block(value: "endif", at: .unknown) + ] - let parser = TokenParser(tokens: tokens, environment: Environment()) - let nodes = try parser.parse() - let node = nodes.first as? IfNode + let parser = TokenParser(tokens: tokens, environment: Environment()) + let nodes = try parser.parse() + let node = nodes.first as? IfNode - let conditions = node?.conditions - try expect(conditions?.count) == 2 + let conditions = node?.conditions + try expect(conditions?.count) == 2 - try expect(conditions?[0].nodes.count) == 1 - let trueNode = conditions?[0].nodes.first as? TextNode - try expect(trueNode?.text) == "true" + try expect(conditions?[0].nodes.count) == 1 + let trueNode = conditions?[0].nodes.first as? TextNode + try expect(trueNode?.text) == "true" - try expect(conditions?[1].nodes.count) == 1 - let falseNode = conditions?[1].nodes.first as? TextNode - try expect(falseNode?.text) == "false" - } + try expect(conditions?[1].nodes.count) == 1 + let falseNode = conditions?[1].nodes.first as? TextNode + try expect(falseNode?.text) == "false" + } - func testParseIfWithElif() throws { - let tokens: [Token] = [ - .block(value: "if value", at: .unknown), - .text(value: "true", at: .unknown), - .block(value: "elif something", at: .unknown), - .text(value: "some", at: .unknown), - .block(value: "else", at: .unknown), - .text(value: "false", at: .unknown), - .block(value: "endif", at: .unknown) - ] + func testParseIfWithElif() throws { + let tokens: [Token] = [ + .block(value: "if value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "elif something", at: .unknown), + .text(value: "some", at: .unknown), + .block(value: "else", at: .unknown), + .text(value: "false", at: .unknown), + .block(value: "endif", at: .unknown) + ] - let parser = TokenParser(tokens: tokens, environment: Environment()) - let nodes = try parser.parse() - let node = nodes.first as? IfNode + let parser = TokenParser(tokens: tokens, environment: Environment()) + let nodes = try parser.parse() + let node = nodes.first as? IfNode - let conditions = node?.conditions - try expect(conditions?.count) == 3 + let conditions = node?.conditions + try expect(conditions?.count) == 3 - try expect(conditions?[0].nodes.count) == 1 - let trueNode = conditions?[0].nodes.first as? TextNode - try expect(trueNode?.text) == "true" + try expect(conditions?[0].nodes.count) == 1 + let trueNode = conditions?[0].nodes.first as? TextNode + try expect(trueNode?.text) == "true" - try expect(conditions?[1].nodes.count) == 1 - let elifNode = conditions?[1].nodes.first as? TextNode - try expect(elifNode?.text) == "some" + try expect(conditions?[1].nodes.count) == 1 + let elifNode = conditions?[1].nodes.first as? TextNode + try expect(elifNode?.text) == "some" - try expect(conditions?[2].nodes.count) == 1 - let falseNode = conditions?[2].nodes.first as? TextNode - try expect(falseNode?.text) == "false" - } + try expect(conditions?[2].nodes.count) == 1 + let falseNode = conditions?[2].nodes.first as? TextNode + try expect(falseNode?.text) == "false" + } - func testParseIfWithElifWithoutElse() throws { - let tokens: [Token] = [ - .block(value: "if value", at: .unknown), - .text(value: "true", at: .unknown), - .block(value: "elif something", at: .unknown), - .text(value: "some", at: .unknown), - .block(value: "endif", at: .unknown) - ] + func testParseIfWithElifWithoutElse() throws { + let tokens: [Token] = [ + .block(value: "if value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "elif something", at: .unknown), + .text(value: "some", at: .unknown), + .block(value: "endif", at: .unknown) + ] - let parser = TokenParser(tokens: tokens, environment: Environment()) - let nodes = try parser.parse() - let node = nodes.first as? IfNode + let parser = TokenParser(tokens: tokens, environment: Environment()) + let nodes = try parser.parse() + let node = nodes.first as? IfNode - let conditions = node?.conditions - try expect(conditions?.count) == 2 + let conditions = node?.conditions + try expect(conditions?.count) == 2 - try expect(conditions?[0].nodes.count) == 1 - let trueNode = conditions?[0].nodes.first as? TextNode - try expect(trueNode?.text) == "true" + try expect(conditions?[0].nodes.count) == 1 + let trueNode = conditions?[0].nodes.first as? TextNode + try expect(trueNode?.text) == "true" - try expect(conditions?[1].nodes.count) == 1 - let elifNode = conditions?[1].nodes.first as? TextNode - try expect(elifNode?.text) == "some" - } + try expect(conditions?[1].nodes.count) == 1 + let elifNode = conditions?[1].nodes.first as? TextNode + try expect(elifNode?.text) == "some" + } - func testParseMultipleElif() throws { - let tokens: [Token] = [ - .block(value: "if value", at: .unknown), - .text(value: "true", at: .unknown), - .block(value: "elif something1", at: .unknown), - .text(value: "some1", at: .unknown), - .block(value: "elif something2", at: .unknown), - .text(value: "some2", at: .unknown), - .block(value: "else", at: .unknown), - .text(value: "false", at: .unknown), - .block(value: "endif", at: .unknown) - ] + func testParseMultipleElif() throws { + let tokens: [Token] = [ + .block(value: "if value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "elif something1", at: .unknown), + .text(value: "some1", at: .unknown), + .block(value: "elif something2", at: .unknown), + .text(value: "some2", at: .unknown), + .block(value: "else", at: .unknown), + .text(value: "false", at: .unknown), + .block(value: "endif", at: .unknown) + ] - let parser = TokenParser(tokens: tokens, environment: Environment()) - let nodes = try parser.parse() - let node = nodes.first as? IfNode + let parser = TokenParser(tokens: tokens, environment: Environment()) + let nodes = try parser.parse() + let node = nodes.first as? IfNode - let conditions = node?.conditions - try expect(conditions?.count) == 4 + let conditions = node?.conditions + try expect(conditions?.count) == 4 - try expect(conditions?[0].nodes.count) == 1 - let trueNode = conditions?[0].nodes.first as? TextNode - try expect(trueNode?.text) == "true" + try expect(conditions?[0].nodes.count) == 1 + let trueNode = conditions?[0].nodes.first as? TextNode + try expect(trueNode?.text) == "true" - try expect(conditions?[1].nodes.count) == 1 - let elifNode = conditions?[1].nodes.first as? TextNode - try expect(elifNode?.text) == "some1" + try expect(conditions?[1].nodes.count) == 1 + let elifNode = conditions?[1].nodes.first as? TextNode + try expect(elifNode?.text) == "some1" - try expect(conditions?[2].nodes.count) == 1 - let elif2Node = conditions?[2].nodes.first as? TextNode - try expect(elif2Node?.text) == "some2" + try expect(conditions?[2].nodes.count) == 1 + let elif2Node = conditions?[2].nodes.first as? TextNode + try expect(elif2Node?.text) == "some2" - try expect(conditions?[3].nodes.count) == 1 - let falseNode = conditions?[3].nodes.first as? TextNode - try expect(falseNode?.text) == "false" - } + try expect(conditions?[3].nodes.count) == 1 + let falseNode = conditions?[3].nodes.first as? TextNode + try expect(falseNode?.text) == "false" + } - func testParseIfnot() throws { - let tokens: [Token] = [ - .block(value: "ifnot value", at: .unknown), - .text(value: "false", at: .unknown), - .block(value: "else", at: .unknown), - .text(value: "true", at: .unknown), - .block(value: "endif", at: .unknown) - ] + func testParseIfnot() throws { + let tokens: [Token] = [ + .block(value: "ifnot value", at: .unknown), + .text(value: "false", at: .unknown), + .block(value: "else", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "endif", at: .unknown) + ] - let parser = TokenParser(tokens: tokens, environment: Environment()) - let nodes = try parser.parse() - let node = nodes.first as? IfNode - let conditions = node?.conditions - try expect(conditions?.count) == 2 + let parser = TokenParser(tokens: tokens, environment: Environment()) + let nodes = try parser.parse() + let node = nodes.first as? IfNode + let conditions = node?.conditions + try expect(conditions?.count) == 2 - try expect(conditions?[0].nodes.count) == 1 - let trueNode = conditions?[0].nodes.first as? TextNode - try expect(trueNode?.text) == "true" + try expect(conditions?[0].nodes.count) == 1 + let trueNode = conditions?[0].nodes.first as? TextNode + try expect(trueNode?.text) == "true" - try expect(conditions?[1].nodes.count) == 1 - let falseNode = conditions?[1].nodes.first as? TextNode - try expect(falseNode?.text) == "false" - } + try expect(conditions?[1].nodes.count) == 1 + let falseNode = conditions?[1].nodes.first as? TextNode + try expect(falseNode?.text) == "false" + } - func testParsingErrors() { - it("throws an error when parsing an if block without an endif") { - let tokens: [Token] = [.block(value: "if value", at: .unknown)] + func testParsingErrors() { + it("throws an error when parsing an if block without an endif") { + let tokens: [Token] = [.block(value: "if value", at: .unknown)] - let parser = TokenParser(tokens: tokens, environment: Environment()) - let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first) - try expect(try parser.parse()).toThrow(error) - } + let parser = TokenParser(tokens: tokens, environment: Environment()) + let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first) + try expect(try parser.parse()).toThrow(error) + } - it("throws an error when parsing an ifnot without an endif") { - let tokens: [Token] = [.block(value: "ifnot value", at: .unknown)] + it("throws an error when parsing an ifnot without an endif") { + let tokens: [Token] = [.block(value: "ifnot value", at: .unknown)] - let parser = TokenParser(tokens: tokens, environment: Environment()) - let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first) - try expect(try parser.parse()).toThrow(error) - } - } + let parser = TokenParser(tokens: tokens, environment: Environment()) + let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first) + try expect(try parser.parse()).toThrow(error) + } + } - func testRendering() { - it("renders a true expression") { - let node = IfNode(conditions: [ - IfCondition(expression: VariableExpression(variable: Variable("true")), nodes: [TextNode(text: "1")]), - IfCondition(expression: VariableExpression(variable: Variable("true")), nodes: [TextNode(text: "2")]), - IfCondition(expression: nil, nodes: [TextNode(text: "3")]) - ]) + func testRendering() { + it("renders a true expression") { + let node = IfNode(conditions: [ + IfCondition(expression: VariableExpression(variable: Variable("true")), nodes: [TextNode(text: "1")]), + IfCondition(expression: VariableExpression(variable: Variable("true")), nodes: [TextNode(text: "2")]), + IfCondition(expression: nil, nodes: [TextNode(text: "3")]) + ]) - try expect(try node.render(Context())) == "1" - } + try expect(try node.render(Context())) == "1" + } - it("renders the first true expression") { - let node = IfNode(conditions: [ - IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "1")]), - IfCondition(expression: VariableExpression(variable: Variable("true")), nodes: [TextNode(text: "2")]), - IfCondition(expression: nil, nodes: [TextNode(text: "3")]) - ]) + it("renders the first true expression") { + let node = IfNode(conditions: [ + IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "1")]), + IfCondition(expression: VariableExpression(variable: Variable("true")), nodes: [TextNode(text: "2")]), + IfCondition(expression: nil, nodes: [TextNode(text: "3")]) + ]) - try expect(try node.render(Context())) == "2" - } + try expect(try node.render(Context())) == "2" + } - it("renders the empty expression when other conditions are falsy") { - let node = IfNode(conditions: [ - IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "1")]), - IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "2")]), - IfCondition(expression: nil, nodes: [TextNode(text: "3")]) - ]) + it("renders the empty expression when other conditions are falsy") { + let node = IfNode(conditions: [ + IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "1")]), + IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "2")]), + IfCondition(expression: nil, nodes: [TextNode(text: "3")]) + ]) - try expect(try node.render(Context())) == "3" - } + try expect(try node.render(Context())) == "3" + } - it("renders empty when no truthy conditions") { - let node = IfNode(conditions: [ - IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "1")]), - IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "2")]) - ]) + it("renders empty when no truthy conditions") { + let node = IfNode(conditions: [ + IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "1")]), + IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "2")]) + ]) - try expect(try node.render(Context())) == "" - } - } + try expect(try node.render(Context())) == "" + } + } - func testSupportVariableFilters() throws { - let tokens: [Token] = [ - .block(value: "if value|uppercase == \"TEST\"", at: .unknown), - .text(value: "true", at: .unknown), - .block(value: "endif", at: .unknown) - ] + func testSupportVariableFilters() throws { + let tokens: [Token] = [ + .block(value: "if value|uppercase == \"TEST\"", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "endif", at: .unknown) + ] - let parser = TokenParser(tokens: tokens, environment: Environment()) - let nodes = try parser.parse() + let parser = TokenParser(tokens: tokens, environment: Environment()) + let nodes = try parser.parse() - let result = try renderNodes(nodes, Context(dictionary: ["value": "test"])) - try expect(result) == "true" - } + let result = try renderNodes(nodes, Context(dictionary: ["value": "test"])) + try expect(result) == "true" + } - func testEvaluatesNilAsFalse() throws { - let tokens: [Token] = [ - .block(value: "if instance.value", at: .unknown), - .text(value: "true", at: .unknown), - .block(value: "endif", at: .unknown) - ] + func testEvaluatesNilAsFalse() throws { + let tokens: [Token] = [ + .block(value: "if instance.value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "endif", at: .unknown) + ] - let parser = TokenParser(tokens: tokens, environment: Environment()) - let nodes = try parser.parse() + let parser = TokenParser(tokens: tokens, environment: Environment()) + let nodes = try parser.parse() - let result = try renderNodes(nodes, Context(dictionary: ["instance": SomeType()])) - try expect(result) == "" - } + let result = try renderNodes(nodes, Context(dictionary: ["instance": SomeType()])) + try expect(result) == "" + } - func testSupportsRangeVariables() throws { - let tokens: [Token] = [ - .block(value: "if value in 1...3", at: .unknown), - .text(value: "true", at: .unknown), - .block(value: "else", at: .unknown), - .text(value: "false", at: .unknown), - .block(value: "endif", at: .unknown) - ] + func testSupportsRangeVariables() throws { + let tokens: [Token] = [ + .block(value: "if value in 1...3", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "else", at: .unknown), + .text(value: "false", at: .unknown), + .block(value: "endif", at: .unknown) + ] - let parser = TokenParser(tokens: tokens, environment: Environment()) - let nodes = try parser.parse() + let parser = TokenParser(tokens: tokens, environment: Environment()) + let nodes = try parser.parse() - try expect(renderNodes(nodes, Context(dictionary: ["value": 3]))) == "true" - try expect(renderNodes(nodes, Context(dictionary: ["value": 4]))) == "false" - } + try expect(renderNodes(nodes, Context(dictionary: ["value": 3]))) == "true" + try expect(renderNodes(nodes, Context(dictionary: ["value": 4]))) == "false" + } } // MARK: - Helpers private struct SomeType { - let value: String? = nil + let value: String? = nil } diff --git a/Tests/StencilTests/IncludeSpec.swift b/Tests/StencilTests/IncludeSpec.swift index 7568a69..efa2ae3 100644 --- a/Tests/StencilTests/IncludeSpec.swift +++ b/Tests/StencilTests/IncludeSpec.swift @@ -4,69 +4,69 @@ import Spectre import XCTest final class IncludeTests: XCTestCase { - private let path = Path(#file as String)! / ".." / "fixtures" - private lazy var loader = FileSystemLoader(paths: [path]) - private lazy var environment = Environment(loader: loader) + private let path = Path(#file as String)! / ".." / "fixtures" + private lazy var loader = FileSystemLoader(paths: [path]) + private lazy var environment = Environment(loader: loader) - func testParsing() { - it("throws an error when no template is given") { - let tokens: [Token] = [ .block(value: "include", at: .unknown) ] - let parser = TokenParser(tokens: tokens, environment: Environment()) + func testParsing() { + it("throws an error when no template is given") { + let tokens: [Token] = [ .block(value: "include", at: .unknown) ] + let parser = TokenParser(tokens: tokens, environment: Environment()) - let error = TemplateSyntaxError(reason: """ - 'include' tag requires one argument, the template file to be included. \ - A second optional argument can be used to specify the context that will \ - be passed to the included file - """, token: tokens.first) - try expect(try parser.parse()).toThrow(error) - } + let error = TemplateSyntaxError(reason: """ + 'include' tag requires one argument, the template file to be included. \ + A second optional argument can be used to specify the context that will \ + be passed to the included file + """, token: tokens.first) + try expect(try parser.parse()).toThrow(error) + } - it("can parse a valid include block") { - let tokens: [Token] = [ .block(value: "include \"test.html\"", at: .unknown) ] - let parser = TokenParser(tokens: tokens, environment: Environment()) + it("can parse a valid include block") { + let tokens: [Token] = [ .block(value: "include \"test.html\"", at: .unknown) ] + let parser = TokenParser(tokens: tokens, environment: Environment()) - let nodes = try parser.parse() - let node = nodes.first as? IncludeNode - try expect(nodes.count) == 1 - try expect(node?.templateName) == Variable("\"test.html\"") - } - } + let nodes = try parser.parse() + let node = nodes.first as? IncludeNode + try expect(nodes.count) == 1 + try expect(node?.templateName) == Variable("\"test.html\"") + } + } - func testRendering() { - it("throws an error when rendering without a loader") { - let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown)) + func testRendering() { + it("throws an error when rendering without a loader") { + let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown)) - do { - _ = try node.render(Context()) - } catch { - try expect("\(error)") == "Template named `test.html` does not exist. No loaders found" - } - } + do { + _ = try node.render(Context()) + } catch { + try expect("\(error)") == "Template named `test.html` does not exist. No loaders found" + } + } - it("throws an error when it cannot find the included template") { - let node = IncludeNode(templateName: Variable("\"unknown.html\""), token: .block(value: "", at: .unknown)) + it("throws an error when it cannot find the included template") { + let node = IncludeNode(templateName: Variable("\"unknown.html\""), token: .block(value: "", at: .unknown)) - do { - _ = try node.render(Context(environment: self.environment)) - } catch { - try expect("\(error)".hasPrefix("Template named `unknown.html` does not exist in loader")).to.beTrue() - } - } + do { + _ = try node.render(Context(environment: self.environment)) + } catch { + try expect("\(error)".hasPrefix("Template named `unknown.html` does not exist in loader")).to.beTrue() + } + } - it("successfully renders a found included template") { - let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown)) - let context = Context(dictionary: ["target": "World"], environment: self.environment) - let value = try node.render(context) - try expect(value) == "Hello World!" - } + it("successfully renders a found included template") { + let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown)) + let context = Context(dictionary: ["target": "World"], environment: self.environment) + let value = try node.render(context) + try expect(value) == "Hello World!" + } - it("successfully passes context") { - let template = Template(templateString: """ - {% include "test.html" child %} - """) - let context = Context(dictionary: ["child": ["target": "World"]], environment: self.environment) - let value = try template.render(context) - try expect(value) == "Hello World!" - } - } + it("successfully passes context") { + let template = Template(templateString: """ + {% include "test.html" child %} + """) + let context = Context(dictionary: ["child": ["target": "World"]], environment: self.environment) + let value = try template.render(context) + try expect(value) == "Hello World!" + } + } } diff --git a/Tests/StencilTests/InheritanceSpec.swift b/Tests/StencilTests/InheritanceSpec.swift index fafa985..26e9f6c 100644 --- a/Tests/StencilTests/InheritanceSpec.swift +++ b/Tests/StencilTests/InheritanceSpec.swift @@ -4,70 +4,70 @@ import Stencil import XCTest final class InheritanceTests: XCTestCase { - private let path = Path(#file as String)! / ".." / "fixtures" - private lazy var loader = FileSystemLoader(paths: [path]) - private lazy var environment = Environment(loader: loader) + private let path = Path(#file as String)! / ".." / "fixtures" + private lazy var loader = FileSystemLoader(paths: [path]) + private lazy var environment = Environment(loader: loader) - func testInheritance() { - it("can inherit from another template") { - let template = try self.environment.loadTemplate(name: "child.html") - try expect(try template.render()) == """ - Super_Header Child_Header - Child_Body - """ - } + func testInheritance() { + it("can inherit from another template") { + let template = try self.environment.loadTemplate(name: "child.html") + try expect(try template.render()) == """ + Super_Header Child_Header + Child_Body + """ + } - it("can inherit from another template inheriting from another template") { - let template = try self.environment.loadTemplate(name: "child-child.html") - try expect(try template.render()) == """ - Super_Header Child_Header Child_Child_Header - Child_Body - """ - } + it("can inherit from another template inheriting from another template") { + let template = try self.environment.loadTemplate(name: "child-child.html") + try expect(try template.render()) == """ + Super_Header Child_Header Child_Child_Header + Child_Body + """ + } - it("can inherit from a template that calls a super block") { - let template = try self.environment.loadTemplate(name: "child-super.html") - try expect(try template.render()) == """ - Header - Child_Body - """ - } + it("can inherit from a template that calls a super block") { + let template = try self.environment.loadTemplate(name: "child-super.html") + try expect(try template.render()) == """ + Header + Child_Body + """ + } - it("can render block.super in if tag") { - let template = try self.environment.loadTemplate(name: "if-block-child.html") + it("can render block.super in if tag") { + let template = try self.environment.loadTemplate(name: "if-block-child.html") - try expect(try template.render(["sort": "new"])) == """ - Title - Nieuwste spellen + try expect(try template.render(["sort": "new"])) == """ + Title - Nieuwste spellen - """ + """ - try expect(try template.render(["sort": "upcoming"])) == """ - Title - Binnenkort op de agenda + try expect(try template.render(["sort": "upcoming"])) == """ + Title - Binnenkort op de agenda - """ + """ - try expect(try template.render(["sort": "near-me"])) == """ - Title - In mijn buurt + try expect(try template.render(["sort": "near-me"])) == """ + Title - In mijn buurt - """ - } - } + """ + } + } - func testInheritanceCache() { - it("can call block twice") { - let template: Template = "{% block repeat %}Block{% endblock %}{{ block.repeat }}" - try expect(try template.render()) == "BlockBlock" - } + func testInheritanceCache() { + it("can call block twice") { + let template: Template = "{% block repeat %}Block{% endblock %}{{ block.repeat }}" + try expect(try template.render()) == "BlockBlock" + } - it("renders child content when calling block twice in base template") { - let template = try self.environment.loadTemplate(name: "child-repeat.html") - try expect(try template.render()) == """ - Super_Header Child_Header - Child_Body - Repeat - Super_Header Child_Header - Child_Body - """ - } - } + it("renders child content when calling block twice in base template") { + let template = try self.environment.loadTemplate(name: "child-repeat.html") + try expect(try template.render()) == """ + Super_Header Child_Header + Child_Body + Repeat + Super_Header Child_Header + Child_Body + """ + } + } } diff --git a/Tests/StencilTests/LexerSpec.swift b/Tests/StencilTests/LexerSpec.swift index f1e5438..5cf75ba 100644 --- a/Tests/StencilTests/LexerSpec.swift +++ b/Tests/StencilTests/LexerSpec.swift @@ -4,86 +4,86 @@ import Spectre import XCTest final class LexerTests: XCTestCase { - func testText() throws { - let lexer = Lexer(templateString: "Hello World") - let tokens = lexer.tokenize() + func testText() throws { + let lexer = Lexer(templateString: "Hello World") + let tokens = lexer.tokenize() - try expect(tokens.count) == 1 - try expect(tokens.first) == .text(value: "Hello World", at: makeSourceMap("Hello World", for: lexer)) - } + try expect(tokens.count) == 1 + try expect(tokens.first) == .text(value: "Hello World", at: makeSourceMap("Hello World", for: lexer)) + } - func testComment() throws { - let lexer = Lexer(templateString: "{# Comment #}") - let tokens = lexer.tokenize() + func testComment() throws { + let lexer = Lexer(templateString: "{# Comment #}") + let tokens = lexer.tokenize() - try expect(tokens.count) == 1 - try expect(tokens.first) == .comment(value: "Comment", at: makeSourceMap("Comment", for: lexer)) - } + try expect(tokens.count) == 1 + try expect(tokens.first) == .comment(value: "Comment", at: makeSourceMap("Comment", for: lexer)) + } - func testVariable() throws { - let lexer = Lexer(templateString: "{{ Variable }}") - let tokens = lexer.tokenize() + func testVariable() throws { + let lexer = Lexer(templateString: "{{ Variable }}") + let tokens = lexer.tokenize() - try expect(tokens.count) == 1 - try expect(tokens.first) == .variable(value: "Variable", at: makeSourceMap("Variable", for: lexer)) - } + try expect(tokens.count) == 1 + try expect(tokens.first) == .variable(value: "Variable", at: makeSourceMap("Variable", for: lexer)) + } - func testTokenWithoutSpaces() throws { - let lexer = Lexer(templateString: "{{Variable}}") - let tokens = lexer.tokenize() + func testTokenWithoutSpaces() throws { + let lexer = Lexer(templateString: "{{Variable}}") + let tokens = lexer.tokenize() - try expect(tokens.count) == 1 - try expect(tokens.first) == .variable(value: "Variable", at: makeSourceMap("Variable", for: lexer)) - } + try expect(tokens.count) == 1 + try expect(tokens.first) == .variable(value: "Variable", at: makeSourceMap("Variable", for: lexer)) + } - func testUnclosedTag() throws { - let templateString = "{{ thing" - let lexer = Lexer(templateString: templateString) - let tokens = lexer.tokenize() + func testUnclosedTag() throws { + let templateString = "{{ thing" + let lexer = Lexer(templateString: templateString) + let tokens = lexer.tokenize() - try expect(tokens.count) == 1 - try expect(tokens.first) == .text(value: "", at: makeSourceMap("{{ thing", for: lexer)) - } + try expect(tokens.count) == 1 + try expect(tokens.first) == .text(value: "", at: makeSourceMap("{{ thing", for: lexer)) + } - func testContentMixture() throws { - let templateString = "My name is {{ myname }}." - let lexer = Lexer(templateString: templateString) - let tokens = lexer.tokenize() + func testContentMixture() throws { + let templateString = "My name is {{ myname }}." + let lexer = Lexer(templateString: templateString) + let tokens = lexer.tokenize() - try expect(tokens.count) == 3 - try expect(tokens[0]) == .text(value: "My name is ", at: makeSourceMap("My name is ", for: lexer)) - try expect(tokens[1]) == .variable(value: "myname", at: makeSourceMap("myname", for: lexer)) - try expect(tokens[2]) == .text(value: ".", at: makeSourceMap(".", for: lexer)) - } + try expect(tokens.count) == 3 + try expect(tokens[0]) == .text(value: "My name is ", at: makeSourceMap("My name is ", for: lexer)) + try expect(tokens[1]) == .variable(value: "myname", at: makeSourceMap("myname", for: lexer)) + try expect(tokens[2]) == .text(value: ".", at: makeSourceMap(".", for: lexer)) + } - func testVariablesWithoutBeingGreedy() throws { - let templateString = "{{ thing }}{{ name }}" - let lexer = Lexer(templateString: templateString) - let tokens = lexer.tokenize() + func testVariablesWithoutBeingGreedy() throws { + let templateString = "{{ thing }}{{ name }}" + let lexer = Lexer(templateString: templateString) + let tokens = lexer.tokenize() - try expect(tokens.count) == 2 - try expect(tokens[0]) == .variable(value: "thing", at: makeSourceMap("thing", for: lexer)) - try expect(tokens[1]) == .variable(value: "name", at: makeSourceMap("name", for: lexer)) - } + try expect(tokens.count) == 2 + try expect(tokens[0]) == .variable(value: "thing", at: makeSourceMap("thing", for: lexer)) + try expect(tokens[1]) == .variable(value: "name", at: makeSourceMap("name", for: lexer)) + } - func testUnclosedBlock() throws { - let lexer = Lexer(templateString: "{%}") - _ = lexer.tokenize() - } + func testUnclosedBlock() throws { + let lexer = Lexer(templateString: "{%}") + _ = lexer.tokenize() + } - func testTokenizeIncorrectSyntaxWithoutCrashing() throws { - let lexer = Lexer(templateString: "func some() {{% if %}") - _ = lexer.tokenize() - } + func testTokenizeIncorrectSyntaxWithoutCrashing() throws { + let lexer = Lexer(templateString: "func some() {{% if %}") + _ = lexer.tokenize() + } - func testEmptyVariable() throws { - let lexer = Lexer(templateString: "{{}}") - _ = lexer.tokenize() - } + func testEmptyVariable() throws { + let lexer = Lexer(templateString: "{{}}") + _ = lexer.tokenize() + } - func testNewlines() throws { - // swiftlint:disable indentation_width - let templateString = """ + func testNewlines() throws { + // swiftlint:disable indentation_width + let templateString = """ My name is {% if name and @@ -93,69 +93,69 @@ final class LexerTests: XCTestCase { }}{% endif %}. """ - // swiftlint:enable indentation_width - let lexer = Lexer(templateString: templateString) - let tokens = lexer.tokenize() + // swiftlint:enable indentation_width + let lexer = Lexer(templateString: templateString) + let tokens = lexer.tokenize() - try expect(tokens.count) == 5 - try expect(tokens[0]) == .text(value: "My name is ", at: makeSourceMap("My name is", for: lexer)) - try expect(tokens[1]) == .block(value: "if name and name", at: makeSourceMap("{%", for: lexer)) - try expect(tokens[2]) == .variable(value: "name", at: makeSourceMap("name", for: lexer, options: .backwards)) - try expect(tokens[3]) == .block(value: "endif", at: makeSourceMap("endif", for: lexer)) - try expect(tokens[4]) == .text(value: ".", at: makeSourceMap(".", for: lexer)) - } + try expect(tokens.count) == 5 + try expect(tokens[0]) == .text(value: "My name is ", at: makeSourceMap("My name is", for: lexer)) + try expect(tokens[1]) == .block(value: "if name and name", at: makeSourceMap("{%", for: lexer)) + try expect(tokens[2]) == .variable(value: "name", at: makeSourceMap("name", for: lexer, options: .backwards)) + try expect(tokens[3]) == .block(value: "endif", at: makeSourceMap("endif", for: lexer)) + try expect(tokens[4]) == .text(value: ".", at: makeSourceMap(".", for: lexer)) + } - func testTrimSymbols() throws { - let fBlock = "if hello" - let sBlock = "ta da" - let lexer = Lexer(templateString: "{%+ \(fBlock) -%}{% \(sBlock) -%}") - let tokens = lexer.tokenize() - let behaviours = ( - WhitespaceBehaviour(leading: .keep, trailing: .trim), - WhitespaceBehaviour(leading: .unspecified, trailing: .trim) - ) + func testTrimSymbols() throws { + let fBlock = "if hello" + let sBlock = "ta da" + let lexer = Lexer(templateString: "{%+ \(fBlock) -%}{% \(sBlock) -%}") + let tokens = lexer.tokenize() + let behaviours = ( + WhitespaceBehaviour(leading: .keep, trailing: .trim), + WhitespaceBehaviour(leading: .unspecified, trailing: .trim) + ) - try expect(tokens.count) == 2 - try expect(tokens[0]) == .block(value: fBlock, at: makeSourceMap(fBlock, for: lexer), whitespace: behaviours.0) - try expect(tokens[1]) == .block(value: sBlock, at: makeSourceMap(sBlock, for: lexer), whitespace: behaviours.1) - } + try expect(tokens.count) == 2 + try expect(tokens[0]) == .block(value: fBlock, at: makeSourceMap(fBlock, for: lexer), whitespace: behaviours.0) + try expect(tokens[1]) == .block(value: sBlock, at: makeSourceMap(sBlock, for: lexer), whitespace: behaviours.1) + } - func testEscapeSequence() throws { - let templateString = "class Some {{ '{' }}{% if true %}{{ stuff }}{% endif %}" - let lexer = Lexer(templateString: templateString) - let tokens = lexer.tokenize() + func testEscapeSequence() throws { + let templateString = "class Some {{ '{' }}{% if true %}{{ stuff }}{% endif %}" + let lexer = Lexer(templateString: templateString) + let tokens = lexer.tokenize() - try expect(tokens.count) == 5 - try expect(tokens[0]) == .text(value: "class Some ", at: makeSourceMap("class Some ", for: lexer)) - try expect(tokens[1]) == .variable(value: "'{'", at: makeSourceMap("'{'", for: lexer)) - try expect(tokens[2]) == .block(value: "if true", at: makeSourceMap("if true", for: lexer)) - try expect(tokens[3]) == .variable(value: "stuff", at: makeSourceMap("stuff", for: lexer)) - try expect(tokens[4]) == .block(value: "endif", at: makeSourceMap("endif", for: lexer)) - } + try expect(tokens.count) == 5 + try expect(tokens[0]) == .text(value: "class Some ", at: makeSourceMap("class Some ", for: lexer)) + try expect(tokens[1]) == .variable(value: "'{'", at: makeSourceMap("'{'", for: lexer)) + try expect(tokens[2]) == .block(value: "if true", at: makeSourceMap("if true", for: lexer)) + try expect(tokens[3]) == .variable(value: "stuff", at: makeSourceMap("stuff", for: lexer)) + try expect(tokens[4]) == .block(value: "endif", at: makeSourceMap("endif", for: lexer)) + } - func testPerformance() throws { - let path = Path(#file as String)! / ".." / "fixtures" / "huge.html" - let content: String = try NSString(contentsOfFile: path.string, encoding: String.Encoding.utf8.rawValue).substring(from: 0) as String + func testPerformance() throws { + let path = Path(#file as String)! / ".." / "fixtures" / "huge.html" + let content: String = try NSString(contentsOfFile: path.string, encoding: String.Encoding.utf8.rawValue).substring(from: 0) as String - measure { - let lexer = Lexer(templateString: content) - _ = lexer.tokenize() - } - } + measure { + let lexer = Lexer(templateString: content) + _ = lexer.tokenize() + } + } - func testCombiningDiaeresis() throws { - // the symbol "ü" in the `templateString` is unusually encoded as 0x75 0xCC 0x88 (LATIN SMALL LETTER U + COMBINING - // DIAERESIS) instead of 0xC3 0xBC (LATIN SMALL LETTER U WITH DIAERESIS) - let templateString = "ü\n{% if test %}ü{% endif %}\n{% if ü %}ü{% endif %}\n" - let lexer = Lexer(templateString: templateString) - let tokens = lexer.tokenize() + func testCombiningDiaeresis() throws { + // the symbol "ü" in the `templateString` is unusually encoded as 0x75 0xCC 0x88 (LATIN SMALL LETTER U + COMBINING + // DIAERESIS) instead of 0xC3 0xBC (LATIN SMALL LETTER U WITH DIAERESIS) + let templateString = "ü\n{% if test %}ü{% endif %}\n{% if ü %}ü{% endif %}\n" + let lexer = Lexer(templateString: templateString) + let tokens = lexer.tokenize() - try expect(tokens.count) == 9 - assert(tokens[1].contents == "if test") - } + try expect(tokens.count) == 9 + assert(tokens[1].contents == "if test") + } - private func makeSourceMap(_ token: String, for lexer: Lexer, options: String.CompareOptions = []) -> SourceMap { - guard let range = lexer.templateString.range(of: token, options: options) else { fatalError("Token not found") } - return SourceMap(location: lexer.rangeLocation(range)) - } + private func makeSourceMap(_ token: String, for lexer: Lexer, options: String.CompareOptions = []) -> SourceMap { + guard let range = lexer.templateString.range(of: token, options: options) else { fatalError("Token not found") } + return SourceMap(location: lexer.rangeLocation(range)) + } } diff --git a/Tests/StencilTests/LoaderSpec.swift b/Tests/StencilTests/LoaderSpec.swift index 9433774..882f382 100644 --- a/Tests/StencilTests/LoaderSpec.swift +++ b/Tests/StencilTests/LoaderSpec.swift @@ -4,52 +4,52 @@ import Stencil import XCTest final class TemplateLoaderTests: XCTestCase { - func testFileSystemLoader() { - let path = Path(#file as String)! / ".." / "fixtures" - let loader = FileSystemLoader(paths: [path]) - let environment = Environment(loader: loader) + func testFileSystemLoader() { + let path = Path(#file as String)! / ".." / "fixtures" + let loader = FileSystemLoader(paths: [path]) + let environment = Environment(loader: loader) - it("errors when a template cannot be found") { - try expect(try environment.loadTemplate(name: "unknown.html")).toThrow() - } + it("errors when a template cannot be found") { + try expect(try environment.loadTemplate(name: "unknown.html")).toThrow() + } - it("errors when an array of templates cannot be found") { - try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow() - } + it("errors when an array of templates cannot be found") { + try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow() + } - it("can load a template from a file") { - _ = try environment.loadTemplate(name: "test.html") - } + it("can load a template from a file") { + _ = try environment.loadTemplate(name: "test.html") + } - it("errors when loading absolute file outside of the selected path") { - try expect(try environment.loadTemplate(name: "/etc/hosts")).toThrow() - } + it("errors when loading absolute file outside of the selected path") { + try expect(try environment.loadTemplate(name: "/etc/hosts")).toThrow() + } - it("errors when loading relative file outside of the selected path") { - try expect(try environment.loadTemplate(name: "../LoaderSpec.swift")).toThrow() - } - } + it("errors when loading relative file outside of the selected path") { + try expect(try environment.loadTemplate(name: "../LoaderSpec.swift")).toThrow() + } + } - func testDictionaryLoader() { - let loader = DictionaryLoader(templates: [ - "index.html": "Hello World" - ]) - let environment = Environment(loader: loader) + func testDictionaryLoader() { + let loader = DictionaryLoader(templates: [ + "index.html": "Hello World" + ]) + let environment = Environment(loader: loader) - it("errors when a template cannot be found") { - try expect(try environment.loadTemplate(name: "unknown.html")).toThrow() - } + it("errors when a template cannot be found") { + try expect(try environment.loadTemplate(name: "unknown.html")).toThrow() + } - it("errors when an array of templates cannot be found") { - try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow() - } + it("errors when an array of templates cannot be found") { + try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow() + } - it("can load a template from a known templates") { - _ = try environment.loadTemplate(name: "index.html") - } + it("can load a template from a known templates") { + _ = try environment.loadTemplate(name: "index.html") + } - it("can load a known template from a collection of templates") { - _ = try environment.loadTemplate(names: ["unknown.html", "index.html"]) - } - } + it("can load a known template from a collection of templates") { + _ = try environment.loadTemplate(names: ["unknown.html", "index.html"]) + } + } } diff --git a/Tests/StencilTests/NodeSpec.swift b/Tests/StencilTests/NodeSpec.swift index c7659f6..f6dc3e4 100644 --- a/Tests/StencilTests/NodeSpec.swift +++ b/Tests/StencilTests/NodeSpec.swift @@ -3,109 +3,109 @@ import Spectre import XCTest final class NodeTests: XCTestCase { - private let context = Context(dictionary: [ - "name": "Kyle", - "age": 27, - "items": [1, 2, 3] - ]) + private let context = Context(dictionary: [ + "name": "Kyle", + "age": 27, + "items": [1, 2, 3] + ]) - func testTextNode() { - it("renders the given text") { - let node = TextNode(text: "Hello World") - try expect(try node.render(self.context)) == "Hello World" - } - it("Trims leading whitespace") { - let text = " \n Some text " - let trimBehaviour = TrimBehaviour(leading: .whitespace, trailing: .nothing) - let node = TextNode(text: text, trimBehaviour: trimBehaviour) - try expect(try node.render(self.context)) == "\n Some text " - } - it("Trims leading whitespace and one newline") { - let text = "\n\n Some text " - let trimBehaviour = TrimBehaviour(leading: .whitespaceAndOneNewLine, trailing: .nothing) - let node = TextNode(text: text, trimBehaviour: trimBehaviour) - try expect(try node.render(self.context)) == "\n Some text " - } - it("Trims leading whitespace and one newline") { - let text = "\n\n Some text " - let trimBehaviour = TrimBehaviour(leading: .whitespaceAndNewLines, trailing: .nothing) - let node = TextNode(text: text, trimBehaviour: trimBehaviour) - try expect(try node.render(self.context)) == "Some text " - } - it("Trims trailing whitespace") { - let text = " Some text \n" - let trimBehaviour = TrimBehaviour(leading: .nothing, trailing: .whitespace) - let node = TextNode(text: text, trimBehaviour: trimBehaviour) - try expect(try node.render(self.context)) == " Some text\n" - } - it("Trims trailing whitespace and one newline") { - let text = " Some text \n \n " - let trimBehaviour = TrimBehaviour(leading: .nothing, trailing: .whitespaceAndOneNewLine) - let node = TextNode(text: text, trimBehaviour: trimBehaviour) - try expect(try node.render(self.context)) == " Some text \n " - } - it("Trims trailing whitespace and newlines") { - let text = " Some text \n \n " - let trimBehaviour = TrimBehaviour(leading: .nothing, trailing: .whitespaceAndNewLines) - let node = TextNode(text: text, trimBehaviour: trimBehaviour) - try expect(try node.render(self.context)) == " Some text" - } - it("Trims all whitespace") { - let text = " \n \nSome text \n " - let trimBehaviour = TrimBehaviour(leading: .whitespaceAndNewLines, trailing: .whitespaceAndNewLines) - let node = TextNode(text: text, trimBehaviour: trimBehaviour) - try expect(try node.render(self.context)) == "Some text" - } - } + func testTextNode() { + it("renders the given text") { + let node = TextNode(text: "Hello World") + try expect(try node.render(self.context)) == "Hello World" + } + it("Trims leading whitespace") { + let text = " \n Some text " + let trimBehaviour = TrimBehaviour(leading: .whitespace, trailing: .nothing) + let node = TextNode(text: text, trimBehaviour: trimBehaviour) + try expect(try node.render(self.context)) == "\n Some text " + } + it("Trims leading whitespace and one newline") { + let text = "\n\n Some text " + let trimBehaviour = TrimBehaviour(leading: .whitespaceAndOneNewLine, trailing: .nothing) + let node = TextNode(text: text, trimBehaviour: trimBehaviour) + try expect(try node.render(self.context)) == "\n Some text " + } + it("Trims leading whitespace and one newline") { + let text = "\n\n Some text " + let trimBehaviour = TrimBehaviour(leading: .whitespaceAndNewLines, trailing: .nothing) + let node = TextNode(text: text, trimBehaviour: trimBehaviour) + try expect(try node.render(self.context)) == "Some text " + } + it("Trims trailing whitespace") { + let text = " Some text \n" + let trimBehaviour = TrimBehaviour(leading: .nothing, trailing: .whitespace) + let node = TextNode(text: text, trimBehaviour: trimBehaviour) + try expect(try node.render(self.context)) == " Some text\n" + } + it("Trims trailing whitespace and one newline") { + let text = " Some text \n \n " + let trimBehaviour = TrimBehaviour(leading: .nothing, trailing: .whitespaceAndOneNewLine) + let node = TextNode(text: text, trimBehaviour: trimBehaviour) + try expect(try node.render(self.context)) == " Some text \n " + } + it("Trims trailing whitespace and newlines") { + let text = " Some text \n \n " + let trimBehaviour = TrimBehaviour(leading: .nothing, trailing: .whitespaceAndNewLines) + let node = TextNode(text: text, trimBehaviour: trimBehaviour) + try expect(try node.render(self.context)) == " Some text" + } + it("Trims all whitespace") { + let text = " \n \nSome text \n " + let trimBehaviour = TrimBehaviour(leading: .whitespaceAndNewLines, trailing: .whitespaceAndNewLines) + let node = TextNode(text: text, trimBehaviour: trimBehaviour) + try expect(try node.render(self.context)) == "Some text" + } + } - func testVariableNode() { - it("resolves and renders the variable") { - let node = VariableNode(variable: Variable("name")) - try expect(try node.render(self.context)) == "Kyle" - } + func testVariableNode() { + it("resolves and renders the variable") { + let node = VariableNode(variable: Variable("name")) + try expect(try node.render(self.context)) == "Kyle" + } - it("resolves and renders a non string variable") { - let node = VariableNode(variable: Variable("age")) - try expect(try node.render(self.context)) == "27" - } - } + it("resolves and renders a non string variable") { + let node = VariableNode(variable: Variable("age")) + try expect(try node.render(self.context)) == "27" + } + } - func testRendering() { - it("renders the nodes") { - let nodes: [NodeType] = [ - TextNode(text: "Hello "), - VariableNode(variable: "name") - ] + func testRendering() { + it("renders the nodes") { + let nodes: [NodeType] = [ + TextNode(text: "Hello "), + VariableNode(variable: "name") + ] - try expect(try renderNodes(nodes, self.context)) == "Hello Kyle" - } + try expect(try renderNodes(nodes, self.context)) == "Hello Kyle" + } - it("correctly throws a nodes failure") { - let nodes: [NodeType] = [ - TextNode(text: "Hello "), - VariableNode(variable: "name"), - ErrorNode() - ] + it("correctly throws a nodes failure") { + let nodes: [NodeType] = [ + TextNode(text: "Hello "), + VariableNode(variable: "name"), + ErrorNode() + ] - try expect(try renderNodes(nodes, self.context)).toThrow(TemplateSyntaxError("Custom Error")) - } - } + try expect(try renderNodes(nodes, self.context)).toThrow(TemplateSyntaxError("Custom Error")) + } + } - func testRenderingBooleans() { - it("can render true & false") { - try expect(Template(templateString: "{{ true }}").render()) == "true" - try expect(Template(templateString: "{{ false }}").render()) == "false" - } + func testRenderingBooleans() { + it("can render true & false") { + try expect(Template(templateString: "{{ true }}").render()) == "true" + try expect(Template(templateString: "{{ false }}").render()) == "false" + } - it("can resolve variable") { - let template = Template(templateString: "{{ value == \"known\" }}") - try expect(template.render(["value": "known"])) == "true" - try expect(template.render(["value": "unknown"])) == "false" - } + it("can resolve variable") { + let template = Template(templateString: "{{ value == \"known\" }}") + try expect(template.render(["value": "known"])) == "true" + try expect(template.render(["value": "unknown"])) == "false" + } - it("can render a boolean expression") { - try expect(Template(templateString: "{{ 1 > 0 }}").render()) == "true" - try expect(Template(templateString: "{{ 1 == 2 }}").render()) == "false" - } - } + it("can render a boolean expression") { + try expect(Template(templateString: "{{ 1 > 0 }}").render()) == "true" + try expect(Template(templateString: "{{ 1 == 2 }}").render()) == "false" + } + } } diff --git a/Tests/StencilTests/NowNodeSpec.swift b/Tests/StencilTests/NowNodeSpec.swift index a213a32..a29f586 100644 --- a/Tests/StencilTests/NowNodeSpec.swift +++ b/Tests/StencilTests/NowNodeSpec.swift @@ -3,48 +3,48 @@ import Spectre import XCTest final class NowNodeTests: XCTestCase { - func testParsing() { - it("parses default format without any now arguments") { - #if os(Linux) - throw skip() - #else - let tokens: [Token] = [ .block(value: "now", at: .unknown) ] - let parser = TokenParser(tokens: tokens, environment: Environment()) + func testParsing() { + it("parses default format without any now arguments") { + #if os(Linux) + throw skip() + #else + let tokens: [Token] = [ .block(value: "now", at: .unknown) ] + let parser = TokenParser(tokens: tokens, environment: Environment()) - let nodes = try parser.parse() - let node = nodes.first as? NowNode - try expect(nodes.count) == 1 - try expect(node?.format.variable) == "\"yyyy-MM-dd 'at' HH:mm\"" - #endif - } + let nodes = try parser.parse() + let node = nodes.first as? NowNode + try expect(nodes.count) == 1 + try expect(node?.format.variable) == "\"yyyy-MM-dd 'at' HH:mm\"" + #endif + } - it("parses now with a format") { - #if os(Linux) - throw skip() - #else - let tokens: [Token] = [ .block(value: "now \"HH:mm\"", at: .unknown) ] - let parser = TokenParser(tokens: tokens, environment: Environment()) - let nodes = try parser.parse() - let node = nodes.first as? NowNode - try expect(nodes.count) == 1 - try expect(node?.format.variable) == "\"HH:mm\"" - #endif - } - } + it("parses now with a format") { + #if os(Linux) + throw skip() + #else + let tokens: [Token] = [ .block(value: "now \"HH:mm\"", at: .unknown) ] + let parser = TokenParser(tokens: tokens, environment: Environment()) + let nodes = try parser.parse() + let node = nodes.first as? NowNode + try expect(nodes.count) == 1 + try expect(node?.format.variable) == "\"HH:mm\"" + #endif + } + } - func testRendering() { - it("renders the date") { - #if os(Linux) - throw skip() - #else - let node = NowNode(format: Variable("\"yyyy-MM-dd\"")) + func testRendering() { + it("renders the date") { + #if os(Linux) + throw skip() + #else + let node = NowNode(format: Variable("\"yyyy-MM-dd\"")) - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd" - let date = formatter.string(from: Date()) + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + let date = formatter.string(from: Date()) - try expect(try node.render(Context())) == date - #endif - } - } + try expect(try node.render(Context())) == date + #endif + } + } } diff --git a/Tests/StencilTests/ParserSpec.swift b/Tests/StencilTests/ParserSpec.swift index 44d79d9..d932849 100644 --- a/Tests/StencilTests/ParserSpec.swift +++ b/Tests/StencilTests/ParserSpec.swift @@ -3,77 +3,77 @@ import Spectre import XCTest final class TokenParserTests: XCTestCase { - func testTextToken() throws { - let parser = TokenParser(tokens: [ - .text(value: "Hello World", at: .unknown) - ], environment: Environment()) + func testTextToken() throws { + let parser = TokenParser(tokens: [ + .text(value: "Hello World", at: .unknown) + ], environment: Environment()) - let nodes = try parser.parse() - let node = nodes.first as? TextNode + let nodes = try parser.parse() + let node = nodes.first as? TextNode - try expect(nodes.count) == 1 - try expect(node?.text) == "Hello World" - } + try expect(nodes.count) == 1 + try expect(node?.text) == "Hello World" + } - func testVariableToken() throws { - let parser = TokenParser(tokens: [ - .variable(value: "'name'", at: .unknown) - ], environment: Environment()) + func testVariableToken() throws { + let parser = TokenParser(tokens: [ + .variable(value: "'name'", at: .unknown) + ], environment: Environment()) - let nodes = try parser.parse() - let node = nodes.first as? VariableNode - try expect(nodes.count) == 1 - let result = try node?.render(Context()) - try expect(result) == "name" - } + let nodes = try parser.parse() + let node = nodes.first as? VariableNode + try expect(nodes.count) == 1 + let result = try node?.render(Context()) + try expect(result) == "name" + } - func testCommentToken() throws { - let parser = TokenParser(tokens: [ - .comment(value: "Secret stuff!", at: .unknown) - ], environment: Environment()) + func testCommentToken() throws { + let parser = TokenParser(tokens: [ + .comment(value: "Secret stuff!", at: .unknown) + ], environment: Environment()) - let nodes = try parser.parse() - try expect(nodes.count) == 0 - } + let nodes = try parser.parse() + try expect(nodes.count) == 0 + } - func testTagToken() throws { - let simpleExtension = Extension() - simpleExtension.registerSimpleTag("known") { _ in - "" - } + func testTagToken() throws { + let simpleExtension = Extension() + simpleExtension.registerSimpleTag("known") { _ in + "" + } - let parser = TokenParser(tokens: [ - .block(value: "known", at: .unknown) - ], environment: Environment(extensions: [simpleExtension])) + let parser = TokenParser(tokens: [ + .block(value: "known", at: .unknown) + ], environment: Environment(extensions: [simpleExtension])) - let nodes = try parser.parse() - try expect(nodes.count) == 1 - } + let nodes = try parser.parse() + try expect(nodes.count) == 1 + } - func testErrorUnknownTag() throws { - let tokens: [Token] = [.block(value: "unknown", at: .unknown)] - let parser = TokenParser(tokens: tokens, environment: Environment()) + func testErrorUnknownTag() throws { + let tokens: [Token] = [.block(value: "unknown", at: .unknown)] + let parser = TokenParser(tokens: tokens, environment: Environment()) - try expect(try parser.parse()).toThrow(TemplateSyntaxError( - reason: "Unknown template tag 'unknown'", - token: tokens.first - )) - } + try expect(try parser.parse()).toThrow(TemplateSyntaxError( + reason: "Unknown template tag 'unknown'", + token: tokens.first + )) + } - func testTransformWhitespaceBehaviourToTrimBehaviour() throws { - let simpleExtension = Extension() - simpleExtension.registerSimpleTag("known") { _ in "" } + func testTransformWhitespaceBehaviourToTrimBehaviour() throws { + let simpleExtension = Extension() + simpleExtension.registerSimpleTag("known") { _ in "" } - let parser = TokenParser(tokens: [ - .block(value: "known", at: .unknown, whitespace: WhitespaceBehaviour(leading: .unspecified, trailing: .trim)), - .text(value: " \nSome text ", at: .unknown), - .block(value: "known", at: .unknown, whitespace: WhitespaceBehaviour(leading: .keep, trailing: .trim)) - ], environment: Environment(extensions: [simpleExtension])) + let parser = TokenParser(tokens: [ + .block(value: "known", at: .unknown, whitespace: WhitespaceBehaviour(leading: .unspecified, trailing: .trim)), + .text(value: " \nSome text ", at: .unknown), + .block(value: "known", at: .unknown, whitespace: WhitespaceBehaviour(leading: .keep, trailing: .trim)) + ], environment: Environment(extensions: [simpleExtension])) - let nodes = try parser.parse() - try expect(nodes.count) == 3 - let textNode = nodes[1] as? TextNode - try expect(textNode?.text) == " \nSome text " - try expect(textNode?.trimBehaviour) == TrimBehaviour(leading: .whitespaceAndNewLines, trailing: .nothing) - } + let nodes = try parser.parse() + try expect(nodes.count) == 3 + let textNode = nodes[1] as? TextNode + try expect(textNode?.text) == " \nSome text " + try expect(textNode?.trimBehaviour) == TrimBehaviour(leading: .whitespaceAndNewLines, trailing: .nothing) + } } diff --git a/Tests/StencilTests/StencilSpec.swift b/Tests/StencilTests/StencilSpec.swift index 8e01b63..e42a5f5 100644 --- a/Tests/StencilTests/StencilSpec.swift +++ b/Tests/StencilTests/StencilSpec.swift @@ -3,68 +3,68 @@ import Stencil import XCTest final class StencilTests: XCTestCase { - private lazy var environment: Environment = { - let exampleExtension = Extension() - exampleExtension.registerSimpleTag("simpletag") { _ in - "Hello World" - } - exampleExtension.registerTag("customtag") { _, token in - CustomNode(token: token) - } - return Environment(extensions: [exampleExtension]) - }() + private lazy var environment: Environment = { + let exampleExtension = Extension() + exampleExtension.registerSimpleTag("simpletag") { _ in + "Hello World" + } + exampleExtension.registerTag("customtag") { _, token in + CustomNode(token: token) + } + return Environment(extensions: [exampleExtension]) + }() - func testStencil() { - it("can render the README example") { - let templateString = """ - There are {{ articles.count }} articles. + func testStencil() { + it("can render the README example") { + let templateString = """ + There are {{ articles.count }} articles. - {% for article in articles %}\ - - {{ article.title }} by {{ article.author }}. - {% endfor %} - """ + {% for article in articles %}\ + - {{ article.title }} by {{ article.author }}. + {% endfor %} + """ - let context = [ - "articles": [ - Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"), - Article(title: "Memory Management with ARC", author: "Kyle Fuller") - ] - ] + let context = [ + "articles": [ + Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"), + Article(title: "Memory Management with ARC", author: "Kyle Fuller") + ] + ] - let template = Template(templateString: templateString) - let result = try template.render(context) + let template = Template(templateString: templateString) + let result = try template.render(context) - try expect(result) == """ - There are 2 articles. + try expect(result) == """ + There are 2 articles. - - Migrating from OCUnit to XCTest by Kyle Fuller. - - Memory Management with ARC by Kyle Fuller. + - Migrating from OCUnit to XCTest by Kyle Fuller. + - Memory Management with ARC by Kyle Fuller. - """ - } + """ + } - it("can render a custom template tag") { - let result = try self.environment.renderTemplate(string: "{% customtag %}") - try expect(result) == "Hello World" - } + it("can render a custom template tag") { + let result = try self.environment.renderTemplate(string: "{% customtag %}") + try expect(result) == "Hello World" + } - it("can render a simple custom tag") { - let result = try self.environment.renderTemplate(string: "{% simpletag %}") - try expect(result) == "Hello World" - } - } + it("can render a simple custom tag") { + let result = try self.environment.renderTemplate(string: "{% simpletag %}") + try expect(result) == "Hello World" + } + } } // MARK: - Helpers private struct CustomNode: NodeType { - let token: Token? - func render(_ context: Context) throws -> String { - "Hello World" - } + let token: Token? + func render(_ context: Context) throws -> String { + "Hello World" + } } private struct Article { - let title: String - let author: String + let title: String + let author: String } diff --git a/Tests/StencilTests/TemplateSpec.swift b/Tests/StencilTests/TemplateSpec.swift index 433f1cb..90d0711 100644 --- a/Tests/StencilTests/TemplateSpec.swift +++ b/Tests/StencilTests/TemplateSpec.swift @@ -3,17 +3,17 @@ import Spectre import XCTest final class TemplateTests: XCTestCase { - func testTemplate() { - it("can render a template from a string") { - let template = Template(templateString: "Hello World") - let result = try template.render([ "name": "Kyle" ]) - try expect(result) == "Hello World" - } + func testTemplate() { + it("can render a template from a string") { + let template = Template(templateString: "Hello World") + let result = try template.render([ "name": "Kyle" ]) + try expect(result) == "Hello World" + } - it("can render a template from a string literal") { - let template: Template = "Hello World" - let result = try template.render([ "name": "Kyle" ]) - try expect(result) == "Hello World" - } - } + it("can render a template from a string literal") { + let template: Template = "Hello World" + let result = try template.render([ "name": "Kyle" ]) + try expect(result) == "Hello World" + } + } } diff --git a/Tests/StencilTests/TokenSpec.swift b/Tests/StencilTests/TokenSpec.swift index effa884..5ecb589 100644 --- a/Tests/StencilTests/TokenSpec.swift +++ b/Tests/StencilTests/TokenSpec.swift @@ -3,32 +3,32 @@ import Spectre import XCTest final class TokenTests: XCTestCase { - func testToken() { - it("can split the contents into components") { - let token = Token.text(value: "hello world", at: .unknown) - let components = token.components + func testToken() { + it("can split the contents into components") { + let token = Token.text(value: "hello world", at: .unknown) + let components = token.components - try expect(components.count) == 2 - try expect(components[0]) == "hello" - try expect(components[1]) == "world" - } + try expect(components.count) == 2 + try expect(components[0]) == "hello" + try expect(components[1]) == "world" + } - it("can split the contents into components with single quoted strings") { - let token = Token.text(value: "hello 'kyle fuller'", at: .unknown) - let components = token.components + it("can split the contents into components with single quoted strings") { + let token = Token.text(value: "hello 'kyle fuller'", at: .unknown) + let components = token.components - try expect(components.count) == 2 - try expect(components[0]) == "hello" - try expect(components[1]) == "'kyle fuller'" - } + try expect(components.count) == 2 + try expect(components[0]) == "hello" + try expect(components[1]) == "'kyle fuller'" + } - it("can split the contents into components with double quoted strings") { - let token = Token.text(value: "hello \"kyle fuller\"", at: .unknown) - let components = token.components + it("can split the contents into components with double quoted strings") { + let token = Token.text(value: "hello \"kyle fuller\"", at: .unknown) + let components = token.components - try expect(components.count) == 2 - try expect(components[0]) == "hello" - try expect(components[1]) == "\"kyle fuller\"" - } - } + try expect(components.count) == 2 + try expect(components[0]) == "hello" + try expect(components[1]) == "\"kyle fuller\"" + } + } } diff --git a/Tests/StencilTests/VariableSpec.swift b/Tests/StencilTests/VariableSpec.swift index 8155775..ddb390a 100644 --- a/Tests/StencilTests/VariableSpec.swift +++ b/Tests/StencilTests/VariableSpec.swift @@ -3,357 +3,357 @@ import Spectre import XCTest final class VariableTests: XCTestCase { - private let context: Context = { - let ext = Extension() - ext.registerFilter("incr") { arg in - (arg.flatMap { toNumber(value: $0) } ?? 0) + 1 - } - let environment = Environment(extensions: [ext]) + private let context: Context = { + let ext = Extension() + ext.registerFilter("incr") { arg in + (arg.flatMap { toNumber(value: $0) } ?? 0) + 1 + } + let environment = Environment(extensions: [ext]) - var context = Context(dictionary: [ - "name": "Kyle", - "contacts": ["Katie", "Carlton"], - "profiles": [ - "github": "kylef" - ], - "counter": [ - "count": "kylef" - ], - "article": Article(author: Person(name: "Kyle")), - "blog": Blog(), - "tuple": (one: 1, two: 2), - "dynamic": [ - "enum": DynamicEnum.someValue, - "struct": DynamicStruct() - ] - ], environment: environment) - #if os(OSX) - context["object"] = Object() - #endif - return context - }() + var context = Context(dictionary: [ + "name": "Kyle", + "contacts": ["Katie", "Carlton"], + "profiles": [ + "github": "kylef" + ], + "counter": [ + "count": "kylef" + ], + "article": Article(author: Person(name: "Kyle")), + "blog": Blog(), + "tuple": (one: 1, two: 2), + "dynamic": [ + "enum": DynamicEnum.someValue, + "struct": DynamicStruct() + ] + ], environment: environment) + #if os(OSX) + context["object"] = Object() + #endif + return context + }() - func testLiterals() { - it("can resolve a string literal with double quotes") { - let variable = Variable("\"name\"") - let result = try variable.resolve(self.context) as? String - try expect(result) == "name" - } + func testLiterals() { + it("can resolve a string literal with double quotes") { + let variable = Variable("\"name\"") + let result = try variable.resolve(self.context) as? String + try expect(result) == "name" + } - it("can resolve a string literal with one double quote") { - let variable = Variable("\"") - let result = try variable.resolve(self.context) as? String - try expect(result).to.beNil() - } + it("can resolve a string literal with one double quote") { + let variable = Variable("\"") + let result = try variable.resolve(self.context) as? String + try expect(result).to.beNil() + } - it("can resolve a string literal with single quotes") { - let variable = Variable("'name'") - let result = try variable.resolve(self.context) as? String - try expect(result) == "name" - } + it("can resolve a string literal with single quotes") { + let variable = Variable("'name'") + let result = try variable.resolve(self.context) as? String + try expect(result) == "name" + } - it("can resolve a string literal with one single quote") { - let variable = Variable("'") - let result = try variable.resolve(self.context) as? String - try expect(result).to.beNil() - } + it("can resolve a string literal with one single quote") { + let variable = Variable("'") + let result = try variable.resolve(self.context) as? String + try expect(result).to.beNil() + } - it("can resolve an integer literal") { - let variable = Variable("5") - let result = try variable.resolve(self.context) as? Int - try expect(result) == 5 - } + it("can resolve an integer literal") { + let variable = Variable("5") + let result = try variable.resolve(self.context) as? Int + try expect(result) == 5 + } - it("can resolve an float literal") { - let variable = Variable("3.14") - let result = try variable.resolve(self.context) as? Number - try expect(result) == 3.14 - } + it("can resolve an float literal") { + let variable = Variable("3.14") + let result = try variable.resolve(self.context) as? Number + try expect(result) == 3.14 + } - it("can resolve boolean literal") { - try expect(Variable("true").resolve(self.context) as? Bool) == true - try expect(Variable("false").resolve(self.context) as? Bool) == false - try expect(Variable("0").resolve(self.context) as? Int) == 0 - try expect(Variable("1").resolve(self.context) as? Int) == 1 - } - } + it("can resolve boolean literal") { + try expect(Variable("true").resolve(self.context) as? Bool) == true + try expect(Variable("false").resolve(self.context) as? Bool) == false + try expect(Variable("0").resolve(self.context) as? Int) == 0 + try expect(Variable("1").resolve(self.context) as? Int) == 1 + } + } - func testVariable() { - it("can resolve a string variable") { - let variable = Variable("name") - let result = try variable.resolve(self.context) as? String - try expect(result) == "Kyle" - } - } + func testVariable() { + it("can resolve a string variable") { + let variable = Variable("name") + let result = try variable.resolve(self.context) as? String + try expect(result) == "Kyle" + } + } - func testDictionary() { - it("can resolve an item from a dictionary") { - let variable = Variable("profiles.github") - let result = try variable.resolve(self.context) as? String - try expect(result) == "kylef" - } + func testDictionary() { + it("can resolve an item from a dictionary") { + let variable = Variable("profiles.github") + let result = try variable.resolve(self.context) as? String + try expect(result) == "kylef" + } - it("can get the count of a dictionary") { - let variable = Variable("profiles.count") - let result = try variable.resolve(self.context) as? Int - try expect(result) == 1 - } - } + it("can get the count of a dictionary") { + let variable = Variable("profiles.count") + let result = try variable.resolve(self.context) as? Int + try expect(result) == 1 + } + } - func testArray() { - it("can resolve an item from an array via it's index") { - let variable = Variable("contacts.0") - let result = try variable.resolve(self.context) as? String - try expect(result) == "Katie" + func testArray() { + it("can resolve an item from an array via it's index") { + let variable = Variable("contacts.0") + let result = try variable.resolve(self.context) as? String + try expect(result) == "Katie" - let variable1 = Variable("contacts.1") - let result1 = try variable1.resolve(self.context) as? String - try expect(result1) == "Carlton" - } + let variable1 = Variable("contacts.1") + let result1 = try variable1.resolve(self.context) as? String + try expect(result1) == "Carlton" + } - it("can resolve an item from an array via unknown index") { - let variable = Variable("contacts.5") - let result = try variable.resolve(self.context) as? String - try expect(result).to.beNil() + it("can resolve an item from an array via unknown index") { + let variable = Variable("contacts.5") + let result = try variable.resolve(self.context) as? String + try expect(result).to.beNil() - let variable1 = Variable("contacts.-5") - let result1 = try variable1.resolve(self.context) as? String - try expect(result1).to.beNil() - } + let variable1 = Variable("contacts.-5") + let result1 = try variable1.resolve(self.context) as? String + try expect(result1).to.beNil() + } - it("can resolve the first item from an array") { - let variable = Variable("contacts.first") - let result = try variable.resolve(self.context) as? String - try expect(result) == "Katie" - } + it("can resolve the first item from an array") { + let variable = Variable("contacts.first") + let result = try variable.resolve(self.context) as? String + try expect(result) == "Katie" + } - it("can resolve the last item from an array") { - let variable = Variable("contacts.last") - let result = try variable.resolve(self.context) as? String - try expect(result) == "Carlton" - } - } + it("can resolve the last item from an array") { + let variable = Variable("contacts.last") + let result = try variable.resolve(self.context) as? String + try expect(result) == "Carlton" + } + } - func testDynamicMemberLookup() { - it("can resolve dynamic member lookup") { - let variable = Variable("dynamic.struct.test") - let result = try variable.resolve(self.context) as? String - try expect(result) == "this is a dynamic response" - } + func testDynamicMemberLookup() { + it("can resolve dynamic member lookup") { + let variable = Variable("dynamic.struct.test") + let result = try variable.resolve(self.context) as? String + try expect(result) == "this is a dynamic response" + } - it("can resolve dynamic enum rawValue") { - let variable = Variable("dynamic.enum.rawValue") - let result = try variable.resolve(self.context) as? String - try expect(result) == "this is raw value" - } - } + it("can resolve dynamic enum rawValue") { + let variable = Variable("dynamic.enum.rawValue") + let result = try variable.resolve(self.context) as? String + try expect(result) == "this is raw value" + } + } - func testReflection() { - it("can resolve a property with reflection") { - let variable = Variable("article.author.name") - let result = try variable.resolve(self.context) as? String - try expect(result) == "Kyle" - } + func testReflection() { + it("can resolve a property with reflection") { + let variable = Variable("article.author.name") + let result = try variable.resolve(self.context) as? String + try expect(result) == "Kyle" + } - it("can resolve a value via reflection") { - let variable = Variable("blog.articles.0.author.name") - let result = try variable.resolve(self.context) as? String - try expect(result) == "Kyle" - } + it("can resolve a value via reflection") { + let variable = Variable("blog.articles.0.author.name") + let result = try variable.resolve(self.context) as? String + try expect(result) == "Kyle" + } - it("can resolve a superclass value via reflection") { - let variable = Variable("blog.url") - let result = try variable.resolve(self.context) as? String - try expect(result) == "blog.com" - } + it("can resolve a superclass value via reflection") { + let variable = Variable("blog.url") + let result = try variable.resolve(self.context) as? String + try expect(result) == "blog.com" + } - it("can resolve optional variable property using reflection") { - let variable = Variable("blog.featuring.author.name") - let result = try variable.resolve(self.context) as? String - try expect(result) == "Jhon" - } - } + it("can resolve optional variable property using reflection") { + let variable = Variable("blog.featuring.author.name") + let result = try variable.resolve(self.context) as? String + try expect(result) == "Jhon" + } + } - func testKVO() { - #if os(OSX) - it("can resolve a value via KVO") { - let variable = Variable("object.title") - let result = try variable.resolve(self.context) as? String - try expect(result) == "Hello World" - } + func testKVO() { + #if os(OSX) + it("can resolve a value via KVO") { + let variable = Variable("object.title") + let result = try variable.resolve(self.context) as? String + try expect(result) == "Hello World" + } - it("can resolve a superclass value via KVO") { - let variable = Variable("object.name") - let result = try variable.resolve(self.context) as? String - try expect(result) == "Foo" - } + it("can resolve a superclass value via KVO") { + let variable = Variable("object.name") + let result = try variable.resolve(self.context) as? String + try expect(result) == "Foo" + } - it("does not crash on KVO") { - let variable = Variable("object.fullname") - let result = try variable.resolve(self.context) as? String - try expect(result).to.beNil() - } - #endif - } + it("does not crash on KVO") { + let variable = Variable("object.fullname") + let result = try variable.resolve(self.context) as? String + try expect(result).to.beNil() + } + #endif + } - func testTuple() { - it("can resolve tuple by index") { - let variable = Variable("tuple.0") - let result = try variable.resolve(self.context) as? Int - try expect(result) == 1 - } + func testTuple() { + it("can resolve tuple by index") { + let variable = Variable("tuple.0") + let result = try variable.resolve(self.context) as? Int + try expect(result) == 1 + } - it("can resolve tuple by label") { - let variable = Variable("tuple.two") - let result = try variable.resolve(self.context) as? Int - try expect(result) == 2 - } - } + it("can resolve tuple by label") { + let variable = Variable("tuple.two") + let result = try variable.resolve(self.context) as? Int + try expect(result) == 2 + } + } - func testOptional() { - it("does not render Optional") { - var array: [Any?] = [1, nil] - array.append(array) - let context = Context(dictionary: ["values": array]) + func testOptional() { + it("does not render Optional") { + var array: [Any?] = [1, nil] + array.append(array) + let context = Context(dictionary: ["values": array]) - try expect(VariableNode(variable: "values").render(context)) == "[1, nil, [1, nil]]" - try expect(VariableNode(variable: "values.1").render(context)) == "" - } - } + try expect(VariableNode(variable: "values").render(context)) == "[1, nil, [1, nil]]" + try expect(VariableNode(variable: "values.1").render(context)) == "" + } + } - func testSubscripting() { - it("can resolve a property subscript via reflection") { - try self.context.push(dictionary: ["property": "name"]) { - let variable = Variable("article.author[property]") - let result = try variable.resolve(self.context) as? String - try expect(result) == "Kyle" - } - } + func testSubscripting() { + it("can resolve a property subscript via reflection") { + try self.context.push(dictionary: ["property": "name"]) { + let variable = Variable("article.author[property]") + let result = try variable.resolve(self.context) as? String + try expect(result) == "Kyle" + } + } - it("can subscript an array with a valid index") { - try self.context.push(dictionary: ["property": 0]) { - let variable = Variable("contacts[property]") - let result = try variable.resolve(self.context) as? String - try expect(result) == "Katie" - } - } + it("can subscript an array with a valid index") { + try self.context.push(dictionary: ["property": 0]) { + let variable = Variable("contacts[property]") + let result = try variable.resolve(self.context) as? String + try expect(result) == "Katie" + } + } - it("can subscript an array with an unknown index") { - try self.context.push(dictionary: ["property": 5]) { - let variable = Variable("contacts[property]") - let result = try variable.resolve(self.context) as? String - try expect(result).to.beNil() - } - } + it("can subscript an array with an unknown index") { + try self.context.push(dictionary: ["property": 5]) { + let variable = Variable("contacts[property]") + let result = try variable.resolve(self.context) as? String + try expect(result).to.beNil() + } + } - #if os(OSX) - it("can resolve a subscript via KVO") { - try self.context.push(dictionary: ["property": "name"]) { - let variable = Variable("object[property]") - let result = try variable.resolve(self.context) as? String - try expect(result) == "Foo" - } - } - #endif + #if os(OSX) + it("can resolve a subscript via KVO") { + try self.context.push(dictionary: ["property": "name"]) { + let variable = Variable("object[property]") + let result = try variable.resolve(self.context) as? String + try expect(result) == "Foo" + } + } + #endif - it("can resolve an optional subscript via reflection") { - try self.context.push(dictionary: ["property": "featuring"]) { - let variable = Variable("blog[property].author.name") - let result = try variable.resolve(self.context) as? String - try expect(result) == "Jhon" - } - } - } + it("can resolve an optional subscript via reflection") { + try self.context.push(dictionary: ["property": "featuring"]) { + let variable = Variable("blog[property].author.name") + let result = try variable.resolve(self.context) as? String + try expect(result) == "Jhon" + } + } + } - func testMultipleSubscripting() { - it("can resolve multiple subscripts") { - try self.context.push(dictionary: [ - "prop1": "articles", - "prop2": 0, - "prop3": "name" - ]) { - let variable = Variable("blog[prop1][prop2].author[prop3]") - let result = try variable.resolve(self.context) as? String - try expect(result) == "Kyle" - } - } + func testMultipleSubscripting() { + it("can resolve multiple subscripts") { + try self.context.push(dictionary: [ + "prop1": "articles", + "prop2": 0, + "prop3": "name" + ]) { + let variable = Variable("blog[prop1][prop2].author[prop3]") + let result = try variable.resolve(self.context) as? String + try expect(result) == "Kyle" + } + } - it("can resolve nested subscripts") { - try self.context.push(dictionary: [ - "prop1": "prop2", - "ref": ["prop2": "name"] - ]) { - let variable = Variable("article.author[ref[prop1]]") - let result = try variable.resolve(self.context) as? String - try expect(result) == "Kyle" - } - } + it("can resolve nested subscripts") { + try self.context.push(dictionary: [ + "prop1": "prop2", + "ref": ["prop2": "name"] + ]) { + let variable = Variable("article.author[ref[prop1]]") + let result = try variable.resolve(self.context) as? String + try expect(result) == "Kyle" + } + } - it("throws for invalid keypath syntax") { - try self.context.push(dictionary: ["prop": "name"]) { - let samples = [ - ".", - "..", - ".test", - "test..test", - "[prop]", - "article.author[prop", - "article.author[[prop]", - "article.author[prop]]", - "article.author[]", - "article.author[[]]", - "article.author[prop][]", - "article.author[prop]comments", - "article.author[.]" - ] + it("throws for invalid keypath syntax") { + try self.context.push(dictionary: ["prop": "name"]) { + let samples = [ + ".", + "..", + ".test", + "test..test", + "[prop]", + "article.author[prop", + "article.author[[prop]", + "article.author[prop]]", + "article.author[]", + "article.author[[]]", + "article.author[prop][]", + "article.author[prop]comments", + "article.author[.]" + ] - for lookup in samples { - let variable = Variable(lookup) - try expect(variable.resolve(self.context)).toThrow() - } - } - } - } + for lookup in samples { + let variable = Variable(lookup) + try expect(variable.resolve(self.context)).toThrow() + } + } + } + } - func testRangeVariable() { - func makeVariable(_ token: String) throws -> RangeVariable? { - let token = Token.variable(value: token, at: .unknown) - return try RangeVariable(token.contents, environment: context.environment, containedIn: token) - } + func testRangeVariable() { + func makeVariable(_ token: String) throws -> RangeVariable? { + let token = Token.variable(value: token, at: .unknown) + return try RangeVariable(token.contents, environment: context.environment, containedIn: token) + } - it("can resolve closed range as array") { - let result = try makeVariable("1...3")?.resolve(self.context) as? [Int] - try expect(result) == [1, 2, 3] - } + it("can resolve closed range as array") { + let result = try makeVariable("1...3")?.resolve(self.context) as? [Int] + try expect(result) == [1, 2, 3] + } - it("can resolve decreasing closed range as reversed array") { - let result = try makeVariable("3...1")?.resolve(self.context) as? [Int] - try expect(result) == [3, 2, 1] - } + it("can resolve decreasing closed range as reversed array") { + let result = try makeVariable("3...1")?.resolve(self.context) as? [Int] + try expect(result) == [3, 2, 1] + } - it("can use filter on range variables") { - let result = try makeVariable("1|incr...3|incr")?.resolve(self.context) as? [Int] - try expect(result) == [2, 3, 4] - } + it("can use filter on range variables") { + let result = try makeVariable("1|incr...3|incr")?.resolve(self.context) as? [Int] + try expect(result) == [2, 3, 4] + } - it("throws when left value is not int") { - let template: Template = "{% for i in k...j %}{{ i }}{% endfor %}" - try expect(try template.render(Context(dictionary: ["j": 3, "k": "1"]))).toThrow() - } + it("throws when left value is not int") { + let template: Template = "{% for i in k...j %}{{ i }}{% endfor %}" + try expect(try template.render(Context(dictionary: ["j": 3, "k": "1"]))).toThrow() + } - it("throws when right value is not int") { - let variable = try makeVariable("k...j") - try expect(try variable?.resolve(Context(dictionary: ["j": "3", "k": 1]))).toThrow() - } + it("throws when right value is not int") { + let variable = try makeVariable("k...j") + try expect(try variable?.resolve(Context(dictionary: ["j": "3", "k": 1]))).toThrow() + } - it("throws is left range value is missing") { - try expect(makeVariable("...1")).toThrow() - } + it("throws is left range value is missing") { + try expect(makeVariable("...1")).toThrow() + } - it("throws is right range value is missing") { - try expect(makeVariable("1...")).toThrow() - } - } + it("throws is right range value is missing") { + try expect(makeVariable("1...")).toThrow() + } + } } // MARK: - Helpers @@ -361,38 +361,38 @@ final class VariableTests: XCTestCase { #if os(OSX) @objc class Superclass: NSObject { - @objc let name = "Foo" + @objc let name = "Foo" } @objc class Object: Superclass { - @objc let title = "Hello World" + @objc let title = "Hello World" } #endif private struct Person { - let name: String + let name: String } private struct Article { - let author: Person + let author: Person } private class WebSite { - let url: String = "blog.com" + let url: String = "blog.com" } private class Blog: WebSite { - let articles: [Article] = [Article(author: Person(name: "Kyle"))] - let featuring: Article? = Article(author: Person(name: "Jhon")) + let articles: [Article] = [Article(author: Person(name: "Kyle"))] + let featuring: Article? = Article(author: Person(name: "Jhon")) } @dynamicMemberLookup private struct DynamicStruct: DynamicMemberLookup { - subscript(dynamicMember member: String) -> Any? { - member == "test" ? "this is a dynamic response" : nil - } + subscript(dynamicMember member: String) -> Any? { + member == "test" ? "this is a dynamic response" : nil + } } private enum DynamicEnum: String, DynamicMemberLookup { - case someValue = "this is raw value" + case someValue = "this is raw value" }