diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..e84a57f --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,54 @@ +opt_in_rules: + - anyobject_protocol + - array_init + - attributes + - closure_end_indentation + - closure_spacing + - contains_over_first_not_nil + - convenience_type + - discouraged_optional_boolean + - discouraged_optional_collection + - empty_count + - empty_string + - fallthrough + - fatal_error_message + - first_where + - force_unwrapping + - implicit_return + - implicitly_unwrapped_optional + - joined_default_parameter + - literal_expression_end_indentation + - lower_acl_than_parent + - modifier_order + - multiline_arguments + - multiline_function_chains + - multiline_parameters + - number_separator + - operator_usage_whitespace + - overridden_super_call + - override_in_extension + - private_action + - private_outlet + - prohibited_super_call + - redundant_nil_coalescing + - sorted_first_last + - sorted_imports + - trailing_closure + - unavailable_function + - unneeded_parentheses_in_closure_argument + - vertical_parameter_alignment_on_call + - yoda_condition + +# Rules customization +line_length: + warning: 120 + error: 200 + +nesting: + type_level: + warning: 2 + +# Exclude generated files +excluded: + - .build + - Tests/StencilTests/XCTestManifests.swift diff --git a/.travis.yml b/.travis.yml index 8639971..04f7055 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,5 +20,8 @@ sudo: required dist: trusty install: - eval "$(curl -sL https://swiftenv.fuller.li/install.sh)" + - if [ "$TRAVIS_OS_NAME" == "osx" ]; then wget --output-document /tmp/SwiftLint.pkg https://github.com/realm/SwiftLint/releases/download/0.27.0/SwiftLint.pkg && + sudo installer -pkg /tmp/SwiftLint.pkg -target /; fi script: -- swift test + - swift test + - if [ "$TRAVIS_OS_NAME" == "osx" ]; then swiftlint; fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 316a015..558e8cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,9 @@ _None_ - `Token` type converted to struct to allow computing token components only once. [Ilya Puchka](https://github.com/ilyapuchka) [#256](https://github.com/stencilproject/Stencil/pull/256) +- Added SwiftLint to the project. + [David Jennes](https://github.com/djbe) + [#249](https://github.com/stencilproject/Stencil/pull/249) ## 0.13.1 diff --git a/Sources/Context.swift b/Sources/Context.swift index 157f230..007cf68 100644 --- a/Sources/Context.swift +++ b/Sources/Context.swift @@ -3,9 +3,9 @@ public class Context { var dictionaries: [[String: Any?]] public let environment: Environment - - init(dictionary: [String: Any]? = nil, environment: Environment? = nil) { - if let dictionary = dictionary { + + init(dictionary: [String: Any] = [:], environment: Environment? = nil) { + if !dictionary.isEmpty { dictionaries = [dictionary] } else { dictionaries = [] @@ -28,17 +28,16 @@ public class Context { /// Set a variable in the current context, deleting the variable if it's nil set(value) { - if let dictionary = dictionaries.popLast() { - var mutable_dictionary = dictionary - mutable_dictionary[key] = value - dictionaries.append(mutable_dictionary) + if var dictionary = dictionaries.popLast() { + dictionary[key] = value + dictionaries.append(dictionary) } } } /// Push a new level into the Context - fileprivate func push(_ dictionary: [String: Any]? = nil) { - dictionaries.append(dictionary ?? [:]) + fileprivate func push(_ dictionary: [String: Any] = [:]) { + dictionaries.append(dictionary) } /// Pop the last level off of the Context @@ -47,7 +46,7 @@ public class Context { } /// Push a new level onto the context for the duration of the execution of the given closure - public func push(dictionary: [String: Any]? = nil, closure: (() throws -> Result)) rethrows -> Result { + public func push(dictionary: [String: Any] = [:], closure: (() throws -> Result)) rethrows -> Result { push(dictionary) defer { _ = pop() } return try closure() diff --git a/Sources/Environment.swift b/Sources/Environment.swift index 2778a5d..0c2c72e 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -5,12 +5,12 @@ public struct Environment { public var loader: Loader? public init(loader: Loader? = nil, - extensions: [Extension]? = nil, + extensions: [Extension] = [], templateClass: Template.Type = Template.self) { self.templateClass = templateClass self.loader = loader - self.extensions = (extensions ?? []) + [DefaultExtension()] + self.extensions = extensions + [DefaultExtension()] } public func loadTemplate(name: String) throws -> Template { @@ -29,17 +29,17 @@ public struct Environment { } } - public func renderTemplate(name: String, context: [String: Any]? = nil) throws -> String { + public func renderTemplate(name: String, context: [String: Any] = [:]) throws -> String { let template = try loadTemplate(name: name) return try render(template: template, context: context) } - public func renderTemplate(string: String, context: [String: Any]? = nil) throws -> String { + 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 { + 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/Errors.swift b/Sources/Errors.swift index a6191f9..9c1b584 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -18,14 +18,14 @@ public class TemplateDoesNotExist: Error, CustomStringConvertible { } } -public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { +public struct TemplateSyntaxError: Error, Equatable, CustomStringConvertible { public let reason: String public var description: String { return reason } public internal(set) var token: Token? public internal(set) var stackTrace: [Token] public var templateName: String? { return token?.sourceMap.filename } var allTokens: [Token] { - return stackTrace + (token.map({ [$0] }) ?? []) + return stackTrace + (token.map { [$0] } ?? []) } public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) { @@ -50,7 +50,7 @@ extension Error { } } -public protocol ErrorReporter: class { +public protocol ErrorReporter: AnyObject { func renderError(_ error: Error) -> String } diff --git a/Sources/Expression.swift b/Sources/Expression.swift index 572ad46..045b34c 100644 --- a/Sources/Expression.swift +++ b/Sources/Expression.swift @@ -2,17 +2,14 @@ public protocol Expression: CustomStringConvertible { func evaluate(context: Context) throws -> Bool } - protocol InfixOperator: Expression { init(lhs: Expression, rhs: Expression) } - protocol PrefixOperator: Expression { init(expression: Expression) } - final class StaticExpression: Expression, CustomStringConvertible { let value: Bool @@ -29,7 +26,6 @@ final class StaticExpression: Expression, CustomStringConvertible { } } - final class VariableExpression: Expression, CustomStringConvertible { let variable: Resolvable @@ -48,7 +44,7 @@ final class VariableExpression: Expression, CustomStringConvertible { if let result = result as? [Any] { truthy = !result.isEmpty - } else if let result = result as? [String:Any] { + } else if let result = result as? [String: Any] { truthy = !result.isEmpty } else if let result = result as? Bool { truthy = result @@ -68,7 +64,6 @@ final class VariableExpression: Expression, CustomStringConvertible { } } - final class NotExpression: Expression, PrefixOperator, CustomStringConvertible { let expression: Expression @@ -144,7 +139,6 @@ final class OrExpression: Expression, InfixOperator, CustomStringConvertible { } } - final class AndExpression: Expression, InfixOperator, CustomStringConvertible { let lhs: Expression let rhs: Expression @@ -168,7 +162,6 @@ final class AndExpression: Expression, InfixOperator, CustomStringConvertible { } } - class EqualityExpression: Expression, InfixOperator, CustomStringConvertible { let lhs: Expression let rhs: Expression @@ -204,7 +197,6 @@ class EqualityExpression: Expression, InfixOperator, CustomStringConvertible { } } - class NumericExpression: Expression, InfixOperator, CustomStringConvertible { let lhs: Expression let rhs: Expression @@ -215,7 +207,7 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible { } var description: String { - return "(\(lhs) \(op) \(rhs))" + return "(\(lhs) \(symbol) \(rhs))" } func evaluate(context: Context) throws -> Bool { @@ -233,7 +225,7 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible { return false } - var op: String { + var symbol: String { return "" } @@ -242,9 +234,8 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible { } } - class MoreThanExpression: NumericExpression { - override var op: String { + override var symbol: String { return ">" } @@ -253,9 +244,8 @@ class MoreThanExpression: NumericExpression { } } - class MoreThanEqualExpression: NumericExpression { - override var op: String { + override var symbol: String { return ">=" } @@ -264,9 +254,8 @@ class MoreThanEqualExpression: NumericExpression { } } - class LessThanExpression: NumericExpression { - override var op: String { + override var symbol: String { return "<" } @@ -275,9 +264,8 @@ class LessThanExpression: NumericExpression { } } - class LessThanEqualExpression: NumericExpression { - override var op: String { + override var symbol: String { return "<=" } @@ -286,7 +274,6 @@ class LessThanEqualExpression: NumericExpression { } } - class InequalityExpression: EqualityExpression { override var description: String { return "(\(lhs) != \(rhs))" @@ -297,7 +284,7 @@ class InequalityExpression: EqualityExpression { } } - +// swiftlint:disable:next cyclomatic_complexity func toNumber(value: Any) -> Number? { if let value = value as? Float { return Number(value) diff --git a/Sources/Extension.swift b/Sources/Extension.swift index 5af819e..e994e6e 100644 --- a/Sources/Extension.swift +++ b/Sources/Extension.swift @@ -14,12 +14,13 @@ open class Extension { /// Registers a simple template tag with a name and a handler public func registerSimpleTag(_ name: String, handler: @escaping (Context) throws -> String) { - registerTag(name, parser: { parser, token in - return SimpleNode(token: token, handler: handler) - }) + registerTag(name) { _, token in + SimpleNode(token: token, handler: handler) + } } - + /// Registers boolean filter with it's negative counterpart + // swiftlint:disable:next discouraged_optional_boolean public func registerFilter(name: String, negativeFilterName: String, filter: @escaping (Any?) throws -> Bool?) { filters[name] = .simple(filter) filters[negativeFilterName] = .simple { @@ -44,7 +45,6 @@ open class Extension { } } - class DefaultExtension: Extension { override init() { super.init() @@ -77,7 +77,6 @@ class DefaultExtension: Extension { } } - protocol FilterType { func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any? } diff --git a/Sources/FilterTag.swift b/Sources/FilterTag.swift index 9371b3c..e623b53 100644 --- a/Sources/FilterTag.swift +++ b/Sources/FilterTag.swift @@ -1,4 +1,4 @@ -class FilterNode : NodeType { +class FilterNode: NodeType { let resolvable: Resolvable let nodes: [NodeType] let token: Token? @@ -30,8 +30,7 @@ class FilterNode : NodeType { let value = try renderNodes(nodes, context) return try context.push(dictionary: ["filter_value": value]) { - return try VariableNode(variable: resolvable, token: token).render(context) + try VariableNode(variable: resolvable, token: token).render(context) } } } - diff --git a/Sources/Filters.swift b/Sources/Filters.swift index 693d681..a456299 100644 --- a/Sources/Filters.swift +++ b/Sources/Filters.swift @@ -72,7 +72,7 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? { } var indentWidth = 4 - if arguments.count > 0 { + 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]))) @@ -99,18 +99,17 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? { indentFirst = value } - let indentation = [String](repeating: indentationChar, count: indentWidth).joined(separator: "") + 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 } var lines = content.components(separatedBy: .newlines) let firstLine = (indentFirst ? indentation : "") + lines.removeFirst() - let result = lines.reduce([firstLine]) { (result, line) in - return result + [(line.isEmpty ? "" : "\(indentation)\(line)")] + let result = lines.reduce([firstLine]) { result, line in + result + [(line.isEmpty ? "" : "\(indentation)\(line)")] } return result.joined(separator: "\n") } @@ -120,7 +119,7 @@ func filterFilter(value: Any?, arguments: [Any?], context: Context) throws -> An guard arguments.count == 1 else { throw TemplateSyntaxError("'filter' filter takes one argument") } - + let attribute = stringify(arguments[0]) let expr = try context.environment.compileFilter("$0|\(attribute)") diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index 89a65db..f727324 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -1,14 +1,14 @@ import Foundation -class ForNode : NodeType { +class ForNode: NodeType { let resolvable: Resolvable let loopVariables: [String] - let nodes:[NodeType] + let nodes: [NodeType] let emptyNodes: [NodeType] let `where`: Expression? let token: Token? - class func parse(_ parser:TokenParser, token:Token) throws -> NodeType { + class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { let components = token.components func hasToken(_ token: String, at index: Int) -> Bool { @@ -46,10 +46,24 @@ class ForNode : NodeType { _ = parser.nextToken() } - return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes: emptyNodes, where: `where`, token: token) + return ForNode( + resolvable: resolvable, + loopVariables: loopVariables, + nodes: forNodes, + emptyNodes: emptyNodes, + where: `where`, + token: token + ) } - init(resolvable: Resolvable, loopVariables: [String], nodes: [NodeType], emptyNodes: [NodeType], where: Expression? = nil, token: Token? = nil) { + init( + resolvable: Resolvable, + loopVariables: [String], + nodes: [NodeType], + emptyNodes: [NodeType], + where: Expression? = nil, + token: Token? = nil + ) { self.resolvable = resolvable self.loopVariables = loopVariables self.nodes = nodes @@ -58,10 +72,48 @@ class ForNode : NodeType { self.token = token } - func push(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result { + 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 !values.isEmpty { + let count = values.count + + return try zip(0..., values) + .map { index, item in + let forContext: [String: Any] = [ + "first": index == 0, + "last": index == (count - 1), + "counter": index + 1, + "counter0": index, + "length": count + ] + + return try context.push(dictionary: ["forloop": forContext]) { + try push(value: item, context: context) { + try renderNodes(nodes, context) + } + } + } + .joined() + } + + 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() { - return try closure() + return try context.push { + try closure() } } @@ -71,27 +123,26 @@ class ForNode : NodeType { throw TemplateSyntaxError("Tuple '\(value)' has less values than loop variables") } var variablesContext = [String: Any]() - valueMirror.children.prefix(loopVariables.count).enumerated().forEach({ (offset, element) in + valueMirror.children.prefix(loopVariables.count).enumerated().forEach { offset, element in if loopVariables[offset] != "_" { variablesContext[loopVariables[offset]] = element.value } - }) + } return try context.push(dictionary: variablesContext) { - return try closure() + try closure() } } - return try context.push(dictionary: [loopVariables.first!: value]) { - return try closure() + return try context.push(dictionary: [loopVariables.first ?? "": value]) { + try closure() } } - func render(_ context: Context) throws -> String { + 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] { @@ -120,36 +171,6 @@ class ForNode : NodeType { values = [] } - if let `where` = self.where { - values = try values.filter({ item -> Bool in - return try push(value: item, context: context) { - try `where`.evaluate(context: context) - } - }) - } - - if !values.isEmpty { - let count = values.count - - return try values.enumerated().map { index, item in - let forContext: [String: Any] = [ - "first": index == 0, - "last": index == (count - 1), - "counter": index + 1, - "counter0": index, - "length": count - ] - - return try context.push(dictionary: ["forloop": forContext]) { - return try push(value: item, context: context) { - try renderNodes(nodes, context) - } - } - }.joined(separator: "") - } - - return try context.push { - try renderNodes(emptyNodes, context) - } + return values } } diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index e9fe885..061914a 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -12,7 +12,6 @@ enum Operator { } } - let operators: [Operator] = [ .infix("in", 5, InExpression.self), .infix("or", 6, OrExpression.self), @@ -23,21 +22,17 @@ let operators: [Operator] = [ .infix(">", 10, MoreThanExpression.self), .infix(">=", 10, MoreThanEqualExpression.self), .infix("<", 10, LessThanExpression.self), - .infix("<=", 10, LessThanEqualExpression.self), + .infix("<=", 10, LessThanEqualExpression.self) ] - func findOperator(name: String) -> Operator? { - for op in operators { - if op.name == name { - return op - } + for `operator` in operators where `operator`.name == name { + return `operator` } return nil } - indirect enum IfToken { case infix(name: String, bindingPower: Int, operatorType: InfixOperator.Type) case prefix(name: String, bindingPower: Int, operatorType: PrefixOperator.Type) @@ -51,9 +46,9 @@ indirect enum IfToken { return bindingPower case .prefix(_, let bindingPower, _): return bindingPower - case .variable(_): + case .variable: return 0 - case .subExpression(_): + case .subExpression: return 0 case .end: return 0 @@ -64,9 +59,9 @@ indirect enum IfToken { 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 op): + case .prefix(_, let bindingPower, let operatorType): let expression = try parser.expression(bindingPower: bindingPower) - return op.init(expression: expression) + return operatorType.init(expression: expression) case .variable(let variable): return VariableExpression(variable: variable) case .subExpression(let expression): @@ -78,14 +73,14 @@ indirect enum IfToken { func leftDenotation(left: Expression, parser: IfExpressionParser) throws -> Expression { switch self { - case .infix(_, let bindingPower, let op): + case .infix(_, let bindingPower, let operatorType): let right = try parser.expression(bindingPower: bindingPower) - return op.init(lhs: left, rhs: right) + 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(_): + case .subExpression: throw TemplateSyntaxError("'if' expression error: sub expression was called with a left hand side") case .end: throw TemplateSyntaxError("'if' expression error: end") @@ -102,7 +97,6 @@ indirect enum IfToken { } } - final class IfExpressionParser { let tokens: [IfToken] var position: Int = 0 @@ -110,7 +104,7 @@ final class IfExpressionParser { private init(tokens: [IfToken]) { self.tokens = tokens } - + static func parser(components: [String], environment: Environment, token: Token) throws -> IfExpressionParser { return try IfExpressionParser(components: ArraySlice(components), environment: environment, token: token) } @@ -118,7 +112,7 @@ final class IfExpressionParser { private init(components: ArraySlice, environment: Environment, token: Token) throws { var parsedComponents = Set() var bracketsBalance = 0 - self.tokens = try zip(components.indices, components).compactMap { (index, component) in + self.tokens = try zip(components.indices, components).compactMap { index, component in guard !parsedComponents.contains(index) else { return nil } if component == "(" { @@ -139,8 +133,8 @@ final class IfExpressionParser { return nil } else { parsedComponents.insert(index) - if let op = findOperator(name: component) { - switch op { + 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): @@ -152,17 +146,20 @@ final class IfExpressionParser { } } - private static func subExpression(from components: ArraySlice, environment: Environment, token: Token) throws -> (Expression, Int) { + private static func subExpression( + from components: ArraySlice, + environment: Environment, + token: Token + ) throws -> (Expression, Int) { var bracketsBalance = 1 - let subComponents = components - .prefix(while: { - if $0 == "(" { - bracketsBalance += 1 - } else if $0 == ")" { - bracketsBalance -= 1 - } - return bracketsBalance != 0 - }) + let subComponents = components.prefix { + if $0 == "(" { + bracketsBalance += 1 + } else if $0 == ")" { + bracketsBalance -= 1 + } + return bracketsBalance != 0 + } if bracketsBalance > 0 { throw TemplateSyntaxError("'if' expression error: missing closing bracket") } @@ -171,7 +168,7 @@ final class IfExpressionParser { let expression = try expressionParser.parse() return (expression, subComponents.count) } - + var currentToken: IfToken { if tokens.count > position { return tokens[position] @@ -211,7 +208,6 @@ final class IfExpressionParser { } } - /// Represents an if condition and the associated nodes when the condition /// evaluates final class IfCondition { @@ -225,13 +221,12 @@ final class IfCondition { func render(_ context: Context) throws -> String { return try context.push { - return try renderNodes(nodes, context) + try renderNodes(nodes, context) } } } - -class IfNode : NodeType { +class IfNode: NodeType { let conditions: [IfCondition] let token: Token? @@ -291,8 +286,8 @@ class IfNode : NodeType { return IfNode(conditions: [ IfCondition(expression: expression, nodes: trueNodes), - IfCondition(expression: nil, nodes: falseNodes), - ], token: token) + IfCondition(expression: nil, nodes: falseNodes) + ], token: token) } init(conditions: [IfCondition], token: Token? = nil) { diff --git a/Sources/Include.swift b/Sources/Include.swift index 9c82b68..9d49ed3 100644 --- a/Sources/Include.swift +++ b/Sources/Include.swift @@ -1,7 +1,6 @@ import PathKit - -class IncludeNode : NodeType { +class IncludeNode: NodeType { let templateName: Variable let includeContext: String? let token: Token? @@ -34,9 +33,9 @@ class IncludeNode : NodeType { let template = try context.environment.loadTemplate(name: templateName) do { - let subContext = includeContext.flatMap { context[$0] as? [String: Any] } + let subContext = includeContext.flatMap { context[$0] as? [String: Any] } ?? [:] return try context.push(dictionary: subContext) { - return try template.render(context) + try template.render(context) } } catch { if let error = error as? TemplateSyntaxError { @@ -47,4 +46,3 @@ class IncludeNode : NodeType { } } } - diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index e512bfb..611d28c 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -33,7 +33,6 @@ class BlockContext { } } - extension Collection { func any(_ closure: (Iterator.Element) -> Bool) -> Iterator.Element? { for element in self { @@ -46,10 +45,9 @@ extension Collection { } } - -class ExtendsNode : NodeType { +class ExtendsNode: NodeType { let templateName: Variable - let blocks: [String:BlockNode] + let blocks: [String: BlockNode] let token: Token? class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { @@ -66,7 +64,7 @@ class ExtendsNode : NodeType { let blockNodes = parsedNodes.compactMap { $0 as? BlockNode } - let nodes = blockNodes.reduce([String: BlockNode]()) { (accumulator, node) -> [String: BlockNode] in + let nodes = blockNodes.reduce([String: BlockNode]()) { accumulator, node -> [String: BlockNode] in var dict = accumulator dict[node.name] = node return dict @@ -102,7 +100,7 @@ class ExtendsNode : NodeType { // pushes base template and renders it's content // block_context contains all blocks from child templates return try context.push(dictionary: [BlockContext.contextKey: blockContext]) { - return try baseTemplate.render(context) + try baseTemplate.render(context) } } catch { // if error template is already set (see catch in BlockNode) @@ -117,8 +115,7 @@ class ExtendsNode : NodeType { } } - -class BlockNode : NodeType { +class BlockNode: NodeType { let name: String let nodes: [NodeType] let token: Token? @@ -133,7 +130,7 @@ class BlockNode : NodeType { let blockName = bits[1] let nodes = try parser.parse(until(["endblock"])) _ = parser.nextToken() - return BlockNode(name:blockName, nodes:nodes, token: token) + return BlockNode(name: blockName, nodes: nodes, token: token) } init(name: String, nodes: [NodeType], token: Token) { @@ -148,7 +145,7 @@ class BlockNode : NodeType { // render extension node do { return try context.push(dictionary: childContext) { - return try child.render(context) + try child.render(context) } } catch { throw error.withToken(child.token) @@ -163,8 +160,11 @@ class BlockNode : NodeType { var childContext: [String: Any] = [BlockContext.contextKey: blockContext] if let blockSuperNode = child.nodes.first(where: { - if let token = $0.token, case .variable = token.kind, token.contents == "block.super" { return true } - else { return false} + if let token = $0.token, case .variable = token.kind, token.contents == "block.super" { + return true + } else { + return false + } }) { do { // render base node so that its content can be used as part of child node that extends it diff --git a/Sources/KeyPath.swift b/Sources/KeyPath.swift index 7728dcf..98767b7 100644 --- a/Sources/KeyPath.swift +++ b/Sources/KeyPath.swift @@ -24,8 +24,8 @@ final class KeyPath { subscriptLevel = 0 } - for c in variable { - switch c { + for character in variable { + switch character { case "." where subscriptLevel == 0: try foundSeparator() case "[": @@ -33,7 +33,7 @@ final class KeyPath { case "]": try closeBracket() default: - try addCharacter(c) + try addCharacter(character) } } try finish() @@ -90,12 +90,12 @@ final class KeyPath { subscriptLevel -= 1 } - private func addCharacter(_ c: Character) throws { + private func addCharacter(_ character: Character) throws { guard partialComponents.isEmpty || subscriptLevel > 0 else { - throw TemplateSyntaxError("Unexpected character '\(c)' in variable '\(variable)'") + throw TemplateSyntaxError("Unexpected character '\(character)' in variable '\(variable)'") } - current.append(c) + current.append(character) } private func finish() throws { diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index f6fc426..47465f5 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -24,8 +24,9 @@ struct Lexer { self.templateString = templateString self.lines = templateString.components(separatedBy: .newlines).enumerated().compactMap { - guard !$0.element.isEmpty else { return nil } - return (content: $0.element, number: UInt($0.offset + 1), templateString.range(of: $0.element)!) + guard !$0.element.isEmpty, + let range = templateString.range(of: $0.element) else { return nil } + return (content: $0.element, number: UInt($0.offset + 1), range) } } @@ -43,8 +44,8 @@ struct Lexer { guard string.count > 4 else { return "" } let trimmed = String(string.dropFirst(2).dropLast(2)) .components(separatedBy: "\n") - .filter({ !$0.isEmpty }) - .map({ $0.trim(character: " ") }) + .filter { !$0.isEmpty } + .map { $0.trim(character: " ") } .joined(separator: " ") return trimmed } diff --git a/Sources/Loader.swift b/Sources/Loader.swift index 201dbae..f83d9ae 100644 --- a/Sources/Loader.swift +++ b/Sources/Loader.swift @@ -1,13 +1,11 @@ import Foundation import PathKit - public protocol Loader { func loadTemplate(name: String, environment: Environment) throws -> Template func loadTemplate(names: [String], environment: Environment) throws -> Template } - extension Loader { public func loadTemplate(names: [String], environment: Environment) throws -> Template { for name in names { @@ -24,7 +22,6 @@ extension Loader { } } - // A class for loading a template from disk public class FileSystemLoader: Loader, CustomStringConvertible { public let paths: [Path] @@ -35,7 +32,7 @@ public class FileSystemLoader: Loader, CustomStringConvertible { public init(bundle: [Bundle]) { self.paths = bundle.map { - return Path($0.bundlePath) + Path($0.bundlePath) } } @@ -74,7 +71,6 @@ public class FileSystemLoader: Loader, CustomStringConvertible { } } - public class DictionaryLoader: Loader { public let templates: [String: String] @@ -101,7 +97,6 @@ public class DictionaryLoader: Loader { } } - extension Path { func safeJoin(path: Path) throws -> Path { let newPath = self + path @@ -114,7 +109,6 @@ extension Path { } } - class SuspiciousFileOperation: Error { let basePath: Path let path: Path diff --git a/Sources/Node.swift b/Sources/Node.swift index 2805290..8885ff6 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -2,26 +2,27 @@ import Foundation public protocol NodeType { /// Render the node in the given context - func render(_ context:Context) throws -> String + func render(_ context: Context) throws -> String /// 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 { - return try nodes.map { - do { - return try $0.render(context) - } catch { - throw error.withToken($0.token) +public func renderNodes(_ nodes: [NodeType], _ context: Context) throws -> String { + return try nodes + .map { + do { + return try $0.render(context) + } catch { + throw error.withToken($0.token) + } } - }.joined(separator: "") + .joined() } -public class SimpleNode : NodeType { - public let handler:(Context) throws -> String +public class SimpleNode: NodeType { + public let handler: (Context) throws -> String public let token: Token? public init(token: Token, handler: @escaping (Context) throws -> String) { @@ -34,34 +35,31 @@ public class SimpleNode : NodeType { } } - -public class TextNode : NodeType { - public let text:String +public class TextNode: NodeType { + public let text: String public let token: Token? - public init(text:String) { + public init(text: String) { self.text = text self.token = nil } - public func render(_ context:Context) throws -> String { + public func render(_ context: Context) throws -> String { return self.text } } - public protocol Resolvable { func resolve(_ context: Context) throws -> Any? } - -public class VariableNode : NodeType { +public class VariableNode: NodeType { public let variable: Resolvable public var token: Token? let condition: Expression? let elseExpression: Resolvable? - class func parse(_ parser:TokenParser, token:Token) throws -> NodeType { + class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { var components = token.components func hasToken(_ token: String, at index: Int) -> Bool { @@ -121,7 +119,6 @@ public class VariableNode : NodeType { } } - func stringify(_ result: Any?) -> String { if let result = result as? String { return result @@ -144,7 +141,6 @@ func unwrap(_ array: [Any?]) -> [Any] { } else { return item } - } - else { return item as Any } + } else { return item as Any } } } diff --git a/Sources/NowTag.swift b/Sources/NowTag.swift index ac0ccfb..bad6627 100644 --- a/Sources/NowTag.swift +++ b/Sources/NowTag.swift @@ -1,13 +1,12 @@ #if !os(Linux) import Foundation - -class NowNode : NodeType { - let format:Variable +class NowNode: NodeType { + 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 { @@ -17,10 +16,10 @@ class NowNode : NodeType { format = Variable(components[1]) } - return NowNode(format:format, token: token) + return NowNode(format: format, token: token) } - init(format:Variable?, token: Token? = nil) { + init(format: Variable?, token: Token? = nil) { self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"") self.token = token } @@ -28,18 +27,18 @@ class NowNode : NodeType { func render(_ context: Context) throws -> String { let date = Date() let format = try self.format.resolve(context) - var formatter:DateFormatter? + var formatter: DateFormatter if let format = format as? DateFormatter { formatter = format } else if let format = format as? String { formatter = DateFormatter() - formatter!.dateFormat = format + formatter.dateFormat = format } else { return "" } - return formatter!.string(from: date) + return formatter.string(from: date) } } #endif diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 41dd7b9..404b8e2 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -1,10 +1,8 @@ public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) { return { parser, token in if let name = token.components.first { - for tag in tags { - if name == tag { - return true - } + for tag in tags where name == tag { + return true } } @@ -12,7 +10,6 @@ public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) { } } - /// A class for parsing an array of tokens and converts them into a collection of Node's public class TokenParser { public typealias TagParser = (TokenParser, Token) throws -> NodeType @@ -30,11 +27,11 @@ public class TokenParser { return try parse(nil) } - public func parse(_ parse_until:((_ parser:TokenParser, _ token:Token) -> (Bool))?) throws -> [NodeType] { + public func parse(_ parseUntil: ((_ parser: TokenParser, _ token: Token) -> (Bool))?) throws -> [NodeType] { var nodes = [NodeType]() - while tokens.count > 0 { - let token = nextToken()! + while !tokens.isEmpty { + guard let token = nextToken() else { break } switch token.kind { case .text: @@ -42,7 +39,7 @@ public class TokenParser { case .variable: try nodes.append(VariableNode.parse(self, token: token)) case .block: - if let parse_until = parse_until , parse_until(self, token) { + if let parseUntil = parseUntil, parseUntil(self, token) { prependToken(token) return nodes } @@ -65,14 +62,14 @@ public class TokenParser { } public func nextToken() -> Token? { - if tokens.count > 0 { + if !tokens.isEmpty { return tokens.remove(at: 0) } return nil } - public func prependToken(_ token:Token) { + public func prependToken(_ token: Token) { tokens.insert(token, at: 0) } @@ -94,7 +91,6 @@ public class TokenParser { } extension Environment { - func findTag(name: String) throws -> Extension.TagParser { for ext in extensions { if let filter = ext.tags[name] { @@ -118,23 +114,23 @@ extension Environment { } else { throw TemplateSyntaxError(""" Unknown filter '\(name)'. \ - Found similar filters: \(suggestedFilters.map({ "'\($0)'" }).joined(separator: ", ")). + Found similar filters: \(suggestedFilters.map { "'\($0)'" }.joined(separator: ", ")). """) } } private func suggestedFilters(for name: String) -> [String] { - let allFilters = extensions.flatMap({ $0.filters.keys }) + let allFilters = extensions.flatMap { $0.filters.keys } let filtersWithDistance = allFilters - .map({ (filterName: $0, distance: $0.levenshteinDistance(name)) }) + .map { (filterName: $0, distance: $0.levenshteinDistance(name)) } // do not suggest filters which names are shorter than the distance - .filter({ $0.filterName.count > $0.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 }) + return filtersWithDistance.filter { $0.distance == minDistance }.map { $0.filterName } } /// Create filter expression from a string @@ -153,8 +149,14 @@ extension Environment { // 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)) + 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 } @@ -183,9 +185,8 @@ extension Environment { // https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows extension String { - - subscript(_ i: Int) -> Character { - return self[self.index(self.startIndex, offsetBy: i)] + subscript(_ index: Int) -> Character { + return self[self.index(self.startIndex, offsetBy: index)] } func levenshteinDistance(_ target: String) -> Int { @@ -198,19 +199,19 @@ extension String { last = [Int](0...target.count) current = [Int](repeating: 0, count: target.count + 1) - for i in 0.. String { - return try render(Context(dictionary: dictionary, environment: environment)) + return try render(Context(dictionary: dictionary ?? [:], environment: environment)) } } diff --git a/Sources/Tokenizer.swift b/Sources/Tokenizer.swift index d86852f..30f3117 100644 --- a/Sources/Tokenizer.swift +++ b/Sources/Tokenizer.swift @@ -1,6 +1,5 @@ import Foundation - extension String { /// Split a string by a separator leaving quoted phrases together func smartSplit(separator: Character = " ") -> [String] { @@ -10,37 +9,18 @@ extension String { var singleQuoteCount = 0 var doubleQuoteCount = 0 - let specialCharacters = ",|:" - func appendWord(_ word: String) { - if components.count > 0 { - if let precedingChar = components.last?.last, specialCharacters.contains(precedingChar) { - 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())) - } else if word != "(" && word.last == "(" || word != ")" && word.last == ")" { - appendWord(String(word.dropLast())) - components.append(String(word.suffix(1))) - } else { - components.append(word) - } - } else { - components.append(word) - } - } - for character in self { - if character == "'" { singleQuoteCount += 1 } - else if character == "\"" { doubleQuoteCount += 1 } + if character == "'" { + singleQuoteCount += 1 + } else if character == "\"" { + doubleQuoteCount += 1 + } if character == separate { - if separate != separator { word.append(separate) } else if (singleQuoteCount % 2 == 0 || doubleQuoteCount % 2 == 0) && !word.isEmpty { - appendWord(word) + appendWord(word, to: &components) word = "" } @@ -54,11 +34,33 @@ extension String { } if !word.isEmpty { - appendWord(word) + appendWord(word, to: &components) } return components } + + private func appendWord(_ word: String, to components: inout [String]) { + let specialCharacters = ",|:" + + if !components.isEmpty { + if let precedingChar = components.last?.last, specialCharacters.contains(precedingChar) { + 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 { @@ -72,7 +74,7 @@ public struct SourceMap: Equatable { static let unknown = SourceMap() - public static func ==(lhs: SourceMap, rhs: SourceMap) -> Bool { + public static func == (lhs: SourceMap, rhs: SourceMap) -> Bool { return lhs.filename == rhs.filename && lhs.location == rhs.location } } @@ -88,20 +90,20 @@ public class Token: Equatable { /// A token representing a template block. case block } - + public let contents: String public let kind: Kind public let sourceMap: SourceMap - + /// 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) { self.contents = contents self.kind = kind self.sourceMap = sourceMap } - + /// A token representing a piece of text. public static func text(value: String, at sourceMap: SourceMap) -> Token { return Token(contents: value, kind: .text, sourceMap: sourceMap) @@ -121,9 +123,8 @@ public class Token: Equatable { public static func block(value: String, at sourceMap: SourceMap) -> Token { return Token(contents: value, kind: .block, sourceMap: sourceMap) } - + public static func == (lhs: Token, rhs: Token) -> Bool { return lhs.contents == rhs.contents && lhs.kind == rhs.kind && lhs.sourceMap == rhs.sourceMap } - } diff --git a/Sources/Variable.swift b/Sources/Variable.swift index b2531b9..44569df 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -1,15 +1,13 @@ import Foundation - typealias Number = Float - -class FilterExpression : Resolvable { +class FilterExpression: Resolvable { let filters: [(FilterType, [Variable])] let variable: Variable init(token: String, environment: Environment) throws { - let bits = token.smartSplit(separator: "|").map({ String($0).trim(character: " ") }) + let bits = token.smartSplit(separator: "|").map { String($0).trim(character: " ") } if bits.isEmpty { throw TemplateSyntaxError("Variable tags must include at least 1 argument") } @@ -32,15 +30,15 @@ class FilterExpression : Resolvable { func resolve(_ context: Context) throws -> Any? { let result = try variable.resolve(context) - return try filters.reduce(result) { x, y in - let arguments = try y.1.map { try $0.resolve(context) } - return try y.0.invoke(value: x, 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 struct Variable: Equatable, Resolvable { public let variable: String /// Create a variable with a string representing the variable @@ -48,16 +46,8 @@ public struct Variable : Equatable, Resolvable { self.variable = variable } - // Split the lookup string and resolve references if possible - fileprivate func lookup(_ context: Context) throws -> [String] { - let keyPath = KeyPath(variable, in: context) - return try keyPath.parse() - } - /// Resolve the variable in the given context public func resolve(_ context: Context) throws -> Any? { - var current: Any? = context - if (variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\"")) { // String literal return String(variable[variable.index(after: variable.startIndex) ..< variable.index(before: variable.endIndex)]) @@ -75,35 +65,11 @@ public struct Variable : Equatable, Resolvable { return bool } + var current: Any? = context for bit in try lookup(context) { - current = normalize(current) + current = resolve(bit: bit, context: current) - if let context = current as? Context { - current = context[bit] - } else if let dictionary = current as? [String: Any] { - if bit == "count" { - current = dictionary.count - } else { - current = dictionary[bit] - } - } else if let array = current as? [Any] { - current = resolveCollection(array, bit: bit) - } else if let string = current as? String { - current = resolveCollection(string, bit: bit) - } else if let object = current as? NSObject { // NSKeyValueCoding - #if os(Linux) - return nil - #else - if object.responds(to: Selector(bit)) { - current = object.value(forKey: bit) - } - #endif - } else if let value = current { - current = Mirror(reflecting: value).getValue(for: bit) - if current == nil { - return nil - } - } else { + if current == nil { return nil } } @@ -116,23 +82,66 @@ public struct Variable : Equatable, Resolvable { return normalize(current) } -} -private func resolveCollection(_ collection: T, bit: String) -> Any? { - if let index = Int(bit) { - if index >= 0 && index < collection.count { - return collection[collection.index(collection.startIndex, offsetBy: index)] + // 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) + + 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 object = context as? NSObject { // NSKeyValueCoding + #if os(Linux) + return nil + #else + if object.responds(to: Selector(bit)) { + return object.value(forKey: bit) + } + #endif + } else if let value = context { + return Mirror(reflecting: value).getValue(for: bit) + } + + 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 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 } - } 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 } } @@ -142,6 +151,7 @@ private func resolveCollection(_ collection: T, bit: String) -> A /// 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 init?(_ token: String, environment: Environment) throws { @@ -165,24 +175,23 @@ public struct RangeVariable: Resolvable { } public func resolve(_ context: Context) throws -> Any? { - let fromResolved = try from.resolve(context) - let toResolved = try to.resolve(context) + let lowerResolved = try from.resolve(context) + let upperResolved = try to.resolve(context) - guard let from = fromResolved.flatMap(toNumber(value:)).flatMap(Int.init) else { - throw TemplateSyntaxError("'from' value is not an Integer (\(fromResolved ?? "nil"))") + guard let lower = lowerResolved.flatMap(toNumber(value:)).flatMap(Int.init) else { + throw TemplateSyntaxError("'from' value is not an Integer (\(lowerResolved ?? "nil"))") } - guard let to = toResolved.flatMap(toNumber(value:)).flatMap(Int.init) else { - throw TemplateSyntaxError("'to' value is not an Integer (\(toResolved ?? "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(from, to)...max(from, to) - return from > to ? 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() @@ -195,19 +204,19 @@ protocol Normalizable { func normalize() -> Any? } -extension Array : Normalizable { +extension Array: Normalizable { func normalize() -> Any? { return map { $0 as Any } } } -extension NSArray : Normalizable { +extension NSArray: Normalizable { func normalize() -> Any? { return map { $0 as Any } } } -extension Dictionary : Normalizable { +extension Dictionary: Normalizable { func normalize() -> Any? { var dictionary: [String: Any] = [:] @@ -235,7 +244,7 @@ func parseFilterComponents(token: String) -> (String, [Variable]) { extension Mirror { func getValue(for key: String) -> Any? { - let result = descendant(key) ?? Int(key).flatMap({ descendant($0) }) + 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) @@ -267,5 +276,3 @@ extension Optional: AnyOptional { } } } - - diff --git a/Sources/_SwiftSupport.swift b/Sources/_SwiftSupport.swift index 5333659..2441350 100644 --- a/Sources/_SwiftSupport.swift +++ b/Sources/_SwiftSupport.swift @@ -10,16 +10,16 @@ import Foundation #if !swift(>=4.1) public extension Collection { - func index(_ i: Self.Index, offsetBy n: Int) -> Self.Index { - let indexDistance = Self.IndexDistance(n) - return index(i, offsetBy: indexDistance) + func index(_ index: Self.Index, offsetBy offset: Int) -> Self.Index { + let indexDistance = Self.IndexDistance(offset) + return self.index(index, offsetBy: indexDistance) } } #endif #if !swift(>=4.1) public extension TemplateSyntaxError { - public static func ==(lhs: TemplateSyntaxError, rhs: TemplateSyntaxError) -> Bool { + public static func == (lhs: TemplateSyntaxError, rhs: TemplateSyntaxError) -> Bool { return lhs.reason == rhs.reason && lhs.description == rhs.description && lhs.token == rhs.token && @@ -31,7 +31,7 @@ public extension TemplateSyntaxError { #if !swift(>=4.1) public extension Variable { - public static func ==(lhs: Variable, rhs: Variable) -> Bool { + public static func == (lhs: Variable, rhs: Variable) -> Bool { return lhs.variable == rhs.variable } } diff --git a/Tests/StencilTests/.swiftlint.yml b/Tests/StencilTests/.swiftlint.yml new file mode 100644 index 0000000..46c1b62 --- /dev/null +++ b/Tests/StencilTests/.swiftlint.yml @@ -0,0 +1,3 @@ +disabled_rules: # rule identifiers to exclude from running + - type_body_length + - file_length diff --git a/Tests/StencilTests/ContextSpec.swift b/Tests/StencilTests/ContextSpec.swift index 7e20cbd..191529c 100644 --- a/Tests/StencilTests/ContextSpec.swift +++ b/Tests/StencilTests/ContextSpec.swift @@ -1,14 +1,11 @@ -import XCTest import Spectre @testable import Stencil +import XCTest - -class ContextTests: XCTestCase { - - func testContext() { - describe("Context") { - var context: Context! - +final class ContextTests: XCTestCase { + func testContextSubscripting() { + describe("Context Subscripting") { + var context = Context() $0.before { context = Context(dictionary: ["name": "Kyle"]) } @@ -41,6 +38,15 @@ class ContextTests: XCTestCase { try expect(context["name"] as? String) == "Katie" } } + } + } + + func testContextRestoration() { + describe("Context Restoration") { + var context = Context() + $0.before { + context = Context(dictionary: ["name": "Kyle"]) + } $0.it("allows you to pop to restore previous state") { context.push { diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index f2a7d06..2acfa30 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -1,342 +1,394 @@ -import XCTest -import Spectre import PathKit +import Spectre @testable import Stencil +import XCTest -class EnvironmentTests: XCTestCase { - func testEnvironment() { - describe("Environment") { - var environment: Environment! - var template: Template! +final class EnvironmentTests: XCTestCase { + var environment = Environment(loader: ExampleLoader()) + var template: Template = "" - $0.before { - environment = Environment(loader: ExampleLoader()) - template = nil - } - - $0.it("can load a template from a name") { - let template = try environment.loadTemplate(name: "example.html") - try expect(template.name) == "example.html" - } - - $0.it("can load a template from a names") { - let template = try environment.loadTemplate(names: ["first.html", "example.html"]) - try expect(template.name) == "example.html" - } - - $0.it("can render a template from a string") { - let result = try environment.renderTemplate(string: "Hello World") - try expect(result) == "Hello World" - } - - $0.it("can render a template from a file") { - let result = try environment.renderTemplate(name: "example.html") - try expect(result) == "Hello World!" - } - - $0.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" - } - - 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 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(environment.render(template: 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) - } - - $0.context("given syntax error") { - - $0.it("reports syntax error on invalid for tag syntax") { - template = "Hello {% for name in %}{{ name }}, {% endfor %}!" - try expectError(reason: "'for' statements should use the syntax: `for in [where ]`.", token: "for name in") - } - - $0.it("reports syntax error on missing endfor") { - template = "{% for name in names %}{{ name }}" - try expectError(reason: "`endfor` was not found.", token: "for name in names") - } - - $0.it("reports syntax error on unknown tag") { - template = "{% for name in names %}{{ name }}{% end %}" - try expectError(reason: "Unknown template tag 'end'", token: "end") - } - - } - - $0.context("given unknown filter") { - - $0.it("reports syntax error in for tag") { - template = "{% for name in names|unknown %}{{ name }}{% endfor %}" - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "names|unknown") - } - - $0.it("reports syntax error in for-where tag") { - template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}" - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") - } - - $0.it("reports syntax error in if tag") { - template = "{% if name|unknown %}{{ name }}{% endif %}" - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") - } - - $0.it("reports syntax error in elif tag") { - template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}" - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") - } - - $0.it("reports syntax error in ifnot tag") { - template = "{% ifnot name|unknown %}{{ name }}{% endif %}" - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") - } - - $0.it("reports syntax error in filter tag") { - template = "{% filter unknown %}Text{% endfilter %}" - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "filter unknown") - } - - $0.it("reports syntax error in variable tag") { - template = "{{ name|unknown }}" - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") - } - - } - - $0.context("given rendering error") { - - $0.it("reports rendering error in variable filter") { - let filterExtension = Extension() - filterExtension.registerFilter("throw") { (value: Any?) in - throw TemplateSyntaxError("filter error") - } - environment.extensions += [filterExtension] - - template = Template(templateString: "{{ name|throw }}", environment: environment) - try expectError(reason: "filter error", token: "name|throw") - } - - $0.it("reports rendering error in filter tag") { - let filterExtension = Extension() - filterExtension.registerFilter("throw") { (value: Any?) in - throw TemplateSyntaxError("filter error") - } - environment.extensions += [filterExtension] - - template = Template(templateString: "{% filter throw %}Test{% endfilter %}", environment: environment) - try expectError(reason: "filter error", token: "filter throw") - } - - $0.it("reports rendering error in simple tag") { - let tagExtension = Extension() - tagExtension.registerSimpleTag("simpletag") { context in - throw TemplateSyntaxError("simpletag error") - } - environment.extensions += [tagExtension] - - template = Template(templateString: "{% simpletag %}", environment: environment) - try expectError(reason: "simpletag error", token: "simpletag") - } - - $0.it("reporsts passing argument to simple filter") { - template = "{{ name|uppercase:5 }}" - try expectError(reason: "cannot invoke filter with an argument", token: "name|uppercase:5") - } - - $0.it("reports rendering error in custom tag") { - let tagExtension = Extension() - tagExtension.registerTag("customtag") { parser, token in - return ErrorNode(token: token) - } - environment.extensions += [tagExtension] - - template = Template(templateString: "{% customtag %}", environment: environment) - try expectError(reason: "Custom Error", token: "customtag") - } - - $0.it("reports rendering error in for body") { - let tagExtension = Extension() - tagExtension.registerTag("customtag") { parser, token in - return ErrorNode(token: token) - } - environment.extensions += [tagExtension] - - template = Template(templateString: "{% for name in names %}{% customtag %}{% endfor %}", environment: environment) - try expectError(reason: "Custom Error", token: "customtag") - } - - $0.it("reports rendering error in block") { - let tagExtension = Extension() - tagExtension.registerTag("customtag") { parser, token in - return ErrorNode(token: token) - } - environment.extensions += [tagExtension] - - template = Template(templateString: "{% block some %}{% customtag %}{% endblock %}", environment: environment) - try expectError(reason: "Custom Error", token: "customtag") - } - } - - $0.context("given included template") { - let path = Path(#file) + ".." + "fixtures" - let loader = FileSystemLoader(paths: [path]) - var environment = Environment(loader: loader) - var template: Template! - var includedTemplate: Template! - - $0.before { - environment = Environment(loader: loader) - template = nil - includedTemplate = nil - } - - 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!] - - let error = try expect(environment.render(template: 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) - } - - $0.it("reports syntax error in included template") { - 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") - } - - $0.it("reports runtime error in included template") { - let filterExtension = Extension() - filterExtension.registerFilter("unknown", filter: { (_: 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") - - try expectError(reason: "filter error", - token: "include \"invalid-include.html\"", - includedToken: "target|unknown") - } - - } - - $0.context("given base and child templates") { - let path = Path(#file) + ".." + "fixtures" - let loader = FileSystemLoader(paths: [path]) - var environment: Environment! - var childTemplate: Template! - var baseTemplate: Template! - - $0.before { - environment = Environment(loader: loader) - childTemplate = nil - baseTemplate = nil - } - - 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!] - } - let error = try expect(environment.render(template: 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) - } - - $0.it("reports syntax error in base template") { - 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") - } - - $0.it("reports runtime error in base template") { - let filterExtension = Extension() - filterExtension.registerFilter("unknown", filter: { (_: 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") - - try expectError(reason: "filter error", - childToken: "block.super", - baseToken: "target|unknown") - } - - $0.it("reports syntax error in child template") { - 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) - } - - $0.it("reports runtime error in child template") { - let filterExtension = Extension() - filterExtension.registerFilter("unknown", filter: { (_: 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) - - try expectError(reason: "filter error", - childToken: "target|unknown", - baseToken: nil) - } - - } + 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) + } + + environment = Environment(loader: ExampleLoader()) + environment.extensions += [errorExtension] + template = "" + } + + 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" + } + } + + 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("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" + } + } + + 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 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" + ) + } + + 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 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 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" + ) + } + } + + 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 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: "cannot 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 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") + } + } + + 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) + } +} + +final class EnvironmentIncludeTemplateTests: XCTestCase { + var environment = Environment(loader: ExampleLoader()) + var template: Template = "" + var includedTemplate: Template = "" + + override func setUp() { + super.setUp() + + let path = Path(#file) + ".." + "fixtures" + let loader = FileSystemLoader(paths: [path]) + environment = Environment(loader: loader) + template = "" + includedTemplate = "" + } + + 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") + } + + 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") + + 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 } + + 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) + } +} + +final class EnvironmentBaseAndChildTemplateTests: XCTestCase { + var environment = Environment(loader: ExampleLoader()) + var childTemplate: Template = "" + var baseTemplate: Template = "" + + override func setUp() { + super.setUp() + + let path = Path(#file) + ".." + "fixtures" + let loader = FileSystemLoader(paths: [path]) + environment = Environment(loader: loader) + childTemplate = "" + baseTemplate = "" + } + + 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") + } + + 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") + + try expectError(reason: "filter error", + childToken: "block.super", + baseToken: "target|unknown") + } + + 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) + } + + 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 + ) + + 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) } } extension Expectation { @discardableResult func toThrow() throws -> T { - var thrownError: Error? = nil + var thrownError: Error? do { _ = try expression() @@ -356,7 +408,20 @@ extension Expectation { } } -fileprivate class ExampleLoader: Loader { +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: []) + } +} + +private class ExampleLoader: Loader { func loadTemplate(name: String, environment: Environment) throws -> Template { if name == "example.html" { return Template(templateString: "Hello World!", environment: environment, name: name) @@ -366,8 +431,8 @@ fileprivate class ExampleLoader: Loader { } } - -class CustomTemplate: Template { +private class CustomTemplate: Template { + // swiftlint:disable discouraged_optional_collection override func render(_ dictionary: [String: Any]? = nil) throws -> String { return "here" } diff --git a/Tests/StencilTests/ExpressionSpec.swift b/Tests/StencilTests/ExpressionSpec.swift index a115555..6659d39 100644 --- a/Tests/StencilTests/ExpressionSpec.swift +++ b/Tests/StencilTests/ExpressionSpec.swift @@ -1,345 +1,355 @@ -import XCTest import Spectre @testable import Stencil - -class ExpressionsTests: XCTestCase { - func testExpressions() { - describe("Expression") { - - func parseExpression(components: [String]) throws -> Expression { - let parser = try IfExpressionParser.parser(components: components, environment: Environment(), token: .text(value: "", at: .unknown)) - return try parser.parse() - } - - $0.describe("VariableExpression") { - let expression = VariableExpression(variable: Variable("value")) - - $0.it("evaluates to true when value is not nil") { - let context = Context(dictionary: ["value": "known"]) - try expect(try expression.evaluate(context: context)).to.beTrue() - } - - $0.it("evaluates to false when value is unset") { - let context = Context() - try expect(try expression.evaluate(context: context)).to.beFalse() - } - - $0.it("evaluates to true when array variable is not empty") { - let items: [[String: Any]] = [["key":"key1","value":42],["key":"key2","value":1337]] - let context = Context(dictionary: ["value": [items]]) - try expect(try expression.evaluate(context: context)).to.beTrue() - } - - $0.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() - } - - $0.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() - } - - $0.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() - } - - $0.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() - } - - $0.it("evaluates to true with string") { - let context = Context(dictionary: ["value": "test"]) - try expect(try expression.evaluate(context: context)).to.beTrue() - } - - $0.it("evaluates to false when empty string") { - let context = Context(dictionary: ["value": ""]) - try expect(try expression.evaluate(context: context)).to.beFalse() - } - - $0.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": 0]) - try expect(try expression.evaluate(context: negativeContext)).to.beFalse() - } - - $0.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() - } - - $0.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() - } - - $0.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() - } - - $0.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() - } - - $0.it("evaluates to false when uint is 0") { - let context = Context(dictionary: ["value": UInt(0)]) - try expect(try expression.evaluate(context: context)).to.beFalse() - } - } - - $0.describe("NotExpression") { - $0.it("returns truthy for positive expressions") { - let expression = NotExpression(expression: StaticExpression(value: true)) - try expect(expression.evaluate(context: Context())).to.beFalse() - } - - $0.it("returns falsy for negative expressions") { - let expression = NotExpression(expression: StaticExpression(value: false)) - try expect(expression.evaluate(context: Context())).to.beTrue() - } - } - - $0.describe("expression parsing") { - $0.it("can parse a variable expression") { - let expression = try parseExpression(components: ["value"]) - try expect(expression.evaluate(context: Context())).to.beFalse() - try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue() - } - - $0.it("can parse a not expression") { - let expression = try parseExpression(components: ["not", "value"]) - try expect(expression.evaluate(context: Context())).to.beTrue() - try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse() - } - - $0.describe("and expression") { - let expression = try! parseExpression(components: ["lhs", "and", "rhs"]) - - $0.it("evaluates to false with lhs false") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse() - } - - $0.it("evaluates to false with rhs false") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse() - } - - $0.it("evaluates to false with lhs and rhs false") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse() - } - - $0.it("evaluates to true with lhs and rhs true") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() - } - } - - $0.describe("or expression") { - let expression = try! parseExpression(components: ["lhs", "or", "rhs"]) - - $0.it("evaluates to true with lhs true") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue() - } - - $0.it("evaluates to true with rhs true") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beTrue() - } - - $0.it("evaluates to true with lhs and rhs true") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() - } - - $0.it("evaluates to false with lhs and rhs false") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse() - } - } - - $0.describe("equality expression") { - let expression = try! parseExpression(components: ["lhs", "==", "rhs"]) - - $0.it("evaluates to true with equal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue() - } - - $0.it("evaluates to false with non equal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beFalse() - } - - $0.it("evaluates to true with nils") { - try expect(expression.evaluate(context: Context(dictionary: [:]))).to.beTrue() - } - - $0.it("evaluates to true with numbers") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.0]))).to.beTrue() - } - - $0.it("evaluates to false with non equal numbers") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.1]))).to.beFalse() - } - - $0.it("evaluates to true with booleans") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() - } - - $0.it("evaluates to false with falsy booleans") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse() - } - - $0.it("evaluates to false with different types") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": 1]))).to.beFalse() - } - } - - $0.describe("inequality expression") { - let expression = try! parseExpression(components: ["lhs", "!=", "rhs"]) - - $0.it("evaluates to true with inequal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue() - } - - $0.it("evaluates to false with equal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "b", "rhs": "b"]))).to.beFalse() - } - } - - $0.describe("more than expression") { - let expression = try! parseExpression(components: ["lhs", ">", "rhs"]) - - $0.it("evaluates to true with lhs > rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue() - } - - $0.it("evaluates to false with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse() - } - } - - $0.describe("more than equal expression") { - let expression = try! parseExpression(components: ["lhs", ">=", "rhs"]) - - $0.it("evaluates to true with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() - } - - $0.it("evaluates to false with lhs < rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.1]))).to.beFalse() - } - } - - $0.describe("less than expression") { - let expression = try! parseExpression(components: ["lhs", "<", "rhs"]) - - $0.it("evaluates to true with lhs < rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue() - } - - $0.it("evaluates to false with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse() - } - } - - $0.describe("less than equal expression") { - let expression = try! parseExpression(components: ["lhs", "<=", "rhs"]) - - $0.it("evaluates to true with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() - } - - $0.it("evaluates to false with lhs > rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.1, "rhs": 5.0]))).to.beFalse() - } - } - - $0.describe("multiple expression") { - let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"]) - - $0.it("evaluates to true with one") { - try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue() - } - - $0.it("evaluates to true with one and three") { - try expect(expression.evaluate(context: Context(dictionary: ["one": true, "three": true]))).to.beTrue() - } - - $0.it("evaluates to true with two") { - try expect(expression.evaluate(context: Context(dictionary: ["two": true]))).to.beTrue() - } - - $0.it("evaluates to false with two and three") { - try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse() - } - - $0.it("evaluates to false with two and three") { - try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse() - } - - $0.it("evaluates to false with nothing") { - try expect(expression.evaluate(context: Context())).to.beFalse() - } - } - - $0.describe("in expression") { - let expression = try! parseExpression(components: ["lhs", "in", "rhs"]) - - $0.it("evaluates to true when rhs contains lhs") { - 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() - } - - $0.it("evaluates to false when rhs does not contain lhs") { - 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() - } - } - - $0.describe("sub expression") { - $0.it("evaluates correctly") { - let context = Context(dictionary: ["one": false, "two": false, "three": true, "four": true]) - - let expression = try! parseExpression(components: ["one", "and", "two", "or", "three", "and", "four"]) - let expressionWithBrackets = try! parseExpression(components: ["one", "and", "(", "(", "two", ")", "or", "(", "three", "and", "four", ")", ")"]) - - try expect(expression.evaluate(context: context)).to.beTrue() - try expect(expressionWithBrackets.evaluate(context: context)).to.beFalse() - - let notExpression = try! parseExpression(components: ["not", "one", "or", "three"]) - let notExpressionWithBrackets = try! parseExpression(components: ["not", "(", "one", "or", "three", ")"]) - - try expect(notExpression.evaluate(context: context)).to.beTrue() - try expect(notExpressionWithBrackets.evaluate(context: context)).to.beFalse() - } - - $0.it("fails when brackets are not balanced") { - try expect(parseExpression(components: ["(", "lhs", "and", "rhs"])) - .toThrow(TemplateSyntaxError("'if' expression error: missing closing bracket")) - try expect(parseExpression(components: [")", "lhs", "and", "rhs"])) - .toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket")) - try expect(parseExpression(components: ["lhs", "and", "rhs", ")"])) - .toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket")) - try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", "("])) - .toThrow(TemplateSyntaxError("'if' expression error: missing closing bracket")) - try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", ")"])) - .toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket")) - try expect(parseExpression(components: ["(", "lhs", "and", ")"])) - .toThrow(TemplateSyntaxError("'if' expression error: end")) - try expect(parseExpression(components: ["(", "and", "rhs", ")"])) - .toThrow(TemplateSyntaxError("'if' expression error: infix operator 'and' doesn't have a left hand side")) - } - } - } +import XCTest + +final class ExpressionsTests: XCTestCase { + let parser = TokenParser(tokens: [], environment: Environment()) + + private func makeExpression(_ components: [String]) -> 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: StaticExpression(value: true)) + try expect(expression.evaluate(context: Context())).to.beFalse() + } + + it("returns falsy for negative expressions") { + let expression = NotExpression(expression: StaticExpression(value: 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 4a6dedf..d2ac102 100644 --- a/Tests/StencilTests/FilterSpec.swift +++ b/Tests/StencilTests/FilterSpec.swift @@ -1,413 +1,445 @@ -import XCTest import Spectre @testable import Stencil +import XCTest -class FilterTests: XCTestCase { - func testFilter() { - describe("template filters") { - let context: [String: Any] = ["name": "Kyle"] +final class FilterTests: XCTestCase { + func testRegistration() { + let context: [String: Any] = ["name": "Kyle"] - $0.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)" - } - - return nil + let repeatExtension = Extension() + repeatExtension.registerFilter("repeat") { (value: Any?) in + if let value = value as? String { + return "\(value) \(value)" } - let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))) - try expect(result) == "Kyle Kyle" - } - - $0.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 negativeResult = try Template(templateString: "{{ value|isNotPositive }}") - .render(Context(dictionary: ["value": -1], environment: Environment(extensions: [repeatExtension]))) - try expect(negativeResult) == "true" + return nil } - $0.it("allows you to register a custom filter which accepts single argument") { + 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 + } + + 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" + } + + 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)) + } + + 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"] + + 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" + } + + 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"' }} + """) + + 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 ?? "")" + } + + 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:'value1, "value2"' }} + {{ name|repeat:'value"1"',"value'2'",'(key, value)' }} """) let repeatExtension = Extension() repeatExtension.registerFilter("repeat") { value, arguments in - if !arguments.isEmpty { - return "\(value!) \(value!) with args \(arguments.first!!)" - } - - return nil + 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]))) + let result = try template.render(Context( + dictionary: context, + environment: Environment(extensions: [repeatExtension]) + )) try expect(result) == """ - Kyle Kyle with args value1, "value2" + Kyle Kyle with args 0: value"1", 1: value'2', 2: (key, value) """ - } - - $0.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 - if !arguments.isEmpty { - return "\(value!) \(value!) with args 0: \(arguments[0]!), 1: \(arguments[1]!), 2: \(arguments[2]!)" - } - - return nil - } - - 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) - """ - } - - $0.it("allows you to register a custom which throws") { - let template = Template(templateString: "{{ name|repeat }}") - let repeatExtension = Extension() - repeatExtension.registerFilter("repeat") { (value: 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)) - } - - $0.it("allows you to override a default filter") { - let template = Template(templateString: "{{ name|join }}") - - let repeatExtension = Extension() - repeatExtension.registerFilter("join") { (value: Any?) in - return "joined" - } - - let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))) - try expect(result) == "joined" - } - - $0.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" - } - - $0.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() - } } - describe("string filters") { - $0.context("given string") { - $0.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" - } - - $0.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" - } - - $0.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" - } - } - - $0.context("given array of strings") { - $0.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"] - """ - } - - $0.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"] - """ - } - - $0.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"] - """ - } - } - } - - describe("default filter") { + it("allows whitespace in expression") { let template = Template(templateString: """ - Hello {{ name|default:"World" }} - """) - - $0.it("shows the variable value") { - let result = try template.render(Context(dictionary: ["name": "Kyle"])) - try expect(result) == "Hello Kyle" - } - - $0.it("shows the default value") { - let result = try template.render(Context(dictionary: [:])) - try expect(result) == "Hello World" - } - - $0.it("supports multiple defaults") { - let template = Template(templateString: """ - Hello {{ name|default:a,b,c,"World" }} + {{ value | join : ", " }} """) - let result = try template.render(Context(dictionary: [:])) - try expect(result) == "Hello World" - } + let result = try template.render(Context(dictionary: ["value": ["One", "Two"]])) + try expect(result) == "One, Two" + } + } - $0.it("can use int as default") { - let template = Template(templateString: "{{ value|default:1 }}") - let result = try template.render(Context(dictionary: [:])) - try expect(result) == "1" - } - - $0.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" - } - - $0.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 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" } - describe("join filter") { + 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" + } + } + + 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 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" }} + """) + + 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("supports multiple defaults") { let template = Template(templateString: """ - {{ value|join:", " }} + Hello {{ name|default:a,b,c,"World" }} """) - - $0.it("joins a collection of strings") { - let result = try template.render(Context(dictionary: ["value": ["One", "Two"]])) - try expect(result) == "One, Two" - } - - $0.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" - } - - $0.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" - } - - $0.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" - } + let result = try template.render(Context(dictionary: [:])) + try expect(result) == "Hello World" } - describe("split filter") { + 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("checks for underlying nil value correctly") { let template = Template(templateString: """ - {{ value|split:", " }} + 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" + } + } - $0.it("split a string into array") { - let result = try template.render(Context(dictionary: ["value": "One, Two"])) - try expect(result) == """ - ["One", "Two"] - """ - } + func testJoinFilter() { + let template = Template(templateString: """ + {{ value|join:", " }} + """) - $0.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("joins a collection of strings") { + let result = try template.render(Context(dictionary: ["value": ["One", "Two"]])) + try expect(result) == "One, Two" } - - describe("filter suggestion") { - var template: Template! - var filterExtension: Extension! - - 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 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 environment = Environment(extensions: [filterExtension]) - - 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) - } - - $0.it("made for unknown filter") { - template = Template(templateString: "{{ value|unknownFilter }}") - - filterExtension = Extension() - filterExtension.registerFilter("knownFilter") { value, _ in value } - - try expectError(reason: "Unknown filter 'unknownFilter'. Found similar filters: 'knownFilter'.", token: "value|unknownFilter") - } - - $0.it("made for multiple similar filters") { - template = Template(templateString: "{{ value|lowerFirst }}") - - filterExtension = Extension() - filterExtension.registerFilter("lowerFirstWord") { value, _ in value } - filterExtension.registerFilter("lowerFirstLetter") { value, _ in value } - - try expectError(reason: "Unknown filter 'lowerFirst'. Found similar filters: 'lowerFirstWord', 'lowercase'.", token: "value|lowerFirst") - } - - $0.it("not made when can't find similar filter") { - template = Template(templateString: "{{ value|unknownFilter }}") - try expectError(reason: "Unknown filter 'unknownFilter'. Found similar filters: 'lowerFirstWord'.", token: "value|unknownFilter") - } - + 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" + } - describe("indent filter") { - $0.it("indents content") { - let template = Template(templateString: """ - {{ value|indent:2 }} - """) - let result = try template.render(Context(dictionary: ["value": """ - One - Two - """])) - try expect(result) == """ - One - Two - """ - } + 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" + } + } - $0.it("can indent with arbitrary character") { - let template = Template(templateString: """ - {{ value|indent:2,"\t" }} - """) - let result = try template.render(Context(dictionary: ["value": """ - One - Two - """])) - try expect(result) == """ - One - \t\tTwo - """ - } + func testSplitFilter() { + let template = Template(templateString: """ + {{ value|split:", " }} + """) - $0.it("can indent first line") { - let template = Template(templateString: """ - {{ value|indent:2," ",true }} - """) - 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"] + """ + } - $0.it("does not indent empty lines") { - let template = Template(templateString: """ - {{ value|indent }} - """) - let result = try template.render(Context(dictionary: ["value": """ - One + 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 } + + 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 } + + 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 } + + 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: """ + {{ value|indent:2 }} + """) + let result = try template.render(Context(dictionary: ["value": """ + One + Two + """])) + try expect(result) == """ + One + Two + """ + } + + func testIndentWithArbitraryCharacter() throws { + let template = Template(templateString: """ + {{ value|indent:2,"\t" }} + """) + let result = try template.render(Context(dictionary: ["value": """ + One + Two + """])) + 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": """ + One + Two + """])) + try expect(result) == """ + One + Two + """ + } + + func testIndentNotEmptyLines() throws { + let template = Template(templateString: """ + {{ value|indent }} + """) + let result = try template.render(Context(dictionary: ["value": """ + One + + + Two + + + """])) + try expect(result) == """ + One Two - """])) - try expect(result) == """ - One + """ + } - - Two - - - """ - } + 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" } - - describe("dynamic filter") { - - $0.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" - } - - $0.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" - } - - $0.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("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() + } + } + + 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 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) } } diff --git a/Tests/StencilTests/FilterTagSpec.swift b/Tests/StencilTests/FilterTagSpec.swift index 5423747..d9a82b8 100644 --- a/Tests/StencilTests/FilterTagSpec.swift +++ b/Tests/StencilTests/FilterTagSpec.swift @@ -1,51 +1,54 @@ -import XCTest import Spectre import Stencil +import XCTest -class FilterTagTests: XCTestCase { +final class FilterTagTests: XCTestCase { func testFilterTag() { - describe("Filter Tag") { - $0.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 use a filter") { + let template = Template(templateString: "{% filter uppercase %}Test{% endfilter %}") + let result = try template.render() + try expect(result) == "TEST" + } - $0.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" + } - $0.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() + } - $0.it("can render filters with arguments") { - let ext = Extension() - ext.registerFilter("split", filter: { - return ($0 as! String).components(separatedBy: $1[0] as! String) - }) - let env = Environment(extensions: [ext]) - let result = try env.renderTemplate(string: """ + it("can render filters with arguments") { + let ext = Extension() + ext.registerFilter("split") { + guard let value = $0 as? String, + let argument = $1.first as? String else { return $0 } + 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" - } + try expect(result) == "1;2" + } - $0.it("can render filters with quote as an argument") { - let ext = Extension() - ext.registerFilter("replace", filter: { - print($1[0] as! String) - return ($0 as! String).replacingOccurrences(of: $1[0] as! String, with: $1[1] as! String) - }) - 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") { + guard let value = $0 as? String, + $1.count == 2, + let search = $1.first as? String, + let replacement = $1.last as? String else { return $0 } + 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 d3bfa8c..d3fc65e 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -1,358 +1,341 @@ -import XCTest import Spectre @testable import Stencil -import Foundation +import XCTest + +final class ForNodeTests: XCTestCase { + let context = Context(dictionary: [ + "items": [1, 2, 3], + "anyItems": [1, 2, 3] as [Any], + "nsItems": NSArray(array: [1, 2, 3]), + "emptyItems": [Int](), + "dict": [ + "one": "I", + "two": "II" + ], + "tuples": [(1, 2, 3), (4, 5, 6)] + ]) -class ForNodeTests: XCTestCase { func testForNode() { - describe("ForNode") { - let context = Context(dictionary: [ - "items": [1, 2, 3], - "emptyItems": [Int](), - "dict": [ - "one": "I", - "two": "II", - ], - "tuples": [(1, 2, 3), (4, 5, 6)] - ]) + 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" + } - $0.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(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" + } - $0.it("renders the given empty nodes when no items found item") { - let nodes: [NodeType] = [VariableNode(variable: "item")] - let emptyNodes: [NodeType] = [TextNode(text: "empty")] - let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes) - try expect(try node.render(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" + } - $0.it("renders a context variable of type Array") { - let any_context = Context(dictionary: [ - "items": ([1, 2, 3] as [Any]) - ]) + #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 - let nodes: [NodeType] = [VariableNode(variable: "item")] - let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(any_context)) == "123" - } - - $0.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" - } - - $0.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" - } - - #if os(OSX) - $0.it("renders a context variable of type NSArray") { - let nsarray_context = Context(dictionary: [ - "items": NSArray(array: [1, 2, 3]) - ]) - - let nodes: [NodeType] = [VariableNode(variable: "item")] - let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(nsarray_context)) == "123" - } - #endif - - $0.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(context)) == "1true2false3false" - } - - $0.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(context)) == "1false2false3true" - } - - $0.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(context)) == "112233" - } - - $0.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(context)) == "102132" - } - - $0.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(context)) == "132333" - } - - $0.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(context)) == "2132" - } - - $0.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(context)) == "empty" - } - - $0.it("can render a filter with spaces") { - let templateString = """ + 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) - 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 template = Template(templateString: templateString) - let result = try template.render(context) - - try expect(result) == """ + try expect(result) == """ - Migrating from OCUnit to XCTest by Kyle Fuller. - Memory Management with ARC by Kyle Fuller. """ - } + } + } - $0.context("given array of tuples") { - $0.it("can iterate over all tuple values") { - let templateString = """ - {% for first,second,third in tuples %}\ - {{ first }}, {{ second }}, {{ third }} - {% endfor %} - """ + 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" + } - let template = Template(templateString: templateString) - let result = try template.render(context) + 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" + } - try expect(result) == """ - 1, 2, 3 - 4, 5, 6 + 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" + } - $0.it("can iterate with less number of variables") { - let templateString = """ - {% for first,second in tuples %}\ - {{ first }}, {{ second }} - {% endfor %} - """ + 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" + } + } - let template = Template(templateString: templateString) - let result = try template.render(context) + 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" + } - try expect(result) == """ - 1, 2 - 4, 5 + 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 - $0.it("can use _ to skip variables") { - let templateString = """ - {% for first,_,third in tuples %}\ - {{ first }}, {{ third }} - {% endfor %} - """ + """ + } - let template = Template(templateString: templateString) - let result = try template.render(context) + 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 - try expect(result) == """ - 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 - $0.it("throws when number of variables is more than number of tuple values") { - let templateString = """ - {% for key,value,smth in dict %} - {% endfor %} - """ + """ + } - let template = Template(templateString: templateString) - try expect(template.render(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() + } + } - } - - $0.it("can iterate over dictionary") { - let templateString = """ + func testIterateDictionary() { + it("can iterate over dictionary") { + let template = Template(templateString: """ {% for key, value in dict %}\ {{ key }}: {{ value }},\ {% endfor %} - """ - - let template = Template(templateString: templateString) - let result = try template.render(context) - - try expect(result) == """ + """) + try expect(template.render(self.context)) == """ one: I,two: II, """ - } + } - $0.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, where: nil) - let result = try node.render(context) + 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(result) == """ + try expect(node.render(self.context)) == """ one,two, """ - } + } - $0.it("renders supports iterating over dictionary") { - 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, where: nil) - let result = try node.render(context) + 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(result) == """ + try expect(node.render(self.context)) == """ one=I,two=II, """ - } + } + } - $0.it("handles invalid input") { - 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 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: [] + ) - $0.it("can iterate over struct properties") { - struct MyStruct { - let string: String - let number: Int - } - - let context = Context(dictionary: [ - "struct": MyStruct(string: "abc", number: 123) - ]) - - let nodes: [NodeType] = [ - VariableNode(variable: "property"), - TextNode(text: "="), - VariableNode(variable: "value"), - TextNode(text: "\n"), - ] - let node = ForNode(resolvable: Variable("struct"), loopVariables: ["property", "value"], nodes: nodes, emptyNodes: []) - let result = try node.render(context) - - try expect(result) == """ + 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 """ - } + } - $0.it("can iterate tuple items") { - let context = Context(dictionary: [ - "tuple": (one: 1, two: "dva"), - ]) - - let nodes: [NodeType] = [ - VariableNode(variable: "label"), - TextNode(text: "="), - VariableNode(variable: "value"), - TextNode(text: "\n"), - ] - - let node = ForNode(resolvable: Variable("tuple"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: []) - let result = try node.render(context) - - try expect(result) == """ + it("can iterate tuple items") { + let context = Context(dictionary: [ + "item": (one: 1, two: "dva") + ]) + try expect(node.render(context)) == """ one=1 two=dva """ - } + } - $0.it("can iterate over class properties") { - class MyClass { - var baseString: String - var baseInt: Int - init(_ string: String, _ int: Int) { - baseString = string - baseInt = int - } - } - - class MySubclass: MyClass { - var childString: String - init(_ childString: String, _ string: String, _ int: Int) { - self.childString = childString - super.init(string, int) - } - } - - let context = Context(dictionary: [ - "class": MySubclass("child", "base", 1) - ]) - - let nodes: [NodeType] = [ - VariableNode(variable: "label"), - TextNode(text: "="), - VariableNode(variable: "value"), - TextNode(text: "\n"), - ] - - let node = ForNode(resolvable: Variable("class"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: []) - let result = try node.render(context) - - try expect(result) == """ + 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 """ - } + } + } - $0.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 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" } + 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" + } + + 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) } } -fileprivate struct Article { +private struct MyStruct { + let string: String + let number: Int +} + +private struct Article { let title: String let author: String } + +private class MyClass { + 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) + } +} diff --git a/Tests/StencilTests/IfNodeSpec.swift b/Tests/StencilTests/IfNodeSpec.swift index 1141b26..e3b3838 100644 --- a/Tests/StencilTests/IfNodeSpec.swift +++ b/Tests/StencilTests/IfNodeSpec.swift @@ -1,289 +1,288 @@ -import XCTest import Spectre @testable import Stencil +import XCTest -class IfNodeTests: XCTestCase { - func testIfNode() { - describe("IfNode") { - $0.describe("parsing") { - $0.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) - ] +private struct SomeType { + let value: String? = nil +} - let parser = TokenParser(tokens: tokens, environment: Environment()) - let nodes = try parser.parse() - let node = nodes.first as? IfNode +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) + ] - 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 parser = TokenParser(tokens: tokens, environment: Environment()) + let nodes = try parser.parse() + let node = nodes.first as? IfNode - $0.it("can parse an if with else block") { - 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 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 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?[1].nodes.count) == 1 - let falseNode = conditions?[1].nodes.first as? TextNode - try expect(falseNode?.text) == "false" - } - - $0.it("can parse an if with elif block") { - 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 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?[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" - } - - $0.it("can parse an if with elif block without else") { - 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 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?[1].nodes.count) == 1 - let elifNode = conditions?[1].nodes.first as? TextNode - try expect(elifNode?.text) == "some" - } - - $0.it("can parse an if with multiple elif block") { - 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 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?[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?[3].nodes.count) == 1 - let falseNode = conditions?[3].nodes.first as? TextNode - try expect(falseNode?.text) == "false" - } - - - $0.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() - } - - $0.it("can parse an ifnot block") { - 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 - - 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" - } - - $0.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) - } - - $0.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) - } - } - - $0.describe("rendering") { - $0.it("renders a true expression") { - let node = IfNode(conditions: [ - IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "1")]), - IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]), - IfCondition(expression: nil, nodes: [TextNode(text: "3")]), - ]) - - try expect(try node.render(Context())) == "1" - } - - $0.it("renders the first true expression") { - let node = IfNode(conditions: [ - IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]), - IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]), - IfCondition(expression: nil, nodes: [TextNode(text: "3")]), - ]) - - try expect(try node.render(Context())) == "2" - } - - $0.it("renders the empty expression when other conditions are falsy") { - let node = IfNode(conditions: [ - IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]), - IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]), - IfCondition(expression: nil, nodes: [TextNode(text: "3")]), - ]) - - try expect(try node.render(Context())) == "3" - } - - $0.it("renders empty when no truthy conditions") { - let node = IfNode(conditions: [ - IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]), - IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]), - ]) - - try expect(try node.render(Context())) == "" - } - } - - $0.it("supports variable filters in the if expression") { - 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 result = try renderNodes(nodes, Context(dictionary: ["value": "test"])) - try expect(result) == "true" - } - - $0.it("evaluates nil properties as false") { - 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() - - struct SomeType { - let value: String? = nil - } - let result = try renderNodes(nodes, Context(dictionary: ["instance": SomeType()])) - try expect(result) == "" - } - - $0.it("supports closed range variables") { - 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() - - try expect(renderNodes(nodes, Context(dictionary: ["value": 3]))) == "true" - try expect(renderNodes(nodes, Context(dictionary: ["value": 4]))) == "false" - } + 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() } } + + 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 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?[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) + ] + + 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 + + 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?[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) + ] + + 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?[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) + ] + + 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 + + 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?[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" + } + + 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 + + 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" + } + + 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) + } + + 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) + } + } + + func testRendering() { + it("renders a true expression") { + let node = IfNode(conditions: [ + IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "1")]), + IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]), + IfCondition(expression: nil, nodes: [TextNode(text: "3")]) + ]) + + try expect(try node.render(Context())) == "1" + } + + it("renders the first true expression") { + let node = IfNode(conditions: [ + IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]), + IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]), + IfCondition(expression: nil, nodes: [TextNode(text: "3")]) + ]) + + try expect(try node.render(Context())) == "2" + } + + it("renders the empty expression when other conditions are falsy") { + let node = IfNode(conditions: [ + IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]), + IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]), + IfCondition(expression: nil, nodes: [TextNode(text: "3")]) + ]) + + try expect(try node.render(Context())) == "3" + } + + it("renders empty when no truthy conditions") { + let node = IfNode(conditions: [ + IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]), + IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]) + ]) + + 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) + ] + + 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" + } + + 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 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) + ] + + 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" + } } diff --git a/Tests/StencilTests/IncludeSpec.swift b/Tests/StencilTests/IncludeSpec.swift index 1b587ae..d7d1ddf 100644 --- a/Tests/StencilTests/IncludeSpec.swift +++ b/Tests/StencilTests/IncludeSpec.swift @@ -1,72 +1,72 @@ -import XCTest +import PathKit import Spectre @testable import Stencil -import PathKit +import XCTest -class IncludeTests: XCTestCase { - func testInclude() { - describe("Include") { - let path = Path(#file) + ".." + "fixtures" - let loader = FileSystemLoader(paths: [path]) - let environment = Environment(loader: loader) +final class IncludeTests: XCTestCase { + let path = Path(#file) + ".." + "fixtures" + lazy var loader = FileSystemLoader(paths: [path]) + lazy var environment = Environment(loader: loader) - $0.describe("parsing") { - $0.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) + } - $0.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)) + + do { + _ = try node.render(Context()) + } catch { + try expect("\(error)") == "Template named `test.html` does not exist. No loaders found" } + } - $0.describe("rendering") { - $0.it("throws an error when rendering without a loader") { - let node = IncludeNode(templateName: Variable("\"test.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()) - } catch { - try expect("\(error)") == "Template named `test.html` does not exist. No loaders found" - } - } - - $0.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: environment)) - } catch { - try expect("\(error)".hasPrefix("Template named `unknown.html` does not exist in loader")).to.beTrue() - } - } - - $0.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: environment) - let value = try node.render(context) - try expect(value) == "Hello World!" - } - - $0.it("successfully passes context") { - let template = Template(templateString: """ - {% include "test.html" child %} - """) - let context = Context(dictionary: ["child": ["target": "World"]], environment: environment) - let value = try template.render(context) - try expect(value) == "Hello World!" - } + 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 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 new file mode 100644 index 0000000..c71736a --- /dev/null +++ b/Tests/StencilTests/InheritanceSpec.swift @@ -0,0 +1,36 @@ +import PathKit +import Spectre +import Stencil +import XCTest + +final class InheritanceTests: XCTestCase { + let path = Path(#file) + ".." + "fixtures" + lazy var loader = FileSystemLoader(paths: [path]) + 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 + """ + } + + 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 + """ + } + } +} diff --git a/Tests/StencilTests/InheritenceSpec.swift b/Tests/StencilTests/InheritenceSpec.swift deleted file mode 100644 index d859fb4..0000000 --- a/Tests/StencilTests/InheritenceSpec.swift +++ /dev/null @@ -1,38 +0,0 @@ -import XCTest -import Spectre -import Stencil -import PathKit - -class InheritenceTests: XCTestCase { - func testInheritence() { - describe("Inheritence") { - let path = Path(#file) + ".." + "fixtures" - let loader = FileSystemLoader(paths: [path]) - let environment = Environment(loader: loader) - - $0.it("can inherit from another template") { - let template = try environment.loadTemplate(name: "child.html") - try expect(try template.render()) == """ - Super_Header Child_Header - Child_Body - """ - } - - $0.it("can inherit from another template inheriting from another template") { - let template = try environment.loadTemplate(name: "child-child.html") - try expect(try template.render()) == """ - Super_Header Child_Header Child_Child_Header - Child_Body - """ - } - - $0.it("can inherit from a template that calls a super block") { - let template = try environment.loadTemplate(name: "child-super.html") - try expect(try template.render()) == """ - Header - Child_Body - """ - } - } - } -} diff --git a/Tests/StencilTests/LexerSpec.swift b/Tests/StencilTests/LexerSpec.swift index 872fb0e..9a5e880 100644 --- a/Tests/StencilTests/LexerSpec.swift +++ b/Tests/StencilTests/LexerSpec.swift @@ -3,126 +3,117 @@ import Spectre @testable import Stencil import XCTest -class LexerTests: XCTestCase { - func testLexer() { - describe("Lexer") { - 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)) - } +final class LexerTests: XCTestCase { + func testText() throws { + let lexer = Lexer(templateString: "Hello World") + let tokens = lexer.tokenize() - $0.it("can tokenize text") { - 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() - $0.it("can tokenize a comment") { - 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() - $0.it("can tokenize a variable") { - 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() - $0.it("can tokenize a token without spaces") { - 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() - $0.it("can tokenize unclosed tag by ignoring it") { - 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() - $0.it("can tokenize a mixture of content") { - let templateString = "My name is {{ myname }}." - let lexer = Lexer(templateString: templateString) - let tokens = lexer.tokenize() + try expect(tokens.count) == 3 + try expect(tokens[0]) == Token.text(value: "My name is ", at: makeSourceMap("My name is ", for: lexer)) + try expect(tokens[1]) == Token.variable(value: "myname", at: makeSourceMap("myname", for: lexer)) + try expect(tokens[2]) == Token.text(value: ".", at: makeSourceMap(".", for: lexer)) + } - try expect(tokens.count) == 3 - try expect(tokens[0]) == Token.text(value: "My name is ", at: makeSourceMap("My name is ", for: lexer)) - try expect(tokens[1]) == Token.variable(value: "myname", at: makeSourceMap("myname", for: lexer)) - try expect(tokens[2]) == Token.text(value: ".", at: makeSourceMap(".", for: lexer)) - } + func testVariablesWithoutBeingGreedy() throws { + let templateString = "{{ thing }}{{ name }}" + let lexer = Lexer(templateString: templateString) + let tokens = lexer.tokenize() - $0.it("can tokenize two variables without being greedy") { - let templateString = "{{ thing }}{{ name }}" - let lexer = Lexer(templateString: templateString) - let tokens = lexer.tokenize() + try expect(tokens.count) == 2 + try expect(tokens[0]) == Token.variable(value: "thing", at: makeSourceMap("thing", for: lexer)) + try expect(tokens[1]) == Token.variable(value: "name", at: makeSourceMap("name", for: lexer)) + } - try expect(tokens.count) == 2 - try expect(tokens[0]) == Token.variable(value: "thing", at: makeSourceMap("thing", for: lexer)) - try expect(tokens[1]) == Token.variable(value: "name", at: makeSourceMap("name", for: lexer)) - } + func testUnclosedBlock() throws { + let lexer = Lexer(templateString: "{%}") + _ = lexer.tokenize() + } - $0.it("can tokenize an unclosed block") { - let lexer = Lexer(templateString: "{%}") - _ = lexer.tokenize() - } + func testTokenizeIncorrectSyntaxWithoutCrashing() throws { + let lexer = Lexer(templateString: "func some() {{% if %}") + _ = lexer.tokenize() + } - $0.it("can tokenize incorrect syntax without crashing") { - let lexer = Lexer(templateString: "func some() {{% if %}") - _ = lexer.tokenize() - } + func testEmptyVariable() throws { + let lexer = Lexer(templateString: "{{}}") + _ = lexer.tokenize() + } - $0.it("can tokenize an empty variable") { - let lexer = Lexer(templateString: "{{}}") - _ = lexer.tokenize() - } - - $0.it("can tokenize with new lines") { - let templateString = """ - My name is {% - if name - and - name - %}{{ + func testNewlines() throws { + let templateString = """ + My name is {% + if name + and name - }}{% - endif %}. - """ - let lexer = Lexer(templateString: templateString) - let tokens = lexer.tokenize() + %}{{ + name + }}{% + endif %}. + """ + let lexer = Lexer(templateString: templateString) + let tokens = lexer.tokenize() - try expect(tokens.count) == 5 - try expect(tokens[0]) == Token.text(value: "My name is ", at: makeSourceMap("My name is", for: lexer)) - try expect(tokens[1]) == Token.block(value: "if name and name", at: makeSourceMap("{%", for: lexer)) - try expect(tokens[2]) == Token.variable(value: "name", at: makeSourceMap("name", for: lexer, options: .backwards)) - try expect(tokens[3]) == Token.block(value: "endif", at: makeSourceMap("endif", for: lexer)) - try expect(tokens[4]) == Token.text(value: ".", at: makeSourceMap(".", for: lexer)) - } + try expect(tokens.count) == 5 + try expect(tokens[0]) == Token.text(value: "My name is ", at: makeSourceMap("My name is", for: lexer)) + try expect(tokens[1]) == Token.block(value: "if name and name", at: makeSourceMap("{%", for: lexer)) + try expect(tokens[2]) == Token.variable(value: "name", at: makeSourceMap("name", for: lexer, options: .backwards)) + try expect(tokens[3]) == Token.block(value: "endif", at: makeSourceMap("endif", for: lexer)) + try expect(tokens[4]) == Token.text(value: ".", at: makeSourceMap(".", for: lexer)) + } - $0.it("can tokenize escape sequences") { - 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]) == Token.text(value: "class Some ", at: makeSourceMap("class Some ", for: lexer)) - try expect(tokens[1]) == Token.variable(value: "'{'", at: makeSourceMap("'{'", for: lexer)) - try expect(tokens[2]) == Token.block(value: "if true", at: makeSourceMap("if true", for: lexer)) - try expect(tokens[3]) == Token.variable(value: "stuff", at: makeSourceMap("stuff", for: lexer)) - try expect(tokens[4]) == Token.block(value: "endif", at: makeSourceMap("endif", for: lexer)) - } - } + try expect(tokens.count) == 5 + try expect(tokens[0]) == Token.text(value: "class Some ", at: makeSourceMap("class Some ", for: lexer)) + try expect(tokens[1]) == Token.variable(value: "'{'", at: makeSourceMap("'{'", for: lexer)) + try expect(tokens[2]) == Token.block(value: "if true", at: makeSourceMap("if true", for: lexer)) + try expect(tokens[3]) == Token.variable(value: "stuff", at: makeSourceMap("stuff", for: lexer)) + try expect(tokens[4]) == Token.block(value: "endif", at: makeSourceMap("endif", for: lexer)) } func testPerformance() throws { @@ -134,4 +125,9 @@ class LexerTests: XCTestCase { _ = lexer.tokenize() } } + + 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 91d541f..00a1f43 100644 --- a/Tests/StencilTests/LoaderSpec.swift +++ b/Tests/StencilTests/LoaderSpec.swift @@ -1,57 +1,55 @@ -import XCTest +import PathKit import Spectre import Stencil -import PathKit +import XCTest -class TemplateLoaderTests: XCTestCase { - func testTemplateLoader() { - describe("FileSystemLoader") { - let path = Path(#file) + ".." + "fixtures" - let loader = FileSystemLoader(paths: [path]) - let environment = Environment(loader: loader) +final class TemplateLoaderTests: XCTestCase { + func testFileSystemLoader() { + let path = Path(#file) + ".." + "fixtures" + let loader = FileSystemLoader(paths: [path]) + let environment = Environment(loader: loader) - $0.it("errors when a template cannot be found") { - try expect(try environment.loadTemplate(name: "unknown.html")).toThrow() - } - - $0.it("errors when an array of templates cannot be found") { - try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow() - } - - $0.it("can load a template from a file") { - _ = try environment.loadTemplate(name: "test.html") - } - - $0.it("errors when loading absolute file outside of the selected path") { - try expect(try environment.loadTemplate(name: "/etc/hosts")).toThrow() - } - - $0.it("errors when loading relative file outside of the selected path") { - try expect(try environment.loadTemplate(name: "../LoaderSpec.swift")).toThrow() - } + it("errors when a template cannot be found") { + try expect(try environment.loadTemplate(name: "unknown.html")).toThrow() } - describe("DictionaryLoader") { - let loader = DictionaryLoader(templates: [ + 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("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() + } + } + + func testDictionaryLoader() { + let loader = DictionaryLoader(templates: [ "index.html": "Hello World" - ]) - let environment = Environment(loader: loader) + ]) + let environment = Environment(loader: loader) - $0.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() + } - $0.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() + } - $0.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") + } - $0.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 1987282..0a67cf5 100644 --- a/Tests/StencilTests/NodeSpec.swift +++ b/Tests/StencilTests/NodeSpec.swift @@ -1,8 +1,8 @@ -import XCTest import Spectre @testable import Stencil +import XCTest -class ErrorNode : NodeType { +class ErrorNode: NodeType { let token: Token? init(token: Token? = nil) { self.token = token @@ -13,54 +13,50 @@ class ErrorNode : NodeType { } } -class NodeTests: XCTestCase { - func testNode() { - describe("Node") { - let context = Context(dictionary: [ - "name": "Kyle", - "age": 27, - "items": [1, 2, 3], - ]) +final class NodeTests: XCTestCase { + let context = Context(dictionary: [ + "name": "Kyle", + "age": 27, + "items": [1, 2, 3] + ]) - $0.describe("TextNode") { - $0.it("renders the given text") { - let node = TextNode(text: "Hello World") - try expect(try node.render(context)) == "Hello World" - } - } + func testTextNode() { + it("renders the given text") { + let node = TextNode(text: "Hello World") + try expect(try node.render(self.context)) == "Hello World" + } + } - $0.describe("VariableNode") { - $0.it("resolves and renders the variable") { - let node = VariableNode(variable: Variable("name")) - try expect(try node.render(context)) == "Kyle" - } + func testVariableNode() { + it("resolves and renders the variable") { + let node = VariableNode(variable: Variable("name")) + try expect(try node.render(self.context)) == "Kyle" + } - $0.it("resolves and renders a non string variable") { - let node = VariableNode(variable: Variable("age")) - try expect(try node.render(context)) == "27" - } - } + it("resolves and renders a non string variable") { + let node = VariableNode(variable: Variable("age")) + try expect(try node.render(self.context)) == "27" + } + } - $0.describe("rendering nodes") { - $0.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, context)) == "Hello Kyle" - } + try expect(try renderNodes(nodes, self.context)) == "Hello Kyle" + } - $0.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, context)).toThrow(TemplateSyntaxError("Custom Error")) - } - } + try expect(try renderNodes(nodes, self.context)).toThrow(TemplateSyntaxError("Custom Error")) } } } diff --git a/Tests/StencilTests/NowNodeSpec.swift b/Tests/StencilTests/NowNodeSpec.swift index e9a2a62..7e5d23f 100644 --- a/Tests/StencilTests/NowNodeSpec.swift +++ b/Tests/StencilTests/NowNodeSpec.swift @@ -1,46 +1,50 @@ -import XCTest -import Foundation import Spectre @testable import Stencil +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()) -class NowNodeTests: XCTestCase { - func testNowNode() { - #if !os(Linux) - describe("NowNode") { - $0.describe("parsing") { - $0.it("parses default format without any now arguments") { - 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\"" - } - - $0.it("parses now with a format") { - 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\"" - } - } - - $0.describe("rendering") { - $0.it("renders the date") { - let node = NowNode(format: Variable("\"yyyy-MM-dd\"")) - - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd" - let date = formatter.string(from: NSDate() as Date) - - try expect(try node.render(Context())) == date - } - } + 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 + } + } + + 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: NSDate() as Date) + + try expect(try node.render(Context())) == date +#endif } - #endif } } diff --git a/Tests/StencilTests/ParserSpec.swift b/Tests/StencilTests/ParserSpec.swift index e8650d3..ce5f8c0 100644 --- a/Tests/StencilTests/ParserSpec.swift +++ b/Tests/StencilTests/ParserSpec.swift @@ -1,63 +1,64 @@ -import XCTest import Spectre @testable import Stencil +import XCTest -class TokenParserTests: XCTestCase { +final class TokenParserTests: XCTestCase { func testTokenParser() { - describe("TokenParser") { - $0.it("can parse a text token") { - let parser = TokenParser(tokens: [ - .text(value: "Hello World", at: .unknown) - ], environment: Environment()) + it("can parse a text token") { + 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" + } + + it("can parse a variable token") { + 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" + } + + it("can parse a comment token") { + let parser = TokenParser(tokens: [ + .comment(value: "Secret stuff!", at: .unknown) + ], environment: Environment()) + + let nodes = try parser.parse() + try expect(nodes.count) == 0 + } + + it("can parse a tag token") { + let simpleExtension = Extension() + simpleExtension.registerSimpleTag("known") { _ in + "" } - $0.it("can parse a variable token") { - let parser = TokenParser(tokens: [ - .variable(value: "'name'", at: .unknown) - ], environment: Environment()) + let parser = TokenParser(tokens: [ + .block(value: "known", at: .unknown) + ], environment: Environment(extensions: [simpleExtension])) - 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() + try expect(nodes.count) == 1 + } - $0.it("can parse a comment token") { - let parser = TokenParser(tokens: [ - .comment(value: "Secret stuff!", at: .unknown) - ], environment: Environment()) + it("errors when parsing an unknown tag") { + let tokens: [Token] = [.block(value: "unknown", at: .unknown)] + let parser = TokenParser(tokens: tokens, environment: Environment()) - let nodes = try parser.parse() - try expect(nodes.count) == 0 - } - - $0.it("can parse a tag token") { - let simpleExtension = Extension() - simpleExtension.registerSimpleTag("known") { _ in - return "" - } - - let parser = TokenParser(tokens: [ - .block(value: "known", at: .unknown), - ], environment: Environment(extensions: [simpleExtension])) - - let nodes = try parser.parse() - try expect(nodes.count) == 1 - } - - $0.it("errors when parsing an unknown tag") { - 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) + ) } } } diff --git a/Tests/StencilTests/StencilSpec.swift b/Tests/StencilTests/StencilSpec.swift index 5ec6094..dc8aa6e 100644 --- a/Tests/StencilTests/StencilSpec.swift +++ b/Tests/StencilTests/StencilSpec.swift @@ -1,72 +1,68 @@ -import XCTest import Spectre import Stencil +import XCTest -fileprivate struct CustomNode : NodeType { +private struct CustomNode: NodeType { let token: Token? - func render(_ context:Context) throws -> String { + func render(_ context: Context) throws -> String { return "Hello World" } } -fileprivate struct Article { +private struct Article { let title: String let author: String } -class StencilTests: XCTestCase { +final class StencilTests: XCTestCase { + 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() { - describe("Stencil") { - let exampleExtension = Extension() + it("can render the README example") { + let templateString = """ + There are {{ articles.count }} articles. - exampleExtension.registerSimpleTag("simpletag") { context in - return "Hello World" - } + {% for article in articles %}\ + - {{ article.title }} by {{ article.author }}. + {% endfor %} + """ - exampleExtension.registerTag("customtag") { parser, token in - return CustomNode(token: token) - } - - let environment = Environment(extensions: [exampleExtension]) - - $0.it("can render the README example") { - - let templateString = """ - There are {{ articles.count }} articles. - - {% 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. - """ - } + """ + } - $0.it("can render a custom template tag") { - let result = try 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" + } - $0.it("can render a simple custom tag") { - let result = try 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" } } } diff --git a/Tests/StencilTests/TemplateSpec.swift b/Tests/StencilTests/TemplateSpec.swift index 3d3001b..b1c66a1 100644 --- a/Tests/StencilTests/TemplateSpec.swift +++ b/Tests/StencilTests/TemplateSpec.swift @@ -1,22 +1,19 @@ -import XCTest import Spectre @testable import Stencil +import XCTest -class TemplateTests: XCTestCase { +final class TemplateTests: XCTestCase { func testTemplate() { - describe("Template") { - $0.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" - } - - $0.it("can render a template from a string literal") { + 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" - } - } } } diff --git a/Tests/StencilTests/TokenSpec.swift b/Tests/StencilTests/TokenSpec.swift index ee4be2b..effa884 100644 --- a/Tests/StencilTests/TokenSpec.swift +++ b/Tests/StencilTests/TokenSpec.swift @@ -1,36 +1,34 @@ -import XCTest import Spectre @testable import Stencil +import XCTest -class TokenTests: XCTestCase { +final class TokenTests: XCTestCase { func testToken() { - describe("Token") { - $0.it("can split the contents into components") { - let token = Token.text(value: "hello world", at: .unknown) - let components = token.components + 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" + } - $0.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'" + } - $0.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 2f5933d..6445deb 100644 --- a/Tests/StencilTests/VariableSpec.swift +++ b/Tests/StencilTests/VariableSpec.swift @@ -1,411 +1,355 @@ -import XCTest -import Foundation import Spectre @testable import Stencil - +import XCTest #if os(OSX) -@objc class Superclass: NSObject { +@objc +class Superclass: NSObject { @objc let name = "Foo" } -@objc class Object : Superclass { +@objc +class Object: Superclass { @objc let title = "Hello World" } #endif -fileprivate struct Person { +private struct Person { let name: String } -fileprivate struct Article { +private struct Article { let author: Person } -fileprivate class WebSite { +private class WebSite { let url: String = "blog.com" } -fileprivate class Blog: WebSite { +private class Blog: WebSite { let articles: [Article] = [Article(author: Person(name: "Kyle"))] let featuring: Article? = Article(author: Person(name: "Jhon")) } -class VariableTests: XCTestCase { +final class VariableTests: XCTestCase { + 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) + ], 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" + } + + 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 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 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() { - describe("Variable") { - let context = Context(dictionary: [ - "name": "Kyle", - "contacts": ["Katie", "Carlton"], - "profiles": [ - "github": "kylef", - ], - "counter": [ - "count": "kylef", - ], - "article": Article(author: Person(name: "Kyle")), - "tuple": (one: 1, two: 2) - ]) + it("can resolve a string variable") { + let variable = Variable("name") + let result = try variable.resolve(self.context) as? String + try expect(result) == "Kyle" + } + } - #if os(OSX) - context["object"] = Object() - #endif - context["blog"] = Blog() + 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" + } - $0.it("can resolve a string literal with double quotes") { - let variable = Variable("\"name\"") - let result = try variable.resolve(context) as? String - try expect(result) == "name" - } + 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 + } + } - $0.it("can resolve a string literal with single quotes") { - let variable = Variable("'name'") - let result = try variable.resolve(context) as? String - try expect(result) == "name" - } + 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" - $0.it("can resolve an integer literal") { - let variable = Variable("5") - let result = try variable.resolve(context) as? Int - try expect(result) == 5 - } + let variable1 = Variable("contacts.1") + let result1 = try variable1.resolve(self.context) as? String + try expect(result1) == "Carlton" + } - $0.it("can resolve an float literal") { - let variable = Variable("3.14") - let result = try variable.resolve(context) as? Number - try expect(result) == 3.14 - } + 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() - $0.it("can resolve boolean literal") { - try expect(Variable("true").resolve(context) as? Bool) == true - try expect(Variable("false").resolve(context) as? Bool) == false - try expect(Variable("0").resolve(context) as? Int) == 0 - try expect(Variable("1").resolve(context) as? Int) == 1 - } + let variable1 = Variable("contacts.-5") + let result1 = try variable1.resolve(self.context) as? String + try expect(result1).to.beNil() + } - $0.it("can resolve a string variable") { - let variable = Variable("name") - let result = try variable.resolve(context) as? String + 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" + } + } + + 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 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" + } + } + + 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("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 + } + + 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]) + + 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" } + } - $0.context("given string") { - $0.it("can resolve an item via it's index") { - let variable = Variable("name.0") - let result = try variable.resolve(context) as? Character - try expect(result) == "K" - - let variable1 = Variable("name.1") - let result1 = try variable1.resolve(context) as? Character - try expect(result1) == "y" - } - - $0.it("can resolve an item via unknown index") { - let variable = Variable("name.5") - let result = try variable.resolve(context) as? Character - try expect(result).to.beNil() - - let variable1 = Variable("name.-5") - let result1 = try variable1.resolve(context) as? Character - try expect(result1).to.beNil() - } - - $0.it("can resolve the first item") { - let variable = Variable("name.first") - let result = try variable.resolve(context) as? Character - try expect(result) == "K" - } - - $0.it("can resolve the last item") { - let variable = Variable("name.last") - let result = try variable.resolve(context) as? Character - try expect(result) == "e" - } - - $0.it("can get the characters count") { - let variable = Variable("name.count") - let result = try variable.resolve(context) as? Int - try expect(result) == 4 - } + 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" } + } - $0.context("given dictionary") { - $0.it("can resolve an item") { - let variable = Variable("profiles.github") - let result = try variable.resolve(context) as? String - try expect(result) == "kylef" - } - - $0.it("can get the count") { - let variable = Variable("profiles.count") - let result = try variable.resolve(context) as? Int - try expect(result) == 1 - } - } - - $0.context("given array") { - $0.it("can resolve an item via it's index") { - let variable = Variable("contacts.0") - let result = try variable.resolve(context) as? String - try expect(result) == "Katie" - - let variable1 = Variable("contacts.1") - let result1 = try variable1.resolve(context) as? String - try expect(result1) == "Carlton" - } - - $0.it("can resolve an item via unknown index") { - let variable = Variable("contacts.5") - let result = try variable.resolve(context) as? String - try expect(result).to.beNil() - - let variable1 = Variable("contacts.-5") - let result1 = try variable1.resolve(context) as? String - try expect(result1).to.beNil() - } - - $0.it("can resolve the first item") { - let variable = Variable("contacts.first") - let result = try variable.resolve(context) as? String - try expect(result) == "Katie" - } - - $0.it("can resolve the last item") { - let variable = Variable("contacts.last") - let result = try variable.resolve(context) as? String - try expect(result) == "Carlton" - } - - $0.it("can get the count") { - let variable = Variable("contacts.count") - let result = try variable.resolve(context) as? Int - try expect(result) == 2 - } - } - - $0.it("can resolve a property with reflection") { - let variable = Variable("article.author.name") - let result = try variable.resolve(context) as? String - try expect(result) == "Kyle" - } - - #if os(OSX) - $0.it("can resolve a value via KVO") { - let variable = Variable("object.title") - let result = try variable.resolve(context) as? String - try expect(result) == "Hello World" - } - - $0.it("can resolve a superclass value via KVO") { - let variable = Variable("object.name") - let result = try variable.resolve(context) as? String - try expect(result) == "Foo" - } - - $0.it("does not crash on KVO") { - let variable = Variable("object.fullname") - let result = try variable.resolve(context) as? String + 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() } - #endif + } - $0.it("can resolve a value via reflection") { - let variable = Variable("blog.articles.0.author.name") - let result = try variable.resolve(context) as? String - try expect(result) == "Kyle" +#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 - $0.it("can resolve a superclass value via reflection") { - let variable = Variable("blog.url") - let result = try variable.resolve(context) as? String - try expect(result) == "blog.com" - } - - $0.it("can resolve optional variable property using reflection") { - let variable = Variable("blog.featuring.author.name") - let result = try variable.resolve(context) as? String + 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" } - - $0.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)) == "" - } - - $0.it("can subscript tuple by index") { - let variable = Variable("tuple.0") - let result = try variable.resolve(context) as? Int - try expect(result) == 1 - } - - $0.it("can subscript tuple by label") { - let variable = Variable("tuple.two") - let result = try variable.resolve(context) as? Int - try expect(result) == 2 - } - - $0.describe("Subscripting") { - $0.it("can resolve a property subscript via reflection") { - try context.push(dictionary: ["property": "name"]) { - let variable = Variable("article.author[property]") - let result = try variable.resolve(context) as? String - try expect(result) == "Kyle" - } - } - - $0.it("can subscript an array with a valid index") { - try context.push(dictionary: ["property": 0]) { - let variable = Variable("contacts[property]") - let result = try variable.resolve(context) as? String - try expect(result) == "Katie" - } - } - - $0.it("can subscript an array with an unknown index") { - try context.push(dictionary: ["property": 5]) { - let variable = Variable("contacts[property]") - let result = try variable.resolve(context) as? String - try expect(result).to.beNil() - } - } - - #if os(OSX) - $0.it("can resolve a subscript via KVO") { - try context.push(dictionary: ["property": "name"]) { - let variable = Variable("object[property]") - let result = try variable.resolve(context) as? String - try expect(result) == "Foo" - } - } - #endif - - $0.it("can resolve an optional subscript via reflection") { - try context.push(dictionary: ["property": "featuring"]) { - let variable = Variable("blog[property].author.name") - let result = try variable.resolve(context) as? String - try expect(result) == "Jhon" - } - } - - $0.it("can resolve multiple subscripts") { - try context.push(dictionary: [ - "prop1": "articles", - "prop2": 0, - "prop3": "name" - ]) { - let variable = Variable("blog[prop1][prop2].author[prop3]") - let result = try variable.resolve(context) as? String - try expect(result) == "Kyle" - } - } - - $0.it("can resolve nested subscripts") { - try context.push(dictionary: [ - "prop1": "prop2", - "ref": ["prop2": "name"] - ]) { - let variable = Variable("article.author[ref[prop1]]") - let result = try variable.resolve(context) as? String - try expect(result) == "Kyle" - } - } - - $0.it("throws for invalid keypath syntax") { - try 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(context)).toThrow() - } - } - } - } - } - - describe("RangeVariable") { - - let context: Context = { - let ext = Extension() - ext.registerFilter("incr", filter: { (arg: Any?) in toNumber(value: arg!)! + 1 }) - let environment = Environment(extensions: [ext]) - return Context(dictionary: [:], environment: environment) - }() - - func makeVariable(_ token: String) throws -> RangeVariable? { - let token = Token.variable(value: token, at: .unknown) - return try RangeVariable(token.contents, environment: context.environment, containedIn: token) - } - - $0.it("can resolve closed range as array") { - let result = try makeVariable("1...3")?.resolve(context) as? [Int] - try expect(result) == [1, 2, 3] - } - - $0.it("can resolve decreasing closed range as reversed array") { - let result = try makeVariable("3...1")?.resolve(context) as? [Int] - try expect(result) == [3, 2, 1] - } - - $0.it("can use filter on range variables") { - let result = try makeVariable("1|incr...3|incr")?.resolve(context) as? [Int] - try expect(result) == [2, 3, 4] - } - - $0.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() - } - - $0.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() - } - - $0.it("throws is left range value is missing") { - try expect(makeVariable("...1")).toThrow() - } - - $0.it("throws is right range value is missing") { - try expect(makeVariable("1...")).toThrow() - } - - } - - describe("inline if expression") { - - $0.it("can conditionally render variable") { - let template: Template = "{{ variable if variable|uppercase == \"A\" }}" - try expect(template.render(Context(dictionary: ["variable": "a"]))) == "a" - try expect(template.render(Context(dictionary: ["variable": "b"]))) == "" - } - - $0.it("can render with else expression") { - let template: Template = "{{ variable if variable|uppercase == \"A\" else fallback|uppercase }}" - try expect(template.render(Context(dictionary: ["variable": "b", "fallback": "c"]))) == "C" - } - - $0.it("throws when used invalid condition") { - let template: Template = "{{ variable if variable \"A\" }}" - try expect(template.render(Context(dictionary: ["variable": "a"]))).toThrow() - } + } + } + + 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("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() + } + } + } + } + + 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 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("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 is left range value is missing") { + try expect(makeVariable("...1")).toThrow() + } + + it("throws is right range value is missing") { + try expect(makeVariable("1...")).toThrow() } } } diff --git a/Tests/StencilTests/XCTestManifests.swift b/Tests/StencilTests/XCTestManifests.swift index 73cf026..b31dca7 100644 --- a/Tests/StencilTests/XCTestManifests.swift +++ b/Tests/StencilTests/XCTestManifests.swift @@ -2,19 +2,54 @@ import XCTest extension ContextTests { static let __allTests = [ - ("testContext", testContext), + ("testContextRestoration", testContextRestoration), + ("testContextSubscripting", testContextSubscripting), + ] +} + +extension EnvironmentBaseAndChildTemplateTests { + static let __allTests = [ + ("testRuntimeErrorInBaseTemplate", testRuntimeErrorInBaseTemplate), + ("testRuntimeErrorInChildTemplate", testRuntimeErrorInChildTemplate), + ("testSyntaxErrorInBaseTemplate", testSyntaxErrorInBaseTemplate), + ("testSyntaxErrorInChildTemplate", testSyntaxErrorInChildTemplate), + ] +} + +extension EnvironmentIncludeTemplateTests { + static let __allTests = [ + ("testRuntimeError", testRuntimeError), + ("testSyntaxError", testSyntaxError), ] } extension EnvironmentTests { static let __allTests = [ - ("testEnvironment", testEnvironment), + ("testLoading", testLoading), + ("testRendering", testRendering), + ("testRenderingError", testRenderingError), + ("testSyntaxError", testSyntaxError), + ("testUnknownFilter", testUnknownFilter), ] } extension ExpressionsTests { static let __allTests = [ - ("testExpressions", testExpressions), + ("testAndExpression", testAndExpression), + ("testEqualityExpression", testEqualityExpression), + ("testExpressionParsing", testExpressionParsing), + ("testFalseExpressions", testFalseExpressions), + ("testFalseInExpression", testFalseInExpression), + ("testInequalityExpression", testInequalityExpression), + ("testLessThanEqualExpression", testLessThanEqualExpression), + ("testLessThanExpression", testLessThanExpression), + ("testMoreThanEqualExpression", testMoreThanEqualExpression), + ("testMoreThanExpression", testMoreThanExpression), + ("testMultipleExpressions", testMultipleExpressions), + ("testNotExpression", testNotExpression), + ("testOrExpression", testOrExpression), + ("testTrueExpressions", testTrueExpressions), + ("testTrueInExpression", testTrueInExpression), ] } @@ -26,50 +61,95 @@ extension FilterTagTests { extension FilterTests { static let __allTests = [ - ("testFilter", testFilter), + ("testDefaultFilter", testDefaultFilter), + ("testDynamicFilters", testDynamicFilters), + ("testFilterSuggestion", testFilterSuggestion), + ("testIndentContent", testIndentContent), + ("testIndentFirstLine", testIndentFirstLine), + ("testIndentNotEmptyLines", testIndentNotEmptyLines), + ("testIndentWithArbitraryCharacter", testIndentWithArbitraryCharacter), + ("testJoinFilter", testJoinFilter), + ("testRegistration", testRegistration), + ("testRegistrationOverrideDefault", testRegistrationOverrideDefault), + ("testRegistrationWithArguments", testRegistrationWithArguments), + ("testSplitFilter", testSplitFilter), + ("testStringFilters", testStringFilters), + ("testStringFiltersWithArrays", testStringFiltersWithArrays), ] } extension ForNodeTests { static let __allTests = [ + ("testArrayOfTuples", testArrayOfTuples), ("testForNode", testForNode), + ("testHandleInvalidInput", testHandleInvalidInput), + ("testIterateDictionary", testIterateDictionary), + ("testIterateRange", testIterateRange), + ("testIterateUsingMirroring", testIterateUsingMirroring), + ("testLoopMetadata", testLoopMetadata), + ("testWhereExpression", testWhereExpression), ] } extension IfNodeTests { static let __allTests = [ - ("testIfNode", testIfNode), + ("testEvaluatesNilAsFalse", testEvaluatesNilAsFalse), + ("testParseIf", testParseIf), + ("testParseIfnot", testParseIfnot), + ("testParseIfWithElif", testParseIfWithElif), + ("testParseIfWithElifWithoutElse", testParseIfWithElifWithoutElse), + ("testParseIfWithElse", testParseIfWithElse), + ("testParseMultipleElif", testParseMultipleElif), + ("testParsingErrors", testParsingErrors), + ("testRendering", testRendering), + ("testSupportsRangeVariables", testSupportsRangeVariables), + ("testSupportVariableFilters", testSupportVariableFilters), ] } extension IncludeTests { static let __allTests = [ - ("testInclude", testInclude), + ("testParsing", testParsing), + ("testRendering", testRendering), ] } -extension InheritenceTests { +extension InheritanceTests { static let __allTests = [ - ("testInheritence", testInheritence), + ("testInheritance", testInheritance), ] } extension LexerTests { static let __allTests = [ - ("testLexer", testLexer), + ("testComment", testComment), + ("testContentMixture", testContentMixture), + ("testEmptyVariable", testEmptyVariable), + ("testEscapeSequence", testEscapeSequence), + ("testNewlines", testNewlines), ("testPerformance", testPerformance), + ("testText", testText), + ("testTokenizeIncorrectSyntaxWithoutCrashing", testTokenizeIncorrectSyntaxWithoutCrashing), + ("testTokenWithoutSpaces", testTokenWithoutSpaces), + ("testUnclosedBlock", testUnclosedBlock), + ("testUnclosedTag", testUnclosedTag), + ("testVariable", testVariable), + ("testVariablesWithoutBeingGreedy", testVariablesWithoutBeingGreedy), ] } extension NodeTests { static let __allTests = [ - ("testNode", testNode), + ("testRendering", testRendering), + ("testTextNode", testTextNode), + ("testVariableNode", testVariableNode), ] } extension NowNodeTests { static let __allTests = [ - ("testNowNode", testNowNode), + ("testParsing", testParsing), + ("testRendering", testRendering), ] } @@ -81,7 +161,8 @@ extension StencilTests { extension TemplateLoaderTests { static let __allTests = [ - ("testTemplateLoader", testTemplateLoader), + ("testDictionaryLoader", testDictionaryLoader), + ("testFileSystemLoader", testFileSystemLoader), ] } @@ -105,6 +186,16 @@ extension TokenTests { extension VariableTests { static let __allTests = [ + ("testArray", testArray), + ("testDictionary", testDictionary), + ("testKVO", testKVO), + ("testLiterals", testLiterals), + ("testMultipleSubscripting", testMultipleSubscripting), + ("testOptional", testOptional), + ("testRangeVariable", testRangeVariable), + ("testReflection", testReflection), + ("testSubscripting", testSubscripting), + ("testTuple", testTuple), ("testVariable", testVariable), ] } @@ -113,6 +204,8 @@ extension VariableTests { public func __allTests() -> [XCTestCaseEntry] { return [ testCase(ContextTests.__allTests), + testCase(EnvironmentBaseAndChildTemplateTests.__allTests), + testCase(EnvironmentIncludeTemplateTests.__allTests), testCase(EnvironmentTests.__allTests), testCase(ExpressionsTests.__allTests), testCase(FilterTagTests.__allTests), @@ -120,7 +213,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(ForNodeTests.__allTests), testCase(IfNodeTests.__allTests), testCase(IncludeTests.__allTests), - testCase(InheritenceTests.__allTests), + testCase(InheritanceTests.__allTests), testCase(LexerTests.__allTests), testCase(NodeTests.__allTests), testCase(NowNodeTests.__allTests),