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

View File

@@ -2,7 +2,9 @@
### 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

View File

@@ -1,4 +1,4 @@
// swift-tools-version:4.2
// swift-tools-version:5.0
import PackageDescription
let package = Package(
@@ -13,11 +13,11 @@ let package = Package(
targets: [
.target(name: "Stencil", dependencies: [
"PathKit"
], path: "Sources"),
]),
.testTarget(name: "StencilTests", dependencies: [
"Stencil",
"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
[![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
syntax similar to Django and Mustache. If you're familiar with these, you will
feel right at home with Stencil.

View File

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

View File

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

View File

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

View File

@@ -20,12 +20,12 @@ public class TemplateDoesNotExist: Error, CustomStringConvertible {
public struct TemplateSyntaxError: Error, Equatable, CustomStringConvertible {
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 stackTrace: [Token]
public var templateName: String? { return token?.sourceMap.filename }
public var templateName: String? { token?.sourceMap.filename }
var allTokens: [Token] {
return stackTrace + (token.map { [$0] } ?? [])
stackTrace + (token.map { [$0] } ?? [])
}
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 {
return value
value
}
var description: String {
return "\(value)"
"\(value)"
}
}
@@ -34,7 +34,7 @@ final class VariableExpression: Expression, CustomStringConvertible {
}
var description: String {
return "(variable: \(variable))"
"(variable: \(variable))"
}
/// Resolves a variable in the given context as boolean
@@ -60,7 +60,7 @@ final class VariableExpression: Expression, CustomStringConvertible {
}
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 {
return "not \(expression)"
"not \(expression)"
}
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 {
return "(\(lhs) in \(rhs))"
"(\(lhs) in \(rhs))"
}
func evaluate(context: Context) throws -> Bool {
@@ -125,7 +125,7 @@ final class OrExpression: Expression, InfixOperator, CustomStringConvertible {
}
var description: String {
return "(\(lhs) or \(rhs))"
"(\(lhs) or \(rhs))"
}
func evaluate(context: Context) throws -> Bool {
@@ -148,7 +148,7 @@ final class AndExpression: Expression, InfixOperator, CustomStringConvertible {
}
var description: String {
return "(\(lhs) and \(rhs))"
"(\(lhs) and \(rhs))"
}
func evaluate(context: Context) throws -> Bool {
@@ -171,7 +171,7 @@ class EqualityExpression: Expression, InfixOperator, CustomStringConvertible {
}
var description: String {
return "(\(lhs) == \(rhs))"
"(\(lhs) == \(rhs))"
}
func evaluate(context: Context) throws -> Bool {
@@ -206,7 +206,7 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
}
var description: String {
return "(\(lhs) \(symbol) \(rhs))"
"(\(lhs) \(symbol) \(rhs))"
}
func evaluate(context: Context) throws -> Bool {
@@ -225,61 +225,61 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
}
var symbol: String {
return ""
""
}
func compare(lhs: Number, rhs: Number) -> Bool {
return false
false
}
}
class MoreThanExpression: NumericExpression {
override var symbol: String {
return ">"
">"
}
override func compare(lhs: Number, rhs: Number) -> Bool {
return lhs > rhs
lhs > rhs
}
}
class MoreThanEqualExpression: NumericExpression {
override var symbol: String {
return ">="
">="
}
override func compare(lhs: Number, rhs: Number) -> Bool {
return lhs >= rhs
lhs >= rhs
}
}
class LessThanExpression: NumericExpression {
override var symbol: String {
return "<"
"<"
}
override func compare(lhs: Number, rhs: Number) -> Bool {
return lhs < rhs
lhs < rhs
}
}
class LessThanEqualExpression: NumericExpression {
override var symbol: String {
return "<="
"<="
}
override func compare(lhs: Number, rhs: Number) -> Bool {
return lhs <= rhs
lhs <= rhs
}
}
class InequalityExpression: EqualityExpression {
override var description: String {
return "(\(lhs) != \(rhs))"
"(\(lhs) != \(rhs))"
}
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 {
typealias TagParser = (TokenParser, Token) throws -> NodeType
var tags = [String: TagParser]()
var tags = [String: TagParser]()
var filters = [String: Filter]()
/// Simple initializer
public init() {
}
@@ -20,11 +22,11 @@ open class Extension {
}
/// 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?) {
// swiftlint:disable:previous discouraged_optional_boolean
filters[name] = .simple(filter)
filters[negativeFilterName] = .simple {
guard let result = try filter($0) else { return nil }
filters[negativeFilterName] = .simple { value in
guard let result = try filter(value) else { return nil }
return !result
}
}

View File

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

View File

@@ -12,11 +12,11 @@ class ForNode: NodeType {
let components = token.components
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 {
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 {
@@ -154,9 +154,9 @@ class ForNode: NodeType {
} else if let resolved = resolved {
let mirror = Mirror(reflecting: resolved)
switch mirror.displayStyle {
case .struct?, .tuple?:
case .struct, .tuple:
values = Array(mirror.children)
case .class?:
case .class:
var children = Array(mirror.children)
var currentMirror: Mirror? = mirror
while let superclassMirror = currentMirror?.superclassMirror {

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
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
var blocks: [String: [BlockNode]]

View File

@@ -23,10 +23,10 @@ struct Lexer {
self.templateName = templateName
self.templateString = templateString
self.lines = templateString.components(separatedBy: .newlines).enumerated().compactMap {
guard !$0.element.isEmpty,
let range = templateString.range(of: $0.element) else { return nil }
return (content: $0.element, number: UInt($0.offset + 1), range)
self.lines = zip(1..., templateString.components(separatedBy: .newlines)).compactMap { index, line in
guard !line.isEmpty,
let range = templateString.range(of: line) else { return nil }
return (content: line, number: UInt(index), range)
}
}
@@ -79,12 +79,12 @@ struct Lexer {
let scanner = Scanner(templateString)
while !scanner.isEmpty {
if let (char, text) = scanner.scanForTokenStart(Lexer.tokenChars) {
if let (char, text) = scanner.scanForTokenStart(Self.tokenChars) {
if !text.isEmpty {
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)
tokens.append(createToken(string: result, at: scanner.range))
} else {
@@ -127,7 +127,7 @@ class Scanner {
}
var isEmpty: Bool {
return content.isEmpty
content.isEmpty
}
/// 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 {
var foundChar = false
for (index, char) in content.unicodeScalars.enumerated() {
if foundChar && char == Scanner.tokenEndDelimiter {
for (index, char) in zip(0..., content.unicodeScalars) {
if foundChar && char == Self.tokenEndDelimiter {
let result = String(content.unicodeScalars.prefix(index + 1))
content = String(content.unicodeScalars.dropFirst(index + 1))
range = range.upperBound..<originalContent.unicodeScalars.index(range.upperBound, offsetBy: index + 1)
@@ -178,14 +178,14 @@ class Scanner {
var foundBrace = false
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) {
let result = String(content.unicodeScalars.prefix(index - 1))
content = String(content.unicodeScalars.dropFirst(index - 1))
range = range.upperBound..<originalContent.unicodeScalars.index(range.upperBound, offsetBy: index - 1)
return (char, result)
} 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)

View File

@@ -1,12 +1,16 @@
import Foundation
import PathKit
/// Type used for loading a template
public protocol Loader {
/// Load a template with the given name
func loadTemplate(name: String, environment: Environment) throws -> Template
/// Load a template with the given list of names
func loadTemplate(names: [String], environment: Environment) throws -> Template
}
extension Loader {
/// Default implementation, tries to load the first template that exists from the list of given names
public func loadTemplate(names: [String], environment: Environment) throws -> Template {
for name in names {
do {
@@ -31,13 +35,13 @@ public class FileSystemLoader: Loader, CustomStringConvertible {
}
public init(bundle: [Bundle]) {
self.paths = bundle.map {
Path($0.bundlePath)
self.paths = bundle.map { bundle in
Path(bundle.bundlePath)
}
}
public var description: String {
return "FileSystemLoader(\(paths))"
"FileSystemLoader(\(paths))"
}
public func loadTemplate(name: String, environment: Environment) throws -> Template {
@@ -119,6 +123,6 @@ class SuspiciousFileOperation: Error {
}
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
/// Represents a parsed node
public protocol NodeType {
/// Render the node in the given context
func render(_ context: Context) throws -> String
@@ -10,17 +11,18 @@ public protocol NodeType {
/// Render the collection of nodes in the given context
public func renderNodes(_ nodes: [NodeType], _ context: Context) throws -> String {
return try nodes
.map {
try nodes
.map { node in
do {
return try $0.render(context)
return try node.render(context)
} catch {
throw error.withToken($0.token)
throw error.withToken(node.token)
}
}
.joined()
}
/// Simple node, used for triggering a closure during rendering
public class SimpleNode: NodeType {
public let handler: (Context) throws -> String
public let token: Token?
@@ -31,10 +33,11 @@ public class SimpleNode: NodeType {
}
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 let text: String
public let token: Token?
@@ -45,14 +48,17 @@ public class TextNode: NodeType {
}
public func render(_ context: Context) throws -> String {
return self.text
self.text
}
}
/// Representing something that can be resolved in a context
public protocol Resolvable {
/// Try to resolve this with the given context
func resolve(_ context: Context) throws -> Any?
}
/// Represents a variable, renders the variable, may have conditional expressions.
public class VariableNode: NodeType {
public let variable: Resolvable
public var token: Token?
@@ -63,7 +69,7 @@ public class VariableNode: NodeType {
let components = token.components
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?
@@ -137,7 +143,7 @@ func stringify(_ result: Any?) -> String {
}
func unwrap(_ array: [Any?]) -> [Any] {
return array.map { (item: Any?) -> Any in
array.map { (item: Any?) -> Any in
if let item = item {
if let items = item as? [Any?] {
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) {
return { _, token in
{ _, token in
if let name = token.components.first {
for tag in tags where name == tag {
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
public class TokenParser {
/// Parser for finding a kind of node
public typealias TagParser = (TokenParser, Token) throws -> NodeType
fileprivate var tokens: [Token]
fileprivate let environment: Environment
/// Simple initializer
public init(tokens: [Token], environment: Environment) {
self.tokens = tokens
self.environment = environment
@@ -24,9 +28,11 @@ public class TokenParser {
/// Parse the given tokens into nodes
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] {
var nodes = [NodeType]()
@@ -61,6 +67,7 @@ public class TokenParser {
return nodes
}
/// Pop the next token (returning it)
public func nextToken() -> Token? {
if !tokens.isEmpty {
return tokens.remove(at: 0)
@@ -69,23 +76,24 @@ public class TokenParser {
return nil
}
/// Insert a token
public func prependToken(_ token: Token) {
tokens.insert(token, at: 0)
}
/// Create filter expression from a string contained in provided token
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
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
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 {
throw TemplateSyntaxError("Unknown filter '\(name)'.")
} else {
throw TemplateSyntaxError("""
throw TemplateSyntaxError(
"""
Unknown filter '\(name)'. \
Found similar filters: \(suggestedFilters.map { "'\($0)'" }.joined(separator: ", ")).
""")
"""
)
}
}
@@ -134,7 +144,7 @@ extension Environment {
/// Create filter expression from a string
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
@@ -165,26 +175,26 @@ extension Environment {
/// Create resolvable (i.e. range variable or filter expression) from a string
public func compileResolvable(_ token: String) throws -> Resolvable {
return try RangeVariable(token, environment: self)
try RangeVariable(token, environment: self)
?? compileFilter(token)
}
/// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token
public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable {
return try RangeVariable(token, environment: self, containedIn: containingToken)
try RangeVariable(token, environment: self, containedIn: containingToken)
?? compileFilter(token, containedIn: containingToken)
}
/// Create boolean expression from components contained in provided token
public func compileExpression(components: [String], containedIn token: Token) throws -> Expression {
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
extension String {
subscript(_ index: Int) -> Character {
return self[self.index(self.startIndex, offsetBy: index)]
self[self.index(self.startIndex, offsetBy: index)]
}
func levenshteinDistance(_ target: String) -> Int {

View File

@@ -2,6 +2,7 @@ import Foundation
import PathKit
#if os(Linux)
// swiftlint:disable:next prefixed_toplevel_constant
let NSFileNoSuchFileError = 4
#endif
@@ -77,6 +78,6 @@ open class Template: ExpressibleByStringLiteral {
// swiftlint:disable discouraged_optional_collection
/// Render the given template
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 separate != separator {
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)
word = ""
}
@@ -75,7 +75,7 @@ public struct SourceMap: Equatable {
static let unknown = SourceMap()
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.
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.
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.
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.
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 {
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)]
do {
filters = try filterBits.map {
let (name, arguments) = parseFilterComponents(token: $0)
filters = try filterBits.map { bit in
let (name, arguments) = parseFilterComponents(token: bit)
let filter = try environment.findFilter(name)
return (filter, arguments)
}
@@ -208,13 +208,14 @@ protocol Normalizable {
extension Array: Normalizable {
func normalize() -> Any? {
return map { $0 as Any }
map { $0 as Any }
}
}
// swiftlint:disable:next legacy_objc_type
extension NSArray: Normalizable {
func normalize() -> Any? {
return map { $0 as Any }
map { $0 as Any }
}
}
@@ -273,8 +274,10 @@ protocol AnyOptional {
extension Optional: AnyOptional {
var wrapped: Any? {
switch self {
case let .some(value): return value
case .none: return nil
case let .some(value):
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"
},
"source_files": [
"Sources/*.swift"
"Sources/Stencil/*.swift"
],
"platforms": {
"ios": "8.0",
@@ -25,7 +25,6 @@
},
"cocoapods_version": ">= 1.7.0",
"swift_versions": [
"4.2",
"5.0"
],
"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 {
func testContextSubscripting() {
describe("Context Subscripting") {
describe("Context Subscripting") { test in
var context = Context()
$0.before {
test.before {
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"
}
$0.it("allows you to set a value via subscripting") {
test.it("allows you to set a value via subscripting") {
context["name"] = "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
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 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 {
context["name"] = "Katie"
try expect(context["name"] as? String) == "Katie"
@@ -42,13 +42,13 @@ final class ContextTests: XCTestCase {
}
func testContextRestoration() {
describe("Context Restoration") {
describe("Context Restoration") { test in
var context = Context()
$0.before {
test.before {
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["name"] = "Katie"
}
@@ -56,7 +56,7 @@ final class ContextTests: XCTestCase {
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 {
context["name"] = nil
try expect(context["name"]).to.beNil()
@@ -65,7 +65,7 @@ final class ContextTests: XCTestCase {
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
try context.push(dictionary: ["name": "Katie"]) {
@@ -77,7 +77,7 @@ final class ContextTests: XCTestCase {
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"]) {
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
final class EnvironmentTests: XCTestCase {
var environment = Environment(loader: ExampleLoader())
var template: Template = ""
private var environment = Environment(loader: ExampleLoader())
private var template: Template = ""
override func setUp() {
super.setUp()
@@ -26,6 +26,10 @@ final class EnvironmentTests: XCTestCase {
template = ""
}
override func tearDown() {
super.tearDown()
}
func testLoading() {
it("can load a template from a name") {
let template = try self.environment.loadTemplate(name: "example.html")
@@ -207,242 +211,11 @@ final class EnvironmentTests: XCTestCase {
}
}
final class EnvironmentIncludeTemplateTests: XCTestCase {
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)
}
}
// MARK: - Helpers
private class CustomTemplate: Template {
// swiftlint:disable discouraged_optional_collection
override func render(_ dictionary: [String: Any]? = nil) throws -> String {
return "here"
"here"
}
}

View File

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

View File

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

View File

@@ -23,9 +23,9 @@ final class FilterTagTests: XCTestCase {
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 }
ext.registerFilter("split") { value, args in
guard let value = value as? String,
let argument = args.first as? String else { return value }
return value.components(separatedBy: argument)
}
let env = Environment(extensions: [ext])
@@ -37,11 +37,11 @@ final class FilterTagTests: XCTestCase {
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 }
ext.registerFilter("replace") { value, args in
guard let value = value as? String,
args.count == 2,
let search = args.first as? String,
let replacement = args.last as? String else { return value }
return value.replacingOccurrences(of: search, with: replacement)
}
let env = Environment(extensions: [ext])

View File

@@ -3,9 +3,10 @@ import Spectre
import XCTest
final class ForNodeTests: XCTestCase {
let context = Context(dictionary: [
private let context = Context(dictionary: [
"items": [1, 2, 3],
"anyItems": [1, 2, 3] as [Any],
// swiftlint:disable:next legacy_objc_type
"nsItems": NSArray(array: [1, 2, 3]),
"emptyItems": [Int](),
"dict": [
@@ -313,6 +314,8 @@ final class ForNodeTests: XCTestCase {
}
}
// MARK: - Helpers
private struct MyStruct {
let string: String
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
import XCTest
private struct SomeType {
let value: String? = nil
}
final class IfNodeTests: XCTestCase {
func testParseIf() {
it("can parse an if block") {
@@ -286,3 +282,9 @@ final class IfNodeTests: XCTestCase {
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
final class IncludeTests: XCTestCase {
let path = Path(#file as String) + ".." + "fixtures"
lazy var loader = FileSystemLoader(paths: [path])
lazy var environment = Environment(loader: loader)
private let path = Path(#file as String) + ".." + "fixtures"
private lazy var loader = FileSystemLoader(paths: [path])
private lazy var environment = Environment(loader: loader)
func testParsing() {
it("throws an error when no template is given") {

View File

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

View File

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

View File

@@ -2,19 +2,8 @@ import Spectre
@testable import Stencil
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 {
let context = Context(dictionary: [
private let context = Context(dictionary: [
"name": "Kyle",
"age": 27,
"items": [1, 2, 3]

View File

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

View File

@@ -2,20 +2,8 @@ import Spectre
import Stencil
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 {
lazy var environment: Environment = {
private lazy var environment: Environment = {
let exampleExtension = Extension()
exampleExtension.registerSimpleTag("simpletag") { _ in
"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
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 {
let context: Context = {
private let context: Context = {
let ext = Extension()
ext.registerFilter("incr") { arg in
(arg.flatMap { toNumber(value: $0) } ?? 0) + 1
@@ -66,9 +27,9 @@ final class VariableTests: XCTestCase {
"struct": DynamicStruct()
]
], environment: environment)
#if os(OSX)
#if os(OSX)
context["object"] = Object()
#endif
#endif
return context
}()
@@ -214,7 +175,7 @@ final class VariableTests: XCTestCase {
}
func testKVO() {
#if os(OSX)
#if os(OSX)
it("can resolve a value via KVO") {
let variable = Variable("object.title")
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
try expect(result).to.beNil()
}
#endif
#endif
}
func testTuple() {
@@ -285,7 +246,7 @@ final class VariableTests: XCTestCase {
}
}
#if os(OSX)
#if os(OSX)
it("can resolve a subscript via KVO") {
try self.context.push(dictionary: ["property": "name"]) {
let variable = Variable("object[property]")
@@ -293,7 +254,7 @@ final class VariableTests: XCTestCase {
try expect(result) == "Foo"
}
}
#endif
#endif
it("can resolve an optional subscript via reflection") {
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
# possible paths
paths_sources="Sources"
paths_sources="Sources/Stencil"
paths_tests="Tests/StencilTests"
# load selected group