Merge pull request #323 from stencilproject/feature/drop-swift4

Drop Swift 4 support
This commit is contained in:
David Jennes
2022-07-28 16:39:36 +02:00
committed by GitHub
51 changed files with 780 additions and 891 deletions

View File

@@ -1,90 +1,112 @@
swiftlint_version: 0.48.0 swiftlint_version: 0.48.0
disabled_rules:
# Remove this once we remove old swift support
- implicit_return
opt_in_rules: opt_in_rules:
- accessibility_label_for_image
- anonymous_argument_in_multiline_closure
- anyobject_protocol - anyobject_protocol
- array_init - array_init
- attributes - attributes
- balanced_xctest_lifecycle
- closure_body_length - closure_body_length
- closure_end_indentation - closure_end_indentation
- closure_spacing - closure_spacing
- collection_alignment - collection_alignment
- comment_spacing
- conditional_returns_on_newline
- contains_over_filter_count - contains_over_filter_count
- contains_over_filter_is_empty - contains_over_filter_is_empty
- contains_over_first_not_nil - contains_over_first_not_nil
- contains_over_range_nil_comparison - contains_over_range_nil_comparison
- convenience_type - convenience_type
- discarded_notification_center_observer
- discouraged_assert
- discouraged_none_name
- discouraged_optional_boolean - discouraged_optional_boolean
- discouraged_optional_collection - discouraged_optional_collection
- duplicate_enum_cases
- duplicate_imports
- empty_collection_literal - empty_collection_literal
- empty_count - empty_count
- empty_string - empty_string
- empty_xctest_method
- enum_case_associated_values_count
- fallthrough - fallthrough
- fatal_error_message - fatal_error_message
- file_header
- first_where - first_where
- flatmap_over_map_reduce - flatmap_over_map_reduce
- force_unwrapping - force_unwrapping
- ibinspectable_in_extension
- identical_operands - identical_operands
- inert_defer - implicit_return
- implicitly_unwrapped_optional
- inclusive_language
- indentation_width
- joined_default_parameter - joined_default_parameter
- last_where - last_where
- legacy_hashing - legacy_multiple
- legacy_objc_type
- legacy_random - legacy_random
- literal_expression_end_indentation - literal_expression_end_indentation
- lower_acl_than_parent - lower_acl_than_parent
- missing_docs
- modifier_order - modifier_order
- multiline_arguments - multiline_arguments
- multiline_arguments_brackets
- multiline_function_chains - multiline_function_chains
- multiline_literal_brackets - multiline_literal_brackets
- multiline_parameters - multiline_parameters
- multiline_parameters_brackets - multiline_parameters_brackets
- nslocalizedstring_key - nslocalizedstring_key
- nsobject_prefer_isequal - nslocalizedstring_require_bundle
- number_separator - number_separator
- object_literal
- operator_usage_whitespace - operator_usage_whitespace
- optional_enum_case_matching
- overridden_super_call - overridden_super_call
- override_in_extension - override_in_extension
- prefer_self_in_static_references
- prefer_self_type_over_type_of_self - prefer_self_type_over_type_of_self
- prefer_zero_over_explicit_init
- prefixed_toplevel_constant
- private_action - private_action
- private_outlet - private_outlet
- private_subject
- prohibited_super_call - prohibited_super_call
- raw_value_for_camel_cased_codable_enum - raw_value_for_camel_cased_codable_enum
- reduce_boolean
- reduce_into - reduce_into
- redundant_nil_coalescing - redundant_nil_coalescing
- redundant_objc_attribute - redundant_type_annotation
- required_enum_case
- return_value_from_void_function
- single_test_class
- sorted_first_last - sorted_first_last
- sorted_imports - sorted_imports
- static_operator - static_operator
- strong_iboutlet - strong_iboutlet
- switch_case_on_newline
- test_case_accessibility
- toggle_bool - toggle_bool
- trailing_closure - trailing_closure
- unavailable_function - unavailable_function
- unneeded_parentheses_in_closure_argument - unneeded_parentheses_in_closure_argument
- unowned_variable_capture - unowned_variable_capture
- unused_capture_list - unused_closure_parameter
- unused_control_flow_label
- unused_declaration
- unused_setter_value
- vertical_parameter_alignment_on_call - vertical_parameter_alignment_on_call
- vertical_whitespace_closing_braces - vertical_whitespace_closing_braces
- vertical_whitespace_opening_braces - vertical_whitespace_opening_braces
- void_function_in_ternary
- weak_delegate
- xct_specific_matcher - xct_specific_matcher
- yoda_condition - yoda_condition
# Enable this again once we remove old swift support
# - optional_enum_case_matching
# - legacy_multiple
# Rules customization # Rules customization
closure_body_length: closure_body_length:
warning: 25 warning: 25
conditional_returns_on_newline:
if_only: true
indentation_width:
indentation_width: 2
line_length: line_length:
warning: 120 warning: 120
error: 200 error: 200
@@ -92,8 +114,3 @@ line_length:
nesting: nesting:
type_level: type_level:
warning: 2 warning: 2
# Exclude generated files
excluded:
- .build
- Tests/StencilTests/XCTestManifests.swift

View File

@@ -2,7 +2,9 @@
### Breaking ### Breaking
_None_ - Drop support for Swift < 5. For Swift 4.2 support, you should use Stencil 0.14.2.
[David Jennes](https://github.com/djbe)
[#323](https://github.com/stencilproject/Stencil/pull/323)
### Enhancements ### Enhancements

View File

@@ -1,4 +1,4 @@
// swift-tools-version:4.2 // swift-tools-version:5.0
import PackageDescription import PackageDescription
let package = Package( let package = Package(
@@ -13,11 +13,11 @@ let package = Package(
targets: [ targets: [
.target(name: "Stencil", dependencies: [ .target(name: "Stencil", dependencies: [
"PathKit" "PathKit"
], path: "Sources"), ]),
.testTarget(name: "StencilTests", dependencies: [ .testTarget(name: "StencilTests", dependencies: [
"Stencil", "Stencil",
"Spectre" "Spectre"
]) ])
], ],
swiftLanguageVersions: [.v4_2] swiftLanguageVersions: [.v5]
) )

View File

@@ -1,23 +0,0 @@
// swift-tools-version:5.0
import PackageDescription
let package = Package(
name: "Stencil",
products: [
.library(name: "Stencil", targets: ["Stencil"])
],
dependencies: [
.package(url: "https://github.com/kylef/PathKit.git", from: "1.0.1"),
.package(url: "https://github.com/kylef/Spectre.git", from: "0.10.1")
],
targets: [
.target(name: "Stencil", dependencies: [
"PathKit"
], path: "Sources"),
.testTarget(name: "StencilTests", dependencies: [
"Stencil",
"Spectre"
])
],
swiftLanguageVersions: [.v4_2, .v5]
)

View File

@@ -1,7 +1,5 @@
# Stencil # Stencil
[![Build Status](https://travis-ci.org/stencilproject/Stencil.svg?branch=master)](https://travis-ci.org/stencilproject/Stencil)
Stencil is a simple and powerful template language for Swift. It provides a Stencil is a simple and powerful template language for Swift. It provides a
syntax similar to Django and Mustache. If you're familiar with these, you will syntax similar to Django and Mustache. If you're familiar with these, you will
feel right at home with Stencil. feel right at home with Stencil.

View File

@@ -2,8 +2,14 @@
public class Context { public class Context {
var dictionaries: [[String: Any?]] var dictionaries: [[String: Any?]]
/// The context's environment, such as registered extensions, classes,
public let environment: Environment public let environment: Environment
/// Create a context from a dictionary (and an env.)
///
/// - Parameters:
/// - dictionary: The context's data
/// - environment: Environment such as extensions,
public init(dictionary: [String: Any] = [:], environment: Environment? = nil) { public init(dictionary: [String: Any] = [:], environment: Environment? = nil) {
if !dictionary.isEmpty { if !dictionary.isEmpty {
dictionaries = [dictionary] dictionaries = [dictionary]
@@ -14,6 +20,7 @@ public class Context {
self.environment = environment ?? Environment() self.environment = environment ?? Environment()
} }
/// Access variables in this context by name
public subscript(key: String) -> Any? { public subscript(key: String) -> Any? {
/// Retrieves a variable's value, starting at the current context and going upwards /// Retrieves a variable's value, starting at the current context and going upwards
get { get {
@@ -36,22 +43,35 @@ public class Context {
} }
/// Push a new level into the Context /// Push a new level into the Context
///
/// - Parameters:
/// - dictionary: The new level data
fileprivate func push(_ dictionary: [String: Any] = [:]) { fileprivate func push(_ dictionary: [String: Any] = [:]) {
dictionaries.append(dictionary) dictionaries.append(dictionary)
} }
/// Pop the last level off of the Context /// Pop the last level off of the Context
///
/// - returns: The popped level
fileprivate func pop() -> [String: Any?]? { fileprivate func pop() -> [String: Any?]? {
return dictionaries.popLast() dictionaries.popLast()
} }
/// Push a new level onto the context for the duration of the execution of the given closure /// Push a new level onto the context for the duration of the execution of the given closure
///
/// - Parameters:
/// - dictionary: The new level data
/// - closure: The closure to execute
/// - returns: Return value of the closure
public func push<Result>(dictionary: [String: Any] = [:], closure: (() throws -> Result)) rethrows -> Result { public func push<Result>(dictionary: [String: Any] = [:], closure: (() throws -> Result)) rethrows -> Result {
push(dictionary) push(dictionary)
defer { _ = pop() } defer { _ = pop() }
return try closure() return try closure()
} }
/// Flatten all levels of context data into 1, merging duplicate variables
///
/// - returns: All collected variables
public func flatten() -> [String: Any] { public func flatten() -> [String: Any] {
var accumulator: [String: Any] = [:] var accumulator: [String: Any] = [:]

View File

@@ -6,10 +6,13 @@ public protocol DynamicMemberLookup {
} }
public extension DynamicMemberLookup where Self: RawRepresentable { public extension DynamicMemberLookup where Self: RawRepresentable {
/// Get a value for a given `String` key
subscript(dynamicMember member: String) -> Any? { subscript(dynamicMember member: String) -> Any? {
switch member { switch member {
case "rawValue": return rawValue case "rawValue":
default: return nil return rawValue
default:
return nil
} }
} }
} }

View File

@@ -1,8 +1,18 @@
/// Container for environment data, such as registered extensions
public struct Environment { public struct Environment {
/// The class for loading new templates
public let templateClass: Template.Type public let templateClass: Template.Type
/// List of registered extensions
public var extensions: [Extension] public var extensions: [Extension]
/// Mechanism for loading new files
public var loader: Loader? public var loader: Loader?
/// Basic initializer
///
/// - Parameters:
/// - loader: Mechanism for loading new files
/// - extensions: List of extension containers
/// - templateClass: Class for newly loaded templates
public init( public init(
loader: Loader? = nil, loader: Loader? = nil,
extensions: [Extension] = [], extensions: [Extension] = [],
@@ -13,6 +23,11 @@ public struct Environment {
self.extensions = extensions + [DefaultExtension()] self.extensions = extensions + [DefaultExtension()]
} }
/// Load a template with the given name
///
/// - Parameters:
/// - name: Name of the template
/// - returns: Loaded template instance
public func loadTemplate(name: String) throws -> Template { public func loadTemplate(name: String) throws -> Template {
if let loader = loader { if let loader = loader {
return try loader.loadTemplate(name: name, environment: self) return try loader.loadTemplate(name: name, environment: self)
@@ -21,6 +36,11 @@ public struct Environment {
} }
} }
/// Load a template with the given names
///
/// - Parameters:
/// - names: Names of the template
/// - returns: Loaded template instance
public func loadTemplate(names: [String]) throws -> Template { public func loadTemplate(names: [String]) throws -> Template {
if let loader = loader { if let loader = loader {
return try loader.loadTemplate(names: names, environment: self) return try loader.loadTemplate(names: names, environment: self)
@@ -29,11 +49,23 @@ public struct Environment {
} }
} }
/// Render a template with the given name, providing some data
///
/// - Parameters:
/// - name: Name of the template
/// - context: Data for rendering
/// - returns: Rendered output
public func renderTemplate(name: String, context: [String: Any] = [:]) throws -> String { public func renderTemplate(name: String, context: [String: Any] = [:]) throws -> String {
let template = try loadTemplate(name: name) let template = try loadTemplate(name: name)
return try render(template: template, context: context) return try render(template: template, context: context)
} }
/// Render the given template string, providing some data
///
/// - Parameters:
/// - string: Template string
/// - context: Data for rendering
/// - returns: Rendered output
public func renderTemplate(string: String, context: [String: Any] = [:]) throws -> String { public func renderTemplate(string: String, context: [String: Any] = [:]) throws -> String {
let template = templateClass.init(templateString: string, environment: self) let template = templateClass.init(templateString: string, environment: self)
return try render(template: template, context: context) return try render(template: template, context: context)

View File

@@ -20,12 +20,12 @@ public class TemplateDoesNotExist: Error, CustomStringConvertible {
public struct TemplateSyntaxError: Error, Equatable, CustomStringConvertible { public struct TemplateSyntaxError: Error, Equatable, CustomStringConvertible {
public let reason: String public let reason: String
public var description: String { return reason } public var description: String { reason }
public internal(set) var token: Token? public internal(set) var token: Token?
public internal(set) var stackTrace: [Token] public internal(set) var stackTrace: [Token]
public var templateName: String? { return token?.sourceMap.filename } public var templateName: String? { token?.sourceMap.filename }
var allTokens: [Token] { var allTokens: [Token] {
return stackTrace + (token.map { [$0] } ?? []) stackTrace + (token.map { [$0] } ?? [])
} }
public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) { public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) {

View File

@@ -18,11 +18,11 @@ final class StaticExpression: Expression, CustomStringConvertible {
} }
func evaluate(context: Context) throws -> Bool { func evaluate(context: Context) throws -> Bool {
return value value
} }
var description: String { var description: String {
return "\(value)" "\(value)"
} }
} }
@@ -34,7 +34,7 @@ final class VariableExpression: Expression, CustomStringConvertible {
} }
var description: String { var description: String {
return "(variable: \(variable))" "(variable: \(variable))"
} }
/// Resolves a variable in the given context as boolean /// Resolves a variable in the given context as boolean
@@ -60,7 +60,7 @@ final class VariableExpression: Expression, CustomStringConvertible {
} }
func evaluate(context: Context) throws -> Bool { func evaluate(context: Context) throws -> Bool {
return try resolve(context: context, variable: variable) try resolve(context: context, variable: variable)
} }
} }
@@ -72,11 +72,11 @@ final class NotExpression: Expression, PrefixOperator, CustomStringConvertible {
} }
var description: String { var description: String {
return "not \(expression)" "not \(expression)"
} }
func evaluate(context: Context) throws -> Bool { func evaluate(context: Context) throws -> Bool {
return try !expression.evaluate(context: context) try !expression.evaluate(context: context)
} }
} }
@@ -90,7 +90,7 @@ final class InExpression: Expression, InfixOperator, CustomStringConvertible {
} }
var description: String { var description: String {
return "(\(lhs) in \(rhs))" "(\(lhs) in \(rhs))"
} }
func evaluate(context: Context) throws -> Bool { func evaluate(context: Context) throws -> Bool {
@@ -125,7 +125,7 @@ final class OrExpression: Expression, InfixOperator, CustomStringConvertible {
} }
var description: String { var description: String {
return "(\(lhs) or \(rhs))" "(\(lhs) or \(rhs))"
} }
func evaluate(context: Context) throws -> Bool { func evaluate(context: Context) throws -> Bool {
@@ -148,7 +148,7 @@ final class AndExpression: Expression, InfixOperator, CustomStringConvertible {
} }
var description: String { var description: String {
return "(\(lhs) and \(rhs))" "(\(lhs) and \(rhs))"
} }
func evaluate(context: Context) throws -> Bool { func evaluate(context: Context) throws -> Bool {
@@ -171,7 +171,7 @@ class EqualityExpression: Expression, InfixOperator, CustomStringConvertible {
} }
var description: String { var description: String {
return "(\(lhs) == \(rhs))" "(\(lhs) == \(rhs))"
} }
func evaluate(context: Context) throws -> Bool { func evaluate(context: Context) throws -> Bool {
@@ -206,7 +206,7 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
} }
var description: String { var description: String {
return "(\(lhs) \(symbol) \(rhs))" "(\(lhs) \(symbol) \(rhs))"
} }
func evaluate(context: Context) throws -> Bool { func evaluate(context: Context) throws -> Bool {
@@ -225,61 +225,61 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
} }
var symbol: String { var symbol: String {
return "" ""
} }
func compare(lhs: Number, rhs: Number) -> Bool { func compare(lhs: Number, rhs: Number) -> Bool {
return false false
} }
} }
class MoreThanExpression: NumericExpression { class MoreThanExpression: NumericExpression {
override var symbol: String { override var symbol: String {
return ">" ">"
} }
override func compare(lhs: Number, rhs: Number) -> Bool { override func compare(lhs: Number, rhs: Number) -> Bool {
return lhs > rhs lhs > rhs
} }
} }
class MoreThanEqualExpression: NumericExpression { class MoreThanEqualExpression: NumericExpression {
override var symbol: String { override var symbol: String {
return ">=" ">="
} }
override func compare(lhs: Number, rhs: Number) -> Bool { override func compare(lhs: Number, rhs: Number) -> Bool {
return lhs >= rhs lhs >= rhs
} }
} }
class LessThanExpression: NumericExpression { class LessThanExpression: NumericExpression {
override var symbol: String { override var symbol: String {
return "<" "<"
} }
override func compare(lhs: Number, rhs: Number) -> Bool { override func compare(lhs: Number, rhs: Number) -> Bool {
return lhs < rhs lhs < rhs
} }
} }
class LessThanEqualExpression: NumericExpression { class LessThanEqualExpression: NumericExpression {
override var symbol: String { override var symbol: String {
return "<=" "<="
} }
override func compare(lhs: Number, rhs: Number) -> Bool { override func compare(lhs: Number, rhs: Number) -> Bool {
return lhs <= rhs lhs <= rhs
} }
} }
class InequalityExpression: EqualityExpression { class InequalityExpression: EqualityExpression {
override var description: String { override var description: String {
return "(\(lhs) != \(rhs))" "(\(lhs) != \(rhs))"
} }
override func evaluate(context: Context) throws -> Bool { override func evaluate(context: Context) throws -> Bool {
return try !super.evaluate(context: context) try !super.evaluate(context: context)
} }
} }

View File

@@ -1,9 +1,11 @@
/// Container for registered tags and filters
open class Extension { open class Extension {
typealias TagParser = (TokenParser, Token) throws -> NodeType typealias TagParser = (TokenParser, Token) throws -> NodeType
var tags = [String: TagParser]()
var tags = [String: TagParser]()
var filters = [String: Filter]() var filters = [String: Filter]()
/// Simple initializer
public init() { public init() {
} }
@@ -20,11 +22,11 @@ open class Extension {
} }
/// Registers boolean filter with it's negative counterpart /// 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?) { public func registerFilter(name: String, negativeFilterName: String, filter: @escaping (Any?) throws -> Bool?) {
// swiftlint:disable:previous discouraged_optional_boolean
filters[name] = .simple(filter) filters[name] = .simple(filter)
filters[negativeFilterName] = .simple { filters[negativeFilterName] = .simple { value in
guard let result = try filter($0) else { return nil } guard let result = try filter(value) else { return nil }
return !result return !result
} }
} }

View File

@@ -74,9 +74,11 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? {
var indentWidth = 4 var indentWidth = 4
if !arguments.isEmpty { if !arguments.isEmpty {
guard let value = arguments[0] as? Int else { guard let value = arguments[0] as? Int else {
throw TemplateSyntaxError(""" throw TemplateSyntaxError(
"""
'indent' filter width argument must be an Integer (\(String(describing: arguments[0]))) 'indent' filter width argument must be an Integer (\(String(describing: arguments[0])))
""") """
)
} }
indentWidth = value indentWidth = value
} }
@@ -84,9 +86,11 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? {
var indentationChar = " " var indentationChar = " "
if arguments.count > 1 { if arguments.count > 1 {
guard let value = arguments[1] as? String else { guard let value = arguments[1] as? String else {
throw TemplateSyntaxError(""" throw TemplateSyntaxError(
"""
'indent' filter indentation argument must be a String (\(String(describing: arguments[1])) 'indent' filter indentation argument must be a String (\(String(describing: arguments[1]))
""") """
)
} }
indentationChar = value indentationChar = value
} }

View File

@@ -12,11 +12,11 @@ class ForNode: NodeType {
let components = token.components let components = token.components
func hasToken(_ token: String, at index: Int) -> Bool { func hasToken(_ token: String, at index: Int) -> Bool {
return components.count > (index + 1) && components[index] == token components.count > (index + 1) && components[index] == token
} }
func endsOrHasToken(_ token: String, at index: Int) -> Bool { func endsOrHasToken(_ token: String, at index: Int) -> Bool {
return components.count == index || hasToken(token, at: index) components.count == index || hasToken(token, at: index)
} }
guard hasToken("in", at: 2) && endsOrHasToken("where", at: 4) else { guard hasToken("in", at: 2) && endsOrHasToken("where", at: 4) else {
@@ -154,9 +154,9 @@ class ForNode: NodeType {
} else if let resolved = resolved { } else if let resolved = resolved {
let mirror = Mirror(reflecting: resolved) let mirror = Mirror(reflecting: resolved)
switch mirror.displayStyle { switch mirror.displayStyle {
case .struct?, .tuple?: case .struct, .tuple:
values = Array(mirror.children) values = Array(mirror.children)
case .class?: case .class:
var children = Array(mirror.children) var children = Array(mirror.children)
var currentMirror: Mirror? = mirror var currentMirror: Mirror? = mirror
while let superclassMirror = currentMirror?.superclassMirror { while let superclassMirror = currentMirror?.superclassMirror {

View File

@@ -10,9 +10,8 @@ enum Operator {
return name return name
} }
} }
}
let operators: [Operator] = [ static let all: [Operator] = [
.infix("in", 5, InExpression.self), .infix("in", 5, InExpression.self),
.infix("or", 6, OrExpression.self), .infix("or", 6, OrExpression.self),
.infix("and", 7, AndExpression.self), .infix("and", 7, AndExpression.self),
@@ -23,10 +22,11 @@ let operators: [Operator] = [
.infix(">=", 10, MoreThanEqualExpression.self), .infix(">=", 10, MoreThanEqualExpression.self),
.infix("<", 10, LessThanExpression.self), .infix("<", 10, LessThanExpression.self),
.infix("<=", 10, LessThanEqualExpression.self) .infix("<=", 10, LessThanEqualExpression.self)
] ]
}
func findOperator(name: String) -> Operator? { func findOperator(name: String) -> Operator? {
for `operator` in operators where `operator`.name == name { for `operator` in Operator.all where `operator`.name == name {
return `operator` return `operator`
} }
@@ -106,7 +106,7 @@ final class IfExpressionParser {
} }
static func parser(components: [String], environment: Environment, token: Token) throws -> IfExpressionParser { static func parser(components: [String], environment: Environment, token: Token) throws -> IfExpressionParser {
return try IfExpressionParser(components: ArraySlice(components), environment: environment, token: token) try IfExpressionParser(components: ArraySlice(components), environment: environment, token: token)
} }
private init(components: ArraySlice<String>, environment: Environment, token: Token) throws { private init(components: ArraySlice<String>, environment: Environment, token: Token) throws {
@@ -117,7 +117,7 @@ final class IfExpressionParser {
if component == "(" { if component == "(" {
bracketsBalance += 1 bracketsBalance += 1
let (expression, parsedCount) = try IfExpressionParser.subExpression( let (expression, parsedCount) = try Self.subExpression(
from: components.suffix(from: index + 1), from: components.suffix(from: index + 1),
environment: environment, environment: environment,
token: token token: token
@@ -152,10 +152,10 @@ final class IfExpressionParser {
token: Token token: Token
) throws -> (Expression, Int) { ) throws -> (Expression, Int) {
var bracketsBalance = 1 var bracketsBalance = 1
let subComponents = components.prefix { let subComponents = components.prefix { component in
if $0 == "(" { if component == "(" {
bracketsBalance += 1 bracketsBalance += 1
} else if $0 == ")" { } else if component == ")" {
bracketsBalance -= 1 bracketsBalance -= 1
} }
return bracketsBalance != 0 return bracketsBalance != 0
@@ -220,7 +220,7 @@ final class IfCondition {
} }
func render(_ context: Context) throws -> String { func render(_ context: Context) throws -> String {
return try context.push { try context.push {
try renderNodes(nodes, context) try renderNodes(nodes, context)
} }
} }

View File

@@ -9,11 +9,13 @@ class IncludeNode: NodeType {
let bits = token.components let bits = token.components
guard bits.count == 2 || bits.count == 3 else { guard bits.count == 2 || bits.count == 3 else {
throw TemplateSyntaxError(""" throw TemplateSyntaxError(
"""
'include' tag requires one argument, the template file to be included. \ 'include' tag requires one argument, the template file to be included. \
A second optional argument can be used to specify the context that will \ A second optional argument can be used to specify the context that will \
be passed to the included file be passed to the included file
""") """
)
} }
return IncludeNode(templateName: Variable(bits[1]), includeContext: bits.count == 3 ? bits[2] : nil, token: token) return IncludeNode(templateName: Variable(bits[1]), includeContext: bits.count == 3 ? bits[2] : nil, token: token)

View File

@@ -1,5 +1,5 @@
class BlockContext { class BlockContext {
class var contextKey: String { return "block_context" } class var contextKey: String { "block_context" }
// contains mapping of block names to their nodes and templates where they are defined // contains mapping of block names to their nodes and templates where they are defined
var blocks: [String: [BlockNode]] var blocks: [String: [BlockNode]]

View File

@@ -23,10 +23,10 @@ struct Lexer {
self.templateName = templateName self.templateName = templateName
self.templateString = templateString self.templateString = templateString
self.lines = templateString.components(separatedBy: .newlines).enumerated().compactMap { self.lines = zip(1..., templateString.components(separatedBy: .newlines)).compactMap { index, line in
guard !$0.element.isEmpty, guard !line.isEmpty,
let range = templateString.range(of: $0.element) else { return nil } let range = templateString.range(of: line) else { return nil }
return (content: $0.element, number: UInt($0.offset + 1), range) return (content: line, number: UInt(index), range)
} }
} }
@@ -79,12 +79,12 @@ struct Lexer {
let scanner = Scanner(templateString) let scanner = Scanner(templateString)
while !scanner.isEmpty { while !scanner.isEmpty {
if let (char, text) = scanner.scanForTokenStart(Lexer.tokenChars) { if let (char, text) = scanner.scanForTokenStart(Self.tokenChars) {
if !text.isEmpty { if !text.isEmpty {
tokens.append(createToken(string: text, at: scanner.range)) tokens.append(createToken(string: text, at: scanner.range))
} }
guard let end = Lexer.tokenCharMap[char] else { continue } guard let end = Self.tokenCharMap[char] else { continue }
let result = scanner.scanForTokenEnd(end) let result = scanner.scanForTokenEnd(end)
tokens.append(createToken(string: result, at: scanner.range)) tokens.append(createToken(string: result, at: scanner.range))
} else { } else {
@@ -127,7 +127,7 @@ class Scanner {
} }
var isEmpty: Bool { var isEmpty: Bool {
return content.isEmpty content.isEmpty
} }
/// Scans for the end of a token, with a specific ending character. If we're /// Scans for the end of a token, with a specific ending character. If we're
@@ -144,8 +144,8 @@ class Scanner {
func scanForTokenEnd(_ tokenChar: Unicode.Scalar) -> String { func scanForTokenEnd(_ tokenChar: Unicode.Scalar) -> String {
var foundChar = false var foundChar = false
for (index, char) in content.unicodeScalars.enumerated() { for (index, char) in zip(0..., content.unicodeScalars) {
if foundChar && char == Scanner.tokenEndDelimiter { if foundChar && char == Self.tokenEndDelimiter {
let result = String(content.unicodeScalars.prefix(index + 1)) let result = String(content.unicodeScalars.prefix(index + 1))
content = String(content.unicodeScalars.dropFirst(index + 1)) content = String(content.unicodeScalars.dropFirst(index + 1))
range = range.upperBound..<originalContent.unicodeScalars.index(range.upperBound, offsetBy: index + 1) range = range.upperBound..<originalContent.unicodeScalars.index(range.upperBound, offsetBy: index + 1)
@@ -178,14 +178,14 @@ class Scanner {
var foundBrace = false var foundBrace = false
range = range.upperBound..<range.upperBound range = range.upperBound..<range.upperBound
for (index, char) in content.unicodeScalars.enumerated() { for (index, char) in zip(0..., content.unicodeScalars) {
if foundBrace && tokenChars.contains(char) { if foundBrace && tokenChars.contains(char) {
let result = String(content.unicodeScalars.prefix(index - 1)) let result = String(content.unicodeScalars.prefix(index - 1))
content = String(content.unicodeScalars.dropFirst(index - 1)) content = String(content.unicodeScalars.dropFirst(index - 1))
range = range.upperBound..<originalContent.unicodeScalars.index(range.upperBound, offsetBy: index - 1) range = range.upperBound..<originalContent.unicodeScalars.index(range.upperBound, offsetBy: index - 1)
return (char, result) return (char, result)
} else { } else {
foundBrace = (char == Scanner.tokenStartDelimiter) foundBrace = (char == Self.tokenStartDelimiter)
} }
} }
@@ -227,4 +227,5 @@ extension String {
} }
} }
/// Location in some content (text)
public typealias ContentLocation = (content: String, lineNumber: UInt, lineOffset: Int) public typealias ContentLocation = (content: String, lineNumber: UInt, lineOffset: Int)

View File

@@ -1,12 +1,16 @@
import Foundation import Foundation
import PathKit import PathKit
/// Type used for loading a template
public protocol Loader { public protocol Loader {
/// Load a template with the given name
func loadTemplate(name: String, environment: Environment) throws -> Template func loadTemplate(name: String, environment: Environment) throws -> Template
/// Load a template with the given list of names
func loadTemplate(names: [String], environment: Environment) throws -> Template func loadTemplate(names: [String], environment: Environment) throws -> Template
} }
extension Loader { extension Loader {
/// Default implementation, tries to load the first template that exists from the list of given names
public func loadTemplate(names: [String], environment: Environment) throws -> Template { public func loadTemplate(names: [String], environment: Environment) throws -> Template {
for name in names { for name in names {
do { do {
@@ -31,13 +35,13 @@ public class FileSystemLoader: Loader, CustomStringConvertible {
} }
public init(bundle: [Bundle]) { public init(bundle: [Bundle]) {
self.paths = bundle.map { self.paths = bundle.map { bundle in
Path($0.bundlePath) Path(bundle.bundlePath)
} }
} }
public var description: String { public var description: String {
return "FileSystemLoader(\(paths))" "FileSystemLoader(\(paths))"
} }
public func loadTemplate(name: String, environment: Environment) throws -> Template { public func loadTemplate(name: String, environment: Environment) throws -> Template {
@@ -119,6 +123,6 @@ class SuspiciousFileOperation: Error {
} }
var description: String { var description: String {
return "Path `\(path)` is located outside of base path `\(basePath)`" "Path `\(path)` is located outside of base path `\(basePath)`"
} }
} }

View File

@@ -1,5 +1,6 @@
import Foundation import Foundation
/// Represents a parsed node
public protocol NodeType { public protocol NodeType {
/// Render the node in the given context /// Render the node in the given context
func render(_ context: Context) throws -> String func render(_ context: Context) throws -> String
@@ -10,17 +11,18 @@ public protocol NodeType {
/// Render the collection of nodes in the given context /// Render the collection of nodes in the given context
public func renderNodes(_ nodes: [NodeType], _ context: Context) throws -> String { public func renderNodes(_ nodes: [NodeType], _ context: Context) throws -> String {
return try nodes try nodes
.map { .map { node in
do { do {
return try $0.render(context) return try node.render(context)
} catch { } catch {
throw error.withToken($0.token) throw error.withToken(node.token)
} }
} }
.joined() .joined()
} }
/// Simple node, used for triggering a closure during rendering
public class SimpleNode: NodeType { public class SimpleNode: NodeType {
public let handler: (Context) throws -> String public let handler: (Context) throws -> String
public let token: Token? public let token: Token?
@@ -31,10 +33,11 @@ public class SimpleNode: NodeType {
} }
public func render(_ context: Context) throws -> String { public func render(_ context: Context) throws -> String {
return try handler(context) try handler(context)
} }
} }
/// Represents a block of text, renders the text
public class TextNode: NodeType { public class TextNode: NodeType {
public let text: String public let text: String
public let token: Token? public let token: Token?
@@ -45,14 +48,17 @@ public class TextNode: NodeType {
} }
public func render(_ context: Context) throws -> String { public func render(_ context: Context) throws -> String {
return self.text self.text
} }
} }
/// Representing something that can be resolved in a context
public protocol Resolvable { public protocol Resolvable {
/// Try to resolve this with the given context
func resolve(_ context: Context) throws -> Any? func resolve(_ context: Context) throws -> Any?
} }
/// Represents a variable, renders the variable, may have conditional expressions.
public class VariableNode: NodeType { public class VariableNode: NodeType {
public let variable: Resolvable public let variable: Resolvable
public var token: Token? public var token: Token?
@@ -63,7 +69,7 @@ public class VariableNode: NodeType {
let components = token.components let components = token.components
func hasToken(_ token: String, at index: Int) -> Bool { func hasToken(_ token: String, at index: Int) -> Bool {
return components.count > (index + 1) && components[index] == token components.count > (index + 1) && components[index] == token
} }
let condition: Expression? let condition: Expression?
@@ -137,7 +143,7 @@ func stringify(_ result: Any?) -> String {
} }
func unwrap(_ array: [Any?]) -> [Any] { func unwrap(_ array: [Any?]) -> [Any] {
return array.map { (item: Any?) -> Any in array.map { (item: Any?) -> Any in
if let item = item { if let item = item {
if let items = item as? [Any?] { if let items = item as? [Any?] {
return unwrap(items) return unwrap(items)

View File

@@ -1,5 +1,7 @@
/// Creates a checker that will stop parsing if it encounters a list of tags.
/// Useful for example for scanning until a given "end"-node.
public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) { public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) {
return { _, token in { _, token in
if let name = token.components.first { if let name = token.components.first {
for tag in tags where name == tag { for tag in tags where name == tag {
return true return true
@@ -12,11 +14,13 @@ 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 /// A class for parsing an array of tokens and converts them into a collection of Node's
public class TokenParser { public class TokenParser {
/// Parser for finding a kind of node
public typealias TagParser = (TokenParser, Token) throws -> NodeType public typealias TagParser = (TokenParser, Token) throws -> NodeType
fileprivate var tokens: [Token] fileprivate var tokens: [Token]
fileprivate let environment: Environment fileprivate let environment: Environment
/// Simple initializer
public init(tokens: [Token], environment: Environment) { public init(tokens: [Token], environment: Environment) {
self.tokens = tokens self.tokens = tokens
self.environment = environment self.environment = environment
@@ -24,9 +28,11 @@ public class TokenParser {
/// Parse the given tokens into nodes /// Parse the given tokens into nodes
public func parse() throws -> [NodeType] { public func parse() throws -> [NodeType] {
return try parse(nil) try parse(nil)
} }
/// Parse nodes until a specific "something" is detected, determined by the provided closure.
/// Combine this with the `until(:)` function above to scan nodes until a given token.
public func parse(_ parseUntil: ((_ parser: TokenParser, _ token: Token) -> (Bool))?) throws -> [NodeType] { public func parse(_ parseUntil: ((_ parser: TokenParser, _ token: Token) -> (Bool))?) throws -> [NodeType] {
var nodes = [NodeType]() var nodes = [NodeType]()
@@ -61,6 +67,7 @@ public class TokenParser {
return nodes return nodes
} }
/// Pop the next token (returning it)
public func nextToken() -> Token? { public func nextToken() -> Token? {
if !tokens.isEmpty { if !tokens.isEmpty {
return tokens.remove(at: 0) return tokens.remove(at: 0)
@@ -69,23 +76,24 @@ public class TokenParser {
return nil return nil
} }
/// Insert a token
public func prependToken(_ token: Token) { public func prependToken(_ token: Token) {
tokens.insert(token, at: 0) tokens.insert(token, at: 0)
} }
/// Create filter expression from a string contained in provided token /// Create filter expression from a string contained in provided token
public func compileFilter(_ filterToken: String, containedIn token: Token) throws -> Resolvable { public func compileFilter(_ filterToken: String, containedIn token: Token) throws -> Resolvable {
return try environment.compileFilter(filterToken, containedIn: token) try environment.compileFilter(filterToken, containedIn: token)
} }
/// Create boolean expression from components contained in provided token /// Create boolean expression from components contained in provided token
public func compileExpression(components: [String], token: Token) throws -> Expression { public func compileExpression(components: [String], token: Token) throws -> Expression {
return try environment.compileExpression(components: components, containedIn: token) try environment.compileExpression(components: components, containedIn: token)
} }
/// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token /// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token
public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable { public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable {
return try environment.compileResolvable(token, containedIn: containingToken) try environment.compileResolvable(token, containedIn: containingToken)
} }
} }
@@ -111,10 +119,12 @@ extension Environment {
if suggestedFilters.isEmpty { if suggestedFilters.isEmpty {
throw TemplateSyntaxError("Unknown filter '\(name)'.") throw TemplateSyntaxError("Unknown filter '\(name)'.")
} else { } else {
throw TemplateSyntaxError(""" throw TemplateSyntaxError(
"""
Unknown filter '\(name)'. \ Unknown filter '\(name)'. \
Found similar filters: \(suggestedFilters.map { "'\($0)'" }.joined(separator: ", ")). Found similar filters: \(suggestedFilters.map { "'\($0)'" }.joined(separator: ", ")).
""") """
)
} }
} }
@@ -134,7 +144,7 @@ extension Environment {
/// Create filter expression from a string /// Create filter expression from a string
public func compileFilter(_ token: String) throws -> Resolvable { public func compileFilter(_ token: String) throws -> Resolvable {
return try FilterExpression(token: token, environment: self) try FilterExpression(token: token, environment: self)
} }
/// Create filter expression from a string contained in provided token /// Create filter expression from a string contained in provided token
@@ -165,26 +175,26 @@ extension Environment {
/// Create resolvable (i.e. range variable or filter expression) from a string /// Create resolvable (i.e. range variable or filter expression) from a string
public func compileResolvable(_ token: String) throws -> Resolvable { public func compileResolvable(_ token: String) throws -> Resolvable {
return try RangeVariable(token, environment: self) try RangeVariable(token, environment: self)
?? compileFilter(token) ?? compileFilter(token)
} }
/// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token /// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token
public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable { public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable {
return try RangeVariable(token, environment: self, containedIn: containingToken) try RangeVariable(token, environment: self, containedIn: containingToken)
?? compileFilter(token, containedIn: containingToken) ?? compileFilter(token, containedIn: containingToken)
} }
/// Create boolean expression from components contained in provided token /// Create boolean expression from components contained in provided token
public func compileExpression(components: [String], containedIn token: Token) throws -> Expression { public func compileExpression(components: [String], containedIn token: Token) throws -> Expression {
return try IfExpressionParser.parser(components: components, environment: self, token: token).parse() try IfExpressionParser.parser(components: components, environment: self, token: token).parse()
} }
} }
// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows // https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
extension String { extension String {
subscript(_ index: Int) -> Character { subscript(_ index: Int) -> Character {
return self[self.index(self.startIndex, offsetBy: index)] self[self.index(self.startIndex, offsetBy: index)]
} }
func levenshteinDistance(_ target: String) -> Int { func levenshteinDistance(_ target: String) -> Int {

View File

@@ -2,6 +2,7 @@ import Foundation
import PathKit import PathKit
#if os(Linux) #if os(Linux)
// swiftlint:disable:next prefixed_toplevel_constant
let NSFileNoSuchFileError = 4 let NSFileNoSuchFileError = 4
#endif #endif
@@ -77,6 +78,6 @@ open class Template: ExpressibleByStringLiteral {
// swiftlint:disable discouraged_optional_collection // swiftlint:disable discouraged_optional_collection
/// Render the given template /// Render the given template
open func render(_ dictionary: [String: Any]? = nil) throws -> String { open func render(_ dictionary: [String: Any]? = nil) throws -> String {
return try render(Context(dictionary: dictionary ?? [:], environment: environment)) try render(Context(dictionary: dictionary ?? [:], environment: environment))
} }
} }

View File

@@ -19,7 +19,7 @@ extension String {
if character == separate { if character == separate {
if separate != separator { if separate != separator {
word.append(separate) word.append(separate)
} else if (singleQuoteCount % 2 == 0 || doubleQuoteCount % 2 == 0) && !word.isEmpty { } else if (singleQuoteCount.isMultiple(of: 2) || doubleQuoteCount.isMultiple(of: 2)) && !word.isEmpty {
appendWord(word, to: &components) appendWord(word, to: &components)
word = "" word = ""
} }
@@ -75,7 +75,7 @@ public struct SourceMap: Equatable {
static let unknown = SourceMap() 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 lhs.filename == rhs.filename && lhs.location == rhs.location
} }
} }
@@ -106,25 +106,25 @@ public class Token: Equatable {
/// A token representing a piece of text. /// A token representing a piece of text.
public static func text(value: String, at sourceMap: SourceMap) -> Token { public static func text(value: String, at sourceMap: SourceMap) -> Token {
return Token(contents: value, kind: .text, sourceMap: sourceMap) Token(contents: value, kind: .text, sourceMap: sourceMap)
} }
/// A token representing a variable. /// A token representing a variable.
public static func variable(value: String, at sourceMap: SourceMap) -> Token { public static func variable(value: String, at sourceMap: SourceMap) -> Token {
return Token(contents: value, kind: .variable, sourceMap: sourceMap) Token(contents: value, kind: .variable, sourceMap: sourceMap)
} }
/// A token representing a comment. /// A token representing a comment.
public static func comment(value: String, at sourceMap: SourceMap) -> Token { public static func comment(value: String, at sourceMap: SourceMap) -> Token {
return Token(contents: value, kind: .comment, sourceMap: sourceMap) Token(contents: value, kind: .comment, sourceMap: sourceMap)
} }
/// A token representing a template block. /// A token representing a template block.
public static func block(value: String, at sourceMap: SourceMap) -> Token { public static func block(value: String, at sourceMap: SourceMap) -> Token {
return Token(contents: value, kind: .block, sourceMap: sourceMap) Token(contents: value, kind: .block, sourceMap: sourceMap)
} }
public static func == (lhs: Token, rhs: Token) -> Bool { public static func == (lhs: Token, rhs: Token) -> Bool {
return lhs.contents == rhs.contents && lhs.kind == rhs.kind && lhs.sourceMap == rhs.sourceMap lhs.contents == rhs.contents && lhs.kind == rhs.kind && lhs.sourceMap == rhs.sourceMap
} }
} }

View File

@@ -16,8 +16,8 @@ class FilterExpression: Resolvable {
let filterBits = bits[bits.indices.suffix(from: 1)] let filterBits = bits[bits.indices.suffix(from: 1)]
do { do {
filters = try filterBits.map { filters = try filterBits.map { bit in
let (name, arguments) = parseFilterComponents(token: $0) let (name, arguments) = parseFilterComponents(token: bit)
let filter = try environment.findFilter(name) let filter = try environment.findFilter(name)
return (filter, arguments) return (filter, arguments)
} }
@@ -208,13 +208,14 @@ protocol Normalizable {
extension Array: Normalizable { extension Array: Normalizable {
func normalize() -> Any? { func normalize() -> Any? {
return map { $0 as Any } map { $0 as Any }
} }
} }
// swiftlint:disable:next legacy_objc_type
extension NSArray: Normalizable { extension NSArray: Normalizable {
func normalize() -> Any? { func normalize() -> Any? {
return map { $0 as Any } map { $0 as Any }
} }
} }
@@ -273,8 +274,10 @@ protocol AnyOptional {
extension Optional: AnyOptional { extension Optional: AnyOptional {
var wrapped: Any? { var wrapped: Any? {
switch self { switch self {
case let .some(value): return value case let .some(value):
case .none: return nil return value
case .none:
return nil
} }
} }
} }

View File

@@ -1,9 +0,0 @@
import Foundation
#if !swift(>=4.2)
extension ArraySlice where Element: Equatable {
func firstIndex(of element: Element) -> Int? {
return index(of: element)
}
}
#endif

View File

@@ -16,7 +16,7 @@
"tag": "0.14.2" "tag": "0.14.2"
}, },
"source_files": [ "source_files": [
"Sources/*.swift" "Sources/Stencil/*.swift"
], ],
"platforms": { "platforms": {
"ios": "8.0", "ios": "8.0",
@@ -25,7 +25,6 @@
}, },
"cocoapods_version": ">= 1.7.0", "cocoapods_version": ">= 1.7.0",
"swift_versions": [ "swift_versions": [
"4.2",
"5.0" "5.0"
], ],
"requires_arc": true, "requires_arc": true,

View File

@@ -1,8 +0,0 @@
import XCTest
import StencilTests
var tests = [XCTestCaseEntry]()
tests += StencilTests.__allTests()
XCTMain(tests)

View File

@@ -4,35 +4,35 @@ import XCTest
final class ContextTests: XCTestCase { final class ContextTests: XCTestCase {
func testContextSubscripting() { func testContextSubscripting() {
describe("Context Subscripting") { describe("Context Subscripting") { test in
var context = Context() var context = Context()
$0.before { test.before {
context = Context(dictionary: ["name": "Kyle"]) context = Context(dictionary: ["name": "Kyle"])
} }
$0.it("allows you to get a value via subscripting") { test.it("allows you to get a value via subscripting") {
try expect(context["name"] as? String) == "Kyle" try expect(context["name"] as? String) == "Kyle"
} }
$0.it("allows you to set a value via subscripting") { test.it("allows you to set a value via subscripting") {
context["name"] = "Katie" context["name"] = "Katie"
try expect(context["name"] as? String) == "Katie" try expect(context["name"] as? String) == "Katie"
} }
$0.it("allows you to remove a value via subscripting") { test.it("allows you to remove a value via subscripting") {
context["name"] = nil context["name"] = nil
try expect(context["name"]).to.beNil() try expect(context["name"]).to.beNil()
} }
$0.it("allows you to retrieve a value from a parent") { test.it("allows you to retrieve a value from a parent") {
try context.push { try context.push {
try expect(context["name"] as? String) == "Kyle" try expect(context["name"] as? String) == "Kyle"
} }
} }
$0.it("allows you to override a parent's value") { test.it("allows you to override a parent's value") {
try context.push { try context.push {
context["name"] = "Katie" context["name"] = "Katie"
try expect(context["name"] as? String) == "Katie" try expect(context["name"] as? String) == "Katie"
@@ -42,13 +42,13 @@ final class ContextTests: XCTestCase {
} }
func testContextRestoration() { func testContextRestoration() {
describe("Context Restoration") { describe("Context Restoration") { test in
var context = Context() var context = Context()
$0.before { test.before {
context = Context(dictionary: ["name": "Kyle"]) context = Context(dictionary: ["name": "Kyle"])
} }
$0.it("allows you to pop to restore previous state") { test.it("allows you to pop to restore previous state") {
context.push { context.push {
context["name"] = "Katie" context["name"] = "Katie"
} }
@@ -56,7 +56,7 @@ final class ContextTests: XCTestCase {
try expect(context["name"] as? String) == "Kyle" try expect(context["name"] as? String) == "Kyle"
} }
$0.it("allows you to remove a parent's value in a level") { test.it("allows you to remove a parent's value in a level") {
try context.push { try context.push {
context["name"] = nil context["name"] = nil
try expect(context["name"]).to.beNil() try expect(context["name"]).to.beNil()
@@ -65,7 +65,7 @@ final class ContextTests: XCTestCase {
try expect(context["name"] as? String) == "Kyle" try expect(context["name"] as? String) == "Kyle"
} }
$0.it("allows you to push a dictionary and run a closure then restoring previous state") { test.it("allows you to push a dictionary and run a closure then restoring previous state") {
var didRun = false var didRun = false
try context.push(dictionary: ["name": "Katie"]) { try context.push(dictionary: ["name": "Katie"]) {
@@ -77,7 +77,7 @@ final class ContextTests: XCTestCase {
try expect(context["name"] as? String) == "Kyle" try expect(context["name"] as? String) == "Kyle"
} }
$0.it("allows you to flatten the context contents") { test.it("allows you to flatten the context contents") {
try context.push(dictionary: ["test": "abc"]) { try context.push(dictionary: ["test": "abc"]) {
let flattened = context.flatten() let flattened = context.flatten()

View File

@@ -0,0 +1,125 @@
import PathKit
import Spectre
@testable import Stencil
import XCTest
final class EnvironmentBaseAndChildTemplateTests: XCTestCase {
private var environment = Environment(loader: ExampleLoader())
private var childTemplate: Template = ""
private var baseTemplate: Template = ""
override func setUp() {
super.setUp()
let path = Path(#file as String) + ".." + "fixtures"
let loader = FileSystemLoader(paths: [path])
environment = Environment(loader: loader)
childTemplate = ""
baseTemplate = ""
}
override func tearDown() {
super.tearDown()
}
func testSyntaxErrorInBaseTemplate() throws {
childTemplate = try environment.loadTemplate(name: "invalid-child-super.html")
baseTemplate = try environment.loadTemplate(name: "invalid-base.html")
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: "extends \"invalid-base.html\"",
baseToken: "target|unknown"
)
}
func testSyntaxErrorInChildTemplate() throws {
childTemplate = Template(
templateString: """
{% extends "base.html" %}
{% block body %}Child {{ target|unknown }}{% endblock %}
""",
environment: environment,
name: nil
)
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)
}
}

View File

@@ -0,0 +1,88 @@
import PathKit
import Spectre
@testable import Stencil
import XCTest
final class EnvironmentIncludeTemplateTests: XCTestCase {
private var environment = Environment(loader: ExampleLoader())
private var template: Template = ""
private var includedTemplate: Template = ""
override func setUp() {
super.setUp()
let path = Path(#file as String) + ".." + "fixtures"
let loader = FileSystemLoader(paths: [path])
environment = Environment(loader: loader)
template = ""
includedTemplate = ""
}
override func tearDown() {
super.tearDown()
}
func testSyntaxError() throws {
template = Template(templateString: """
{% include "invalid-include.html" %}
""", environment: environment)
includedTemplate = try environment.loadTemplate(name: "invalid-include.html")
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)
}
}

View File

@@ -4,8 +4,8 @@ import Spectre
import XCTest import XCTest
final class EnvironmentTests: XCTestCase { final class EnvironmentTests: XCTestCase {
var environment = Environment(loader: ExampleLoader()) private var environment = Environment(loader: ExampleLoader())
var template: Template = "" private var template: Template = ""
override func setUp() { override func setUp() {
super.setUp() super.setUp()
@@ -26,6 +26,10 @@ final class EnvironmentTests: XCTestCase {
template = "" template = ""
} }
override func tearDown() {
super.tearDown()
}
func testLoading() { func testLoading() {
it("can load a template from a name") { it("can load a template from a name") {
let template = try self.environment.loadTemplate(name: "example.html") let template = try self.environment.loadTemplate(name: "example.html")
@@ -207,242 +211,11 @@ final class EnvironmentTests: XCTestCase {
} }
} }
final class EnvironmentIncludeTemplateTests: XCTestCase { // MARK: - Helpers
var environment = Environment(loader: ExampleLoader())
var template: Template = ""
var includedTemplate: Template = ""
override func setUp() {
super.setUp()
let path = Path(#file as String) + ".." + "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 as String) + ".." + "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: "extends \"invalid-base.html\"",
baseToken: "target|unknown")
}
func testSyntaxErrorInChildTemplate() throws {
childTemplate = Template(
templateString: """
{% extends "base.html" %}
{% block body %}Child {{ target|unknown }}{% endblock %}
""",
environment: environment,
name: nil
)
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<T: Error>() throws -> T {
var thrownError: Error?
do {
_ = try expression()
} catch {
thrownError = error
}
if let thrownError = thrownError {
if let thrownError = thrownError as? T {
return thrownError
} else {
throw failure("\(thrownError) is not \(T.self)")
}
} else {
throw failure("expression did not throw an error")
}
}
}
extension XCTestCase {
func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError {
guard let range = template.templateString.range(of: token) else {
fatalError("Can't find '\(token)' in '\(template)'")
}
let lexer = Lexer(templateString: template.templateString)
let location = lexer.rangeLocation(range)
let sourceMap = SourceMap(filename: template.name, location: location)
let token = Token.block(value: token, at: sourceMap)
return TemplateSyntaxError(reason: description, token: token, stackTrace: [])
}
}
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)
}
throw TemplateDoesNotExist(templateNames: [name], loader: self)
}
}
private class CustomTemplate: Template { private class CustomTemplate: Template {
// swiftlint:disable discouraged_optional_collection // swiftlint:disable discouraged_optional_collection
override func render(_ dictionary: [String: Any]? = nil) throws -> String { override func render(_ dictionary: [String: Any]? = nil) throws -> String {
return "here" "here"
} }
} }

View File

@@ -3,7 +3,7 @@ import Spectre
import XCTest import XCTest
final class ExpressionsTests: XCTestCase { final class ExpressionsTests: XCTestCase {
let parser = TokenParser(tokens: [], environment: Environment()) private let parser = TokenParser(tokens: [], environment: Environment())
private func makeExpression(_ components: [String]) -> Expression { private func makeExpression(_ components: [String]) -> Expression {
do { do {

View File

@@ -363,10 +363,12 @@ final class FilterTests: XCTestCase {
Two Two
""" """
])) ]))
// swiftlint:disable indentation_width
try expect(result) == """ try expect(result) == """
One One
Two Two
""" """
// swiftlint:enable indentation_width
} }
func testIndentNotEmptyLines() throws { func testIndentNotEmptyLines() throws {
@@ -383,6 +385,7 @@ final class FilterTests: XCTestCase {
""" """
])) ]))
// swiftlint:disable indentation_width
try expect(result) == """ try expect(result) == """
One One
@@ -391,6 +394,7 @@ final class FilterTests: XCTestCase {
""" """
// swiftlint:enable indentation_width
} }
func testDynamicFilters() throws { func testDynamicFilters() throws {

View File

@@ -23,9 +23,9 @@ final class FilterTagTests: XCTestCase {
it("can render filters with arguments") { it("can render filters with arguments") {
let ext = Extension() let ext = Extension()
ext.registerFilter("split") { ext.registerFilter("split") { value, args in
guard let value = $0 as? String, guard let value = value as? String,
let argument = $1.first as? String else { return $0 } let argument = args.first as? String else { return value }
return value.components(separatedBy: argument) return value.components(separatedBy: argument)
} }
let env = Environment(extensions: [ext]) let env = Environment(extensions: [ext])
@@ -37,11 +37,11 @@ final class FilterTagTests: XCTestCase {
it("can render filters with quote as an argument") { it("can render filters with quote as an argument") {
let ext = Extension() let ext = Extension()
ext.registerFilter("replace") { ext.registerFilter("replace") { value, args in
guard let value = $0 as? String, guard let value = value as? String,
$1.count == 2, args.count == 2,
let search = $1.first as? String, let search = args.first as? String,
let replacement = $1.last as? String else { return $0 } let replacement = args.last as? String else { return value }
return value.replacingOccurrences(of: search, with: replacement) return value.replacingOccurrences(of: search, with: replacement)
} }
let env = Environment(extensions: [ext]) let env = Environment(extensions: [ext])

View File

@@ -3,9 +3,10 @@ import Spectre
import XCTest import XCTest
final class ForNodeTests: XCTestCase { final class ForNodeTests: XCTestCase {
let context = Context(dictionary: [ private let context = Context(dictionary: [
"items": [1, 2, 3], "items": [1, 2, 3],
"anyItems": [1, 2, 3] as [Any], "anyItems": [1, 2, 3] as [Any],
// swiftlint:disable:next legacy_objc_type
"nsItems": NSArray(array: [1, 2, 3]), "nsItems": NSArray(array: [1, 2, 3]),
"emptyItems": [Int](), "emptyItems": [Int](),
"dict": [ "dict": [
@@ -313,6 +314,8 @@ final class ForNodeTests: XCTestCase {
} }
} }
// MARK: - Helpers
private struct MyStruct { private struct MyStruct {
let string: String let string: String
let number: Int let number: Int

View File

@@ -0,0 +1,63 @@
import PathKit
import Spectre
@testable import Stencil
import XCTest
extension Expectation {
@discardableResult
func toThrow<T: Error>() throws -> T {
var thrownError: Error?
do {
_ = try expression()
} catch {
thrownError = error
}
if let thrownError = thrownError {
if let thrownError = thrownError as? T {
return thrownError
} else {
throw failure("\(thrownError) is not \(T.self)")
}
} else {
throw failure("expression did not throw an error")
}
}
}
extension XCTestCase {
func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError {
guard let range = template.templateString.range(of: token) else {
fatalError("Can't find '\(token)' in '\(template)'")
}
let lexer = Lexer(templateString: template.templateString)
let location = lexer.rangeLocation(range)
let sourceMap = SourceMap(filename: template.name, location: location)
let token = Token.block(value: token, at: sourceMap)
return TemplateSyntaxError(reason: description, token: token, stackTrace: [])
}
}
// MARK: - Test Types
class ExampleLoader: Loader {
func loadTemplate(name: String, environment: Environment) throws -> Template {
if name == "example.html" {
return Template(templateString: "Hello World!", environment: environment, name: name)
}
throw TemplateDoesNotExist(templateNames: [name], loader: self)
}
}
class ErrorNode: NodeType {
let token: Token?
init(token: Token? = nil) {
self.token = token
}
func render(_ context: Context) throws -> String {
throw TemplateSyntaxError("Custom Error")
}
}

View File

@@ -2,10 +2,6 @@ import Spectre
@testable import Stencil @testable import Stencil
import XCTest import XCTest
private struct SomeType {
let value: String? = nil
}
final class IfNodeTests: XCTestCase { final class IfNodeTests: XCTestCase {
func testParseIf() { func testParseIf() {
it("can parse an if block") { it("can parse an if block") {
@@ -286,3 +282,9 @@ final class IfNodeTests: XCTestCase {
try expect(renderNodes(nodes, Context(dictionary: ["value": 4]))) == "false" try expect(renderNodes(nodes, Context(dictionary: ["value": 4]))) == "false"
} }
} }
// MARK: - Helpers
private struct SomeType {
let value: String? = nil
}

View File

@@ -4,9 +4,9 @@ import Spectre
import XCTest import XCTest
final class IncludeTests: XCTestCase { final class IncludeTests: XCTestCase {
let path = Path(#file as String) + ".." + "fixtures" private let path = Path(#file as String) + ".." + "fixtures"
lazy var loader = FileSystemLoader(paths: [path]) private lazy var loader = FileSystemLoader(paths: [path])
lazy var environment = Environment(loader: loader) private lazy var environment = Environment(loader: loader)
func testParsing() { func testParsing() {
it("throws an error when no template is given") { it("throws an error when no template is given") {

View File

@@ -4,9 +4,9 @@ import Stencil
import XCTest import XCTest
final class InheritanceTests: XCTestCase { final class InheritanceTests: XCTestCase {
let path = Path(#file as String) + ".." + "fixtures" private let path = Path(#file as String) + ".." + "fixtures"
lazy var loader = FileSystemLoader(paths: [path]) private lazy var loader = FileSystemLoader(paths: [path])
lazy var environment = Environment(loader: loader) private lazy var environment = Environment(loader: loader)
func testInheritance() { func testInheritance() {
it("can inherit from another template") { it("can inherit from another template") {

View File

@@ -82,6 +82,7 @@ final class LexerTests: XCTestCase {
} }
func testNewlines() throws { func testNewlines() throws {
// swiftlint:disable indentation_width
let templateString = """ let templateString = """
My name is {% My name is {%
if name if name
@@ -92,6 +93,7 @@ final class LexerTests: XCTestCase {
}}{% }}{%
endif %}. endif %}.
""" """
// swiftlint:enable indentation_width
let lexer = Lexer(templateString: templateString) let lexer = Lexer(templateString: templateString)
let tokens = lexer.tokenize() let tokens = lexer.tokenize()

View File

@@ -2,19 +2,8 @@ import Spectre
@testable import Stencil @testable import Stencil
import XCTest import XCTest
class ErrorNode: NodeType {
let token: Token?
init(token: Token? = nil) {
self.token = token
}
func render(_ context: Context) throws -> String {
throw TemplateSyntaxError("Custom Error")
}
}
final class NodeTests: XCTestCase { final class NodeTests: XCTestCase {
let context = Context(dictionary: [ private let context = Context(dictionary: [
"name": "Kyle", "name": "Kyle",
"age": 27, "age": 27,
"items": [1, 2, 3] "items": [1, 2, 3]

View File

@@ -5,9 +5,9 @@ import XCTest
final class NowNodeTests: XCTestCase { final class NowNodeTests: XCTestCase {
func testParsing() { func testParsing() {
it("parses default format without any now arguments") { it("parses default format without any now arguments") {
#if os(Linux) #if os(Linux)
throw skip() throw skip()
#else #else
let tokens: [Token] = [ .block(value: "now", at: .unknown) ] let tokens: [Token] = [ .block(value: "now", at: .unknown) ]
let parser = TokenParser(tokens: tokens, environment: Environment()) let parser = TokenParser(tokens: tokens, environment: Environment())
@@ -15,36 +15,36 @@ final class NowNodeTests: XCTestCase {
let node = nodes.first as? NowNode let node = nodes.first as? NowNode
try expect(nodes.count) == 1 try expect(nodes.count) == 1
try expect(node?.format.variable) == "\"yyyy-MM-dd 'at' HH:mm\"" try expect(node?.format.variable) == "\"yyyy-MM-dd 'at' HH:mm\""
#endif #endif
} }
it("parses now with a format") { it("parses now with a format") {
#if os(Linux) #if os(Linux)
throw skip() throw skip()
#else #else
let tokens: [Token] = [ .block(value: "now \"HH:mm\"", at: .unknown) ] let tokens: [Token] = [ .block(value: "now \"HH:mm\"", at: .unknown) ]
let parser = TokenParser(tokens: tokens, environment: Environment()) let parser = TokenParser(tokens: tokens, environment: Environment())
let nodes = try parser.parse() let nodes = try parser.parse()
let node = nodes.first as? NowNode let node = nodes.first as? NowNode
try expect(nodes.count) == 1 try expect(nodes.count) == 1
try expect(node?.format.variable) == "\"HH:mm\"" try expect(node?.format.variable) == "\"HH:mm\""
#endif #endif
} }
} }
func testRendering() { func testRendering() {
it("renders the date") { it("renders the date") {
#if os(Linux) #if os(Linux)
throw skip() throw skip()
#else #else
let node = NowNode(format: Variable("\"yyyy-MM-dd\"")) let node = NowNode(format: Variable("\"yyyy-MM-dd\""))
let formatter = DateFormatter() let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd" formatter.dateFormat = "yyyy-MM-dd"
let date = formatter.string(from: NSDate() as Date) let date = formatter.string(from: Date())
try expect(try node.render(Context())) == date try expect(try node.render(Context())) == date
#endif #endif
} }
} }
} }

View File

@@ -57,8 +57,8 @@ final class TokenParserTests: XCTestCase {
try expect(try parser.parse()).toThrow(TemplateSyntaxError( try expect(try parser.parse()).toThrow(TemplateSyntaxError(
reason: "Unknown template tag 'unknown'", reason: "Unknown template tag 'unknown'",
token: tokens.first) token: tokens.first
) ))
} }
} }
} }

View File

@@ -2,20 +2,8 @@ import Spectre
import Stencil import Stencil
import XCTest import XCTest
private struct CustomNode: NodeType {
let token: Token?
func render(_ context: Context) throws -> String {
return "Hello World"
}
}
private struct Article {
let title: String
let author: String
}
final class StencilTests: XCTestCase { final class StencilTests: XCTestCase {
lazy var environment: Environment = { private lazy var environment: Environment = {
let exampleExtension = Extension() let exampleExtension = Extension()
exampleExtension.registerSimpleTag("simpletag") { _ in exampleExtension.registerSimpleTag("simpletag") { _ in
"Hello World" "Hello World"
@@ -66,3 +54,17 @@ final class StencilTests: XCTestCase {
} }
} }
} }
// MARK: - Helpers
private struct CustomNode: NodeType {
let token: Token?
func render(_ context: Context) throws -> String {
"Hello World"
}
}
private struct Article {
let title: String
let author: String
}

View File

@@ -2,47 +2,8 @@ import Spectre
@testable import Stencil @testable import Stencil
import XCTest import XCTest
#if os(OSX)
@objc
class Superclass: NSObject {
@objc let name = "Foo"
}
@objc
class Object: Superclass {
@objc let title = "Hello World"
}
#endif
private struct Person {
let name: String
}
private struct Article {
let author: Person
}
private class WebSite {
let url: String = "blog.com"
}
private class Blog: WebSite {
let articles: [Article] = [Article(author: Person(name: "Kyle"))]
let featuring: Article? = Article(author: Person(name: "Jhon"))
}
@dynamicMemberLookup
private struct DynamicStruct: DynamicMemberLookup {
subscript(dynamicMember member: String) -> Any? {
member == "test" ? "this is a dynamic response" : nil
}
}
private enum DynamicEnum: String, DynamicMemberLookup {
case someValue = "this is raw value"
}
final class VariableTests: XCTestCase { final class VariableTests: XCTestCase {
let context: Context = { private let context: Context = {
let ext = Extension() let ext = Extension()
ext.registerFilter("incr") { arg in ext.registerFilter("incr") { arg in
(arg.flatMap { toNumber(value: $0) } ?? 0) + 1 (arg.flatMap { toNumber(value: $0) } ?? 0) + 1
@@ -66,9 +27,9 @@ final class VariableTests: XCTestCase {
"struct": DynamicStruct() "struct": DynamicStruct()
] ]
], environment: environment) ], environment: environment)
#if os(OSX) #if os(OSX)
context["object"] = Object() context["object"] = Object()
#endif #endif
return context return context
}() }()
@@ -214,7 +175,7 @@ final class VariableTests: XCTestCase {
} }
func testKVO() { func testKVO() {
#if os(OSX) #if os(OSX)
it("can resolve a value via KVO") { it("can resolve a value via KVO") {
let variable = Variable("object.title") let variable = Variable("object.title")
let result = try variable.resolve(self.context) as? String let result = try variable.resolve(self.context) as? String
@@ -232,7 +193,7 @@ final class VariableTests: XCTestCase {
let result = try variable.resolve(self.context) as? String let result = try variable.resolve(self.context) as? String
try expect(result).to.beNil() try expect(result).to.beNil()
} }
#endif #endif
} }
func testTuple() { func testTuple() {
@@ -285,7 +246,7 @@ final class VariableTests: XCTestCase {
} }
} }
#if os(OSX) #if os(OSX)
it("can resolve a subscript via KVO") { it("can resolve a subscript via KVO") {
try self.context.push(dictionary: ["property": "name"]) { try self.context.push(dictionary: ["property": "name"]) {
let variable = Variable("object[property]") let variable = Variable("object[property]")
@@ -293,7 +254,7 @@ final class VariableTests: XCTestCase {
try expect(result) == "Foo" try expect(result) == "Foo"
} }
} }
#endif #endif
it("can resolve an optional subscript via reflection") { it("can resolve an optional subscript via reflection") {
try self.context.push(dictionary: ["property": "featuring"]) { try self.context.push(dictionary: ["property": "featuring"]) {
@@ -394,3 +355,44 @@ final class VariableTests: XCTestCase {
} }
} }
} }
// MARK: - Helpers
#if os(OSX)
@objc
class Superclass: NSObject {
@objc let name = "Foo"
}
@objc
class Object: Superclass {
@objc let title = "Hello World"
}
#endif
private struct Person {
let name: String
}
private struct Article {
let author: Person
}
private class WebSite {
let url: String = "blog.com"
}
private class Blog: WebSite {
let articles: [Article] = [Article(author: Person(name: "Kyle"))]
let featuring: Article? = Article(author: Person(name: "Jhon"))
}
@dynamicMemberLookup
private struct DynamicStruct: DynamicMemberLookup {
subscript(dynamicMember member: String) -> Any? {
member == "test" ? "this is a dynamic response" : nil
}
}
private enum DynamicEnum: String, DynamicMemberLookup {
case someValue = "this is raw value"
}

View File

@@ -1,228 +0,0 @@
import XCTest
extension ContextTests {
static let __allTests = [
("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 = [
("testLoading", testLoading),
("testRendering", testRendering),
("testRenderingError", testRenderingError),
("testSyntaxError", testSyntaxError),
("testUnknownFilter", testUnknownFilter),
]
}
extension ExpressionsTests {
static let __allTests = [
("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),
]
}
extension FilterTagTests {
static let __allTests = [
("testFilterTag", testFilterTag),
]
}
extension FilterTests {
static let __allTests = [
("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 = [
("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 = [
("testParsing", testParsing),
("testRendering", testRendering),
]
}
extension InheritanceTests {
static let __allTests = [
("testInheritance", testInheritance),
]
}
extension LexerTests {
static let __allTests = [
("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 = [
("testRendering", testRendering),
("testTextNode", testTextNode),
("testVariableNode", testVariableNode),
]
}
extension NowNodeTests {
static let __allTests = [
("testParsing", testParsing),
("testRendering", testRendering),
]
}
extension StencilTests {
static let __allTests = [
("testStencil", testStencil),
]
}
extension TemplateLoaderTests {
static let __allTests = [
("testDictionaryLoader", testDictionaryLoader),
("testFileSystemLoader", testFileSystemLoader),
]
}
extension TemplateTests {
static let __allTests = [
("testTemplate", testTemplate),
]
}
extension TokenParserTests {
static let __allTests = [
("testTokenParser", testTokenParser),
]
}
extension TokenTests {
static let __allTests = [
("testToken", testToken),
]
}
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),
]
}
#if !os(macOS)
public func __allTests() -> [XCTestCaseEntry] {
return [
testCase(ContextTests.__allTests),
testCase(EnvironmentBaseAndChildTemplateTests.__allTests),
testCase(EnvironmentIncludeTemplateTests.__allTests),
testCase(EnvironmentTests.__allTests),
testCase(ExpressionsTests.__allTests),
testCase(FilterTagTests.__allTests),
testCase(FilterTests.__allTests),
testCase(ForNodeTests.__allTests),
testCase(IfNodeTests.__allTests),
testCase(IncludeTests.__allTests),
testCase(InheritanceTests.__allTests),
testCase(LexerTests.__allTests),
testCase(NodeTests.__allTests),
testCase(NowNodeTests.__allTests),
testCase(StencilTests.__allTests),
testCase(TemplateLoaderTests.__allTests),
testCase(TemplateTests.__allTests),
testCase(TokenParserTests.__allTests),
testCase(TokenTests.__allTests),
testCase(VariableTests.__allTests),
]
}
#endif

View File

@@ -10,7 +10,7 @@ else
fi fi
# possible paths # possible paths
paths_sources="Sources" paths_sources="Sources/Stencil"
paths_tests="Tests/StencilTests" paths_tests="Tests/StencilTests"
# load selected group # load selected group