Merge pull request #249 from stencilproject/feature/swiftlint
SwiftLint integration
This commit is contained in:
54
.swiftlint.yml
Normal file
54
.swiftlint.yml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
opt_in_rules:
|
||||||
|
- anyobject_protocol
|
||||||
|
- array_init
|
||||||
|
- attributes
|
||||||
|
- closure_end_indentation
|
||||||
|
- closure_spacing
|
||||||
|
- contains_over_first_not_nil
|
||||||
|
- convenience_type
|
||||||
|
- discouraged_optional_boolean
|
||||||
|
- discouraged_optional_collection
|
||||||
|
- empty_count
|
||||||
|
- empty_string
|
||||||
|
- fallthrough
|
||||||
|
- fatal_error_message
|
||||||
|
- first_where
|
||||||
|
- force_unwrapping
|
||||||
|
- implicit_return
|
||||||
|
- implicitly_unwrapped_optional
|
||||||
|
- joined_default_parameter
|
||||||
|
- literal_expression_end_indentation
|
||||||
|
- lower_acl_than_parent
|
||||||
|
- modifier_order
|
||||||
|
- multiline_arguments
|
||||||
|
- multiline_function_chains
|
||||||
|
- multiline_parameters
|
||||||
|
- number_separator
|
||||||
|
- operator_usage_whitespace
|
||||||
|
- overridden_super_call
|
||||||
|
- override_in_extension
|
||||||
|
- private_action
|
||||||
|
- private_outlet
|
||||||
|
- prohibited_super_call
|
||||||
|
- redundant_nil_coalescing
|
||||||
|
- sorted_first_last
|
||||||
|
- sorted_imports
|
||||||
|
- trailing_closure
|
||||||
|
- unavailable_function
|
||||||
|
- unneeded_parentheses_in_closure_argument
|
||||||
|
- vertical_parameter_alignment_on_call
|
||||||
|
- yoda_condition
|
||||||
|
|
||||||
|
# Rules customization
|
||||||
|
line_length:
|
||||||
|
warning: 120
|
||||||
|
error: 200
|
||||||
|
|
||||||
|
nesting:
|
||||||
|
type_level:
|
||||||
|
warning: 2
|
||||||
|
|
||||||
|
# Exclude generated files
|
||||||
|
excluded:
|
||||||
|
- .build
|
||||||
|
- Tests/StencilTests/XCTestManifests.swift
|
||||||
@@ -20,5 +20,8 @@ sudo: required
|
|||||||
dist: trusty
|
dist: trusty
|
||||||
install:
|
install:
|
||||||
- eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"
|
- eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"
|
||||||
|
- if [ "$TRAVIS_OS_NAME" == "osx" ]; then wget --output-document /tmp/SwiftLint.pkg https://github.com/realm/SwiftLint/releases/download/0.27.0/SwiftLint.pkg &&
|
||||||
|
sudo installer -pkg /tmp/SwiftLint.pkg -target /; fi
|
||||||
script:
|
script:
|
||||||
- swift test
|
- swift test
|
||||||
|
- if [ "$TRAVIS_OS_NAME" == "osx" ]; then swiftlint; fi
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ _None_
|
|||||||
- `Token` type converted to struct to allow computing token components only once.
|
- `Token` type converted to struct to allow computing token components only once.
|
||||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
[#256](https://github.com/stencilproject/Stencil/pull/256)
|
[#256](https://github.com/stencilproject/Stencil/pull/256)
|
||||||
|
- Added SwiftLint to the project.
|
||||||
|
[David Jennes](https://github.com/djbe)
|
||||||
|
[#249](https://github.com/stencilproject/Stencil/pull/249)
|
||||||
|
|
||||||
|
|
||||||
## 0.13.1
|
## 0.13.1
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ public class Context {
|
|||||||
|
|
||||||
public let environment: Environment
|
public let environment: Environment
|
||||||
|
|
||||||
init(dictionary: [String: Any]? = nil, environment: Environment? = nil) {
|
init(dictionary: [String: Any] = [:], environment: Environment? = nil) {
|
||||||
if let dictionary = dictionary {
|
if !dictionary.isEmpty {
|
||||||
dictionaries = [dictionary]
|
dictionaries = [dictionary]
|
||||||
} else {
|
} else {
|
||||||
dictionaries = []
|
dictionaries = []
|
||||||
@@ -28,17 +28,16 @@ public class Context {
|
|||||||
|
|
||||||
/// Set a variable in the current context, deleting the variable if it's nil
|
/// Set a variable in the current context, deleting the variable if it's nil
|
||||||
set(value) {
|
set(value) {
|
||||||
if let dictionary = dictionaries.popLast() {
|
if var dictionary = dictionaries.popLast() {
|
||||||
var mutable_dictionary = dictionary
|
dictionary[key] = value
|
||||||
mutable_dictionary[key] = value
|
dictionaries.append(dictionary)
|
||||||
dictionaries.append(mutable_dictionary)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push a new level into the Context
|
/// Push a new level into the Context
|
||||||
fileprivate func push(_ dictionary: [String: Any]? = nil) {
|
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
|
||||||
@@ -47,7 +46,7 @@ public class Context {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
public func push<Result>(dictionary: [String: Any]? = nil, 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()
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ public struct Environment {
|
|||||||
public var loader: Loader?
|
public var loader: Loader?
|
||||||
|
|
||||||
public init(loader: Loader? = nil,
|
public init(loader: Loader? = nil,
|
||||||
extensions: [Extension]? = nil,
|
extensions: [Extension] = [],
|
||||||
templateClass: Template.Type = Template.self) {
|
templateClass: Template.Type = Template.self) {
|
||||||
|
|
||||||
self.templateClass = templateClass
|
self.templateClass = templateClass
|
||||||
self.loader = loader
|
self.loader = loader
|
||||||
self.extensions = (extensions ?? []) + [DefaultExtension()]
|
self.extensions = extensions + [DefaultExtension()]
|
||||||
}
|
}
|
||||||
|
|
||||||
public func loadTemplate(name: String) throws -> Template {
|
public func loadTemplate(name: String) throws -> Template {
|
||||||
@@ -29,17 +29,17 @@ public struct Environment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func renderTemplate(name: String, context: [String: Any]? = nil) throws -> String {
|
public func renderTemplate(name: String, context: [String: Any] = [:]) throws -> String {
|
||||||
let template = try loadTemplate(name: name)
|
let template = try loadTemplate(name: name)
|
||||||
return try render(template: template, context: context)
|
return try render(template: template, context: context)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func renderTemplate(string: String, context: [String: Any]? = nil) throws -> String {
|
public func renderTemplate(string: String, context: [String: Any] = [:]) throws -> String {
|
||||||
let template = templateClass.init(templateString: string, environment: self)
|
let template = templateClass.init(templateString: string, environment: self)
|
||||||
return try render(template: template, context: context)
|
return try render(template: template, context: context)
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(template: Template, context: [String: Any]?) throws -> String {
|
func render(template: Template, context: [String: Any]) throws -> String {
|
||||||
// update template environment as it can be created from string literal with default environment
|
// update template environment as it can be created from string literal with default environment
|
||||||
template.environment = self
|
template.environment = self
|
||||||
return try template.render(context)
|
return try template.render(context)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
|
|||||||
public internal(set) var stackTrace: [Token]
|
public internal(set) var stackTrace: [Token]
|
||||||
public var templateName: String? { return token?.sourceMap.filename }
|
public var templateName: String? { return token?.sourceMap.filename }
|
||||||
var allTokens: [Token] {
|
var allTokens: [Token] {
|
||||||
return stackTrace + (token.map({ [$0] }) ?? [])
|
return stackTrace + (token.map { [$0] } ?? [])
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) {
|
public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) {
|
||||||
@@ -50,7 +50,7 @@ extension Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public protocol ErrorReporter: class {
|
public protocol ErrorReporter: AnyObject {
|
||||||
func renderError(_ error: Error) -> String
|
func renderError(_ error: Error) -> String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,14 @@ public protocol Expression: CustomStringConvertible {
|
|||||||
func evaluate(context: Context) throws -> Bool
|
func evaluate(context: Context) throws -> Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protocol InfixOperator: Expression {
|
protocol InfixOperator: Expression {
|
||||||
init(lhs: Expression, rhs: Expression)
|
init(lhs: Expression, rhs: Expression)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protocol PrefixOperator: Expression {
|
protocol PrefixOperator: Expression {
|
||||||
init(expression: Expression)
|
init(expression: Expression)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
final class StaticExpression: Expression, CustomStringConvertible {
|
final class StaticExpression: Expression, CustomStringConvertible {
|
||||||
let value: Bool
|
let value: Bool
|
||||||
|
|
||||||
@@ -29,7 +26,6 @@ final class StaticExpression: Expression, CustomStringConvertible {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
final class VariableExpression: Expression, CustomStringConvertible {
|
final class VariableExpression: Expression, CustomStringConvertible {
|
||||||
let variable: Resolvable
|
let variable: Resolvable
|
||||||
|
|
||||||
@@ -68,7 +64,6 @@ final class VariableExpression: Expression, CustomStringConvertible {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
final class NotExpression: Expression, PrefixOperator, CustomStringConvertible {
|
final class NotExpression: Expression, PrefixOperator, CustomStringConvertible {
|
||||||
let expression: Expression
|
let expression: Expression
|
||||||
|
|
||||||
@@ -144,7 +139,6 @@ final class OrExpression: Expression, InfixOperator, CustomStringConvertible {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
final class AndExpression: Expression, InfixOperator, CustomStringConvertible {
|
final class AndExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||||
let lhs: Expression
|
let lhs: Expression
|
||||||
let rhs: Expression
|
let rhs: Expression
|
||||||
@@ -168,7 +162,6 @@ final class AndExpression: Expression, InfixOperator, CustomStringConvertible {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class EqualityExpression: Expression, InfixOperator, CustomStringConvertible {
|
class EqualityExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||||
let lhs: Expression
|
let lhs: Expression
|
||||||
let rhs: Expression
|
let rhs: Expression
|
||||||
@@ -204,7 +197,6 @@ class EqualityExpression: Expression, InfixOperator, CustomStringConvertible {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
|
class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||||
let lhs: Expression
|
let lhs: Expression
|
||||||
let rhs: Expression
|
let rhs: Expression
|
||||||
@@ -215,7 +207,7 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
return "(\(lhs) \(op) \(rhs))"
|
return "(\(lhs) \(symbol) \(rhs))"
|
||||||
}
|
}
|
||||||
|
|
||||||
func evaluate(context: Context) throws -> Bool {
|
func evaluate(context: Context) throws -> Bool {
|
||||||
@@ -233,7 +225,7 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var op: String {
|
var symbol: String {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,9 +234,8 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class MoreThanExpression: NumericExpression {
|
class MoreThanExpression: NumericExpression {
|
||||||
override var op: String {
|
override var symbol: String {
|
||||||
return ">"
|
return ">"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,9 +244,8 @@ class MoreThanExpression: NumericExpression {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class MoreThanEqualExpression: NumericExpression {
|
class MoreThanEqualExpression: NumericExpression {
|
||||||
override var op: String {
|
override var symbol: String {
|
||||||
return ">="
|
return ">="
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,9 +254,8 @@ class MoreThanEqualExpression: NumericExpression {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class LessThanExpression: NumericExpression {
|
class LessThanExpression: NumericExpression {
|
||||||
override var op: String {
|
override var symbol: String {
|
||||||
return "<"
|
return "<"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,9 +264,8 @@ class LessThanExpression: NumericExpression {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class LessThanEqualExpression: NumericExpression {
|
class LessThanEqualExpression: NumericExpression {
|
||||||
override var op: String {
|
override var symbol: String {
|
||||||
return "<="
|
return "<="
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +274,6 @@ class LessThanEqualExpression: NumericExpression {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class InequalityExpression: EqualityExpression {
|
class InequalityExpression: EqualityExpression {
|
||||||
override var description: String {
|
override var description: String {
|
||||||
return "(\(lhs) != \(rhs))"
|
return "(\(lhs) != \(rhs))"
|
||||||
@@ -297,7 +284,7 @@ class InequalityExpression: EqualityExpression {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// swiftlint:disable:next cyclomatic_complexity
|
||||||
func toNumber(value: Any) -> Number? {
|
func toNumber(value: Any) -> Number? {
|
||||||
if let value = value as? Float {
|
if let value = value as? Float {
|
||||||
return Number(value)
|
return Number(value)
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ open class Extension {
|
|||||||
|
|
||||||
/// Registers a simple template tag with a name and a handler
|
/// Registers a simple template tag with a name and a handler
|
||||||
public func registerSimpleTag(_ name: String, handler: @escaping (Context) throws -> String) {
|
public func registerSimpleTag(_ name: String, handler: @escaping (Context) throws -> String) {
|
||||||
registerTag(name, parser: { parser, token in
|
registerTag(name) { _, token in
|
||||||
return SimpleNode(token: token, handler: handler)
|
SimpleNode(token: token, handler: handler)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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?) {
|
||||||
filters[name] = .simple(filter)
|
filters[name] = .simple(filter)
|
||||||
filters[negativeFilterName] = .simple {
|
filters[negativeFilterName] = .simple {
|
||||||
@@ -44,7 +45,6 @@ open class Extension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DefaultExtension: Extension {
|
class DefaultExtension: Extension {
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
@@ -77,7 +77,6 @@ class DefaultExtension: Extension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protocol FilterType {
|
protocol FilterType {
|
||||||
func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any?
|
func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ class FilterNode : NodeType {
|
|||||||
let value = try renderNodes(nodes, context)
|
let value = try renderNodes(nodes, context)
|
||||||
|
|
||||||
return try context.push(dictionary: ["filter_value": value]) {
|
return try context.push(dictionary: ["filter_value": value]) {
|
||||||
return try VariableNode(variable: resolvable, token: token).render(context)
|
try VariableNode(variable: resolvable, token: token).render(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var indentWidth = 4
|
var indentWidth = 4
|
||||||
if arguments.count > 0 {
|
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])))
|
||||||
@@ -99,18 +99,17 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
|||||||
indentFirst = value
|
indentFirst = value
|
||||||
}
|
}
|
||||||
|
|
||||||
let indentation = [String](repeating: indentationChar, count: indentWidth).joined(separator: "")
|
let indentation = [String](repeating: indentationChar, count: indentWidth).joined()
|
||||||
return indent(stringify(value), indentation: indentation, indentFirst: indentFirst)
|
return indent(stringify(value), indentation: indentation, indentFirst: indentFirst)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func indent(_ content: String, indentation: String, indentFirst: Bool) -> String {
|
func indent(_ content: String, indentation: String, indentFirst: Bool) -> String {
|
||||||
guard !indentation.isEmpty else { return content }
|
guard !indentation.isEmpty else { return content }
|
||||||
|
|
||||||
var lines = content.components(separatedBy: .newlines)
|
var lines = content.components(separatedBy: .newlines)
|
||||||
let firstLine = (indentFirst ? indentation : "") + lines.removeFirst()
|
let firstLine = (indentFirst ? indentation : "") + lines.removeFirst()
|
||||||
let result = lines.reduce([firstLine]) { (result, line) in
|
let result = lines.reduce([firstLine]) { result, line in
|
||||||
return result + [(line.isEmpty ? "" : "\(indentation)\(line)")]
|
result + [(line.isEmpty ? "" : "\(indentation)\(line)")]
|
||||||
}
|
}
|
||||||
return result.joined(separator: "\n")
|
return result.joined(separator: "\n")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,10 +46,24 @@ class ForNode : NodeType {
|
|||||||
_ = parser.nextToken()
|
_ = parser.nextToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes: emptyNodes, where: `where`, token: token)
|
return ForNode(
|
||||||
|
resolvable: resolvable,
|
||||||
|
loopVariables: loopVariables,
|
||||||
|
nodes: forNodes,
|
||||||
|
emptyNodes: emptyNodes,
|
||||||
|
where: `where`,
|
||||||
|
token: token
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(resolvable: Resolvable, loopVariables: [String], nodes: [NodeType], emptyNodes: [NodeType], where: Expression? = nil, token: Token? = nil) {
|
init(
|
||||||
|
resolvable: Resolvable,
|
||||||
|
loopVariables: [String],
|
||||||
|
nodes: [NodeType],
|
||||||
|
emptyNodes: [NodeType],
|
||||||
|
where: Expression? = nil,
|
||||||
|
token: Token? = nil
|
||||||
|
) {
|
||||||
self.resolvable = resolvable
|
self.resolvable = resolvable
|
||||||
self.loopVariables = loopVariables
|
self.loopVariables = loopVariables
|
||||||
self.nodes = nodes
|
self.nodes = nodes
|
||||||
@@ -58,10 +72,48 @@ class ForNode : NodeType {
|
|||||||
self.token = token
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result {
|
func render(_ context: Context) throws -> String {
|
||||||
|
var values = try resolve(context)
|
||||||
|
|
||||||
|
if let `where` = self.where {
|
||||||
|
values = try values.filter { item -> Bool in
|
||||||
|
try push(value: item, context: context) {
|
||||||
|
try `where`.evaluate(context: context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !values.isEmpty {
|
||||||
|
let count = values.count
|
||||||
|
|
||||||
|
return try zip(0..., values)
|
||||||
|
.map { index, item in
|
||||||
|
let forContext: [String: Any] = [
|
||||||
|
"first": index == 0,
|
||||||
|
"last": index == (count - 1),
|
||||||
|
"counter": index + 1,
|
||||||
|
"counter0": index,
|
||||||
|
"length": count
|
||||||
|
]
|
||||||
|
|
||||||
|
return try context.push(dictionary: ["forloop": forContext]) {
|
||||||
|
try push(value: item, context: context) {
|
||||||
|
try renderNodes(nodes, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.joined()
|
||||||
|
}
|
||||||
|
|
||||||
|
return try context.push {
|
||||||
|
try renderNodes(emptyNodes, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result {
|
||||||
if loopVariables.isEmpty {
|
if loopVariables.isEmpty {
|
||||||
return try context.push() {
|
return try context.push {
|
||||||
return try closure()
|
try closure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,27 +123,26 @@ class ForNode : NodeType {
|
|||||||
throw TemplateSyntaxError("Tuple '\(value)' has less values than loop variables")
|
throw TemplateSyntaxError("Tuple '\(value)' has less values than loop variables")
|
||||||
}
|
}
|
||||||
var variablesContext = [String: Any]()
|
var variablesContext = [String: Any]()
|
||||||
valueMirror.children.prefix(loopVariables.count).enumerated().forEach({ (offset, element) in
|
valueMirror.children.prefix(loopVariables.count).enumerated().forEach { offset, element in
|
||||||
if loopVariables[offset] != "_" {
|
if loopVariables[offset] != "_" {
|
||||||
variablesContext[loopVariables[offset]] = element.value
|
variablesContext[loopVariables[offset]] = element.value
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
return try context.push(dictionary: variablesContext) {
|
return try context.push(dictionary: variablesContext) {
|
||||||
return try closure()
|
try closure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return try context.push(dictionary: [loopVariables.first!: value]) {
|
return try context.push(dictionary: [loopVariables.first ?? "": value]) {
|
||||||
return try closure()
|
try closure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(_ context: Context) throws -> String {
|
private func resolve(_ context: Context) throws -> [Any] {
|
||||||
let resolved = try resolvable.resolve(context)
|
let resolved = try resolvable.resolve(context)
|
||||||
|
|
||||||
var values: [Any]
|
var values: [Any]
|
||||||
|
|
||||||
if let dictionary = resolved as? [String: Any], !dictionary.isEmpty {
|
if let dictionary = resolved as? [String: Any], !dictionary.isEmpty {
|
||||||
values = dictionary.sorted { $0.key < $1.key }
|
values = dictionary.sorted { $0.key < $1.key }
|
||||||
} else if let array = resolved as? [Any] {
|
} else if let array = resolved as? [Any] {
|
||||||
@@ -120,36 +171,6 @@ class ForNode : NodeType {
|
|||||||
values = []
|
values = []
|
||||||
}
|
}
|
||||||
|
|
||||||
if let `where` = self.where {
|
return values
|
||||||
values = try values.filter({ item -> Bool in
|
|
||||||
return try push(value: item, context: context) {
|
|
||||||
try `where`.evaluate(context: context)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !values.isEmpty {
|
|
||||||
let count = values.count
|
|
||||||
|
|
||||||
return try values.enumerated().map { index, item in
|
|
||||||
let forContext: [String: Any] = [
|
|
||||||
"first": index == 0,
|
|
||||||
"last": index == (count - 1),
|
|
||||||
"counter": index + 1,
|
|
||||||
"counter0": index,
|
|
||||||
"length": count
|
|
||||||
]
|
|
||||||
|
|
||||||
return try context.push(dictionary: ["forloop": forContext]) {
|
|
||||||
return try push(value: item, context: context) {
|
|
||||||
try renderNodes(nodes, context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.joined(separator: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
return try context.push {
|
|
||||||
try renderNodes(emptyNodes, context)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ enum Operator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let operators: [Operator] = [
|
let operators: [Operator] = [
|
||||||
.infix("in", 5, InExpression.self),
|
.infix("in", 5, InExpression.self),
|
||||||
.infix("or", 6, OrExpression.self),
|
.infix("or", 6, OrExpression.self),
|
||||||
@@ -23,21 +22,17 @@ let operators: [Operator] = [
|
|||||||
.infix(">", 10, MoreThanExpression.self),
|
.infix(">", 10, MoreThanExpression.self),
|
||||||
.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 op in operators {
|
for `operator` in operators where `operator`.name == name {
|
||||||
if op.name == name {
|
return `operator`
|
||||||
return op
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
indirect enum IfToken {
|
indirect enum IfToken {
|
||||||
case infix(name: String, bindingPower: Int, operatorType: InfixOperator.Type)
|
case infix(name: String, bindingPower: Int, operatorType: InfixOperator.Type)
|
||||||
case prefix(name: String, bindingPower: Int, operatorType: PrefixOperator.Type)
|
case prefix(name: String, bindingPower: Int, operatorType: PrefixOperator.Type)
|
||||||
@@ -51,9 +46,9 @@ indirect enum IfToken {
|
|||||||
return bindingPower
|
return bindingPower
|
||||||
case .prefix(_, let bindingPower, _):
|
case .prefix(_, let bindingPower, _):
|
||||||
return bindingPower
|
return bindingPower
|
||||||
case .variable(_):
|
case .variable:
|
||||||
return 0
|
return 0
|
||||||
case .subExpression(_):
|
case .subExpression:
|
||||||
return 0
|
return 0
|
||||||
case .end:
|
case .end:
|
||||||
return 0
|
return 0
|
||||||
@@ -64,9 +59,9 @@ indirect enum IfToken {
|
|||||||
switch self {
|
switch self {
|
||||||
case .infix(let name, _, _):
|
case .infix(let name, _, _):
|
||||||
throw TemplateSyntaxError("'if' expression error: infix operator '\(name)' doesn't have a left hand side")
|
throw TemplateSyntaxError("'if' expression error: infix operator '\(name)' doesn't have a left hand side")
|
||||||
case .prefix(_, let bindingPower, let op):
|
case .prefix(_, let bindingPower, let operatorType):
|
||||||
let expression = try parser.expression(bindingPower: bindingPower)
|
let expression = try parser.expression(bindingPower: bindingPower)
|
||||||
return op.init(expression: expression)
|
return operatorType.init(expression: expression)
|
||||||
case .variable(let variable):
|
case .variable(let variable):
|
||||||
return VariableExpression(variable: variable)
|
return VariableExpression(variable: variable)
|
||||||
case .subExpression(let expression):
|
case .subExpression(let expression):
|
||||||
@@ -78,14 +73,14 @@ indirect enum IfToken {
|
|||||||
|
|
||||||
func leftDenotation(left: Expression, parser: IfExpressionParser) throws -> Expression {
|
func leftDenotation(left: Expression, parser: IfExpressionParser) throws -> Expression {
|
||||||
switch self {
|
switch self {
|
||||||
case .infix(_, let bindingPower, let op):
|
case .infix(_, let bindingPower, let operatorType):
|
||||||
let right = try parser.expression(bindingPower: bindingPower)
|
let right = try parser.expression(bindingPower: bindingPower)
|
||||||
return op.init(lhs: left, rhs: right)
|
return operatorType.init(lhs: left, rhs: right)
|
||||||
case .prefix(let name, _, _):
|
case .prefix(let name, _, _):
|
||||||
throw TemplateSyntaxError("'if' expression error: prefix operator '\(name)' was called with a left hand side")
|
throw TemplateSyntaxError("'if' expression error: prefix operator '\(name)' was called with a left hand side")
|
||||||
case .variable(let variable):
|
case .variable(let variable):
|
||||||
throw TemplateSyntaxError("'if' expression error: variable '\(variable)' was called with a left hand side")
|
throw TemplateSyntaxError("'if' expression error: variable '\(variable)' was called with a left hand side")
|
||||||
case .subExpression(_):
|
case .subExpression:
|
||||||
throw TemplateSyntaxError("'if' expression error: sub expression was called with a left hand side")
|
throw TemplateSyntaxError("'if' expression error: sub expression was called with a left hand side")
|
||||||
case .end:
|
case .end:
|
||||||
throw TemplateSyntaxError("'if' expression error: end")
|
throw TemplateSyntaxError("'if' expression error: end")
|
||||||
@@ -102,7 +97,6 @@ indirect enum IfToken {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
final class IfExpressionParser {
|
final class IfExpressionParser {
|
||||||
let tokens: [IfToken]
|
let tokens: [IfToken]
|
||||||
var position: Int = 0
|
var position: Int = 0
|
||||||
@@ -118,7 +112,7 @@ final class IfExpressionParser {
|
|||||||
private init(components: ArraySlice<String>, environment: Environment, token: Token) throws {
|
private init(components: ArraySlice<String>, environment: Environment, token: Token) throws {
|
||||||
var parsedComponents = Set<Int>()
|
var parsedComponents = Set<Int>()
|
||||||
var bracketsBalance = 0
|
var bracketsBalance = 0
|
||||||
self.tokens = try zip(components.indices, components).compactMap { (index, component) in
|
self.tokens = try zip(components.indices, components).compactMap { index, component in
|
||||||
guard !parsedComponents.contains(index) else { return nil }
|
guard !parsedComponents.contains(index) else { return nil }
|
||||||
|
|
||||||
if component == "(" {
|
if component == "(" {
|
||||||
@@ -139,8 +133,8 @@ final class IfExpressionParser {
|
|||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
parsedComponents.insert(index)
|
parsedComponents.insert(index)
|
||||||
if let op = findOperator(name: component) {
|
if let `operator` = findOperator(name: component) {
|
||||||
switch op {
|
switch `operator` {
|
||||||
case .infix(let name, let bindingPower, let operatorType):
|
case .infix(let name, let bindingPower, let operatorType):
|
||||||
return .infix(name: name, bindingPower: bindingPower, operatorType: operatorType)
|
return .infix(name: name, bindingPower: bindingPower, operatorType: operatorType)
|
||||||
case .prefix(let name, let bindingPower, let operatorType):
|
case .prefix(let name, let bindingPower, let operatorType):
|
||||||
@@ -152,17 +146,20 @@ final class IfExpressionParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func subExpression(from components: ArraySlice<String>, environment: Environment, token: Token) throws -> (Expression, Int) {
|
private static func subExpression(
|
||||||
|
from components: ArraySlice<String>,
|
||||||
|
environment: Environment,
|
||||||
|
token: Token
|
||||||
|
) throws -> (Expression, Int) {
|
||||||
var bracketsBalance = 1
|
var bracketsBalance = 1
|
||||||
let subComponents = components
|
let subComponents = components.prefix {
|
||||||
.prefix(while: {
|
|
||||||
if $0 == "(" {
|
if $0 == "(" {
|
||||||
bracketsBalance += 1
|
bracketsBalance += 1
|
||||||
} else if $0 == ")" {
|
} else if $0 == ")" {
|
||||||
bracketsBalance -= 1
|
bracketsBalance -= 1
|
||||||
}
|
}
|
||||||
return bracketsBalance != 0
|
return bracketsBalance != 0
|
||||||
})
|
}
|
||||||
if bracketsBalance > 0 {
|
if bracketsBalance > 0 {
|
||||||
throw TemplateSyntaxError("'if' expression error: missing closing bracket")
|
throw TemplateSyntaxError("'if' expression error: missing closing bracket")
|
||||||
}
|
}
|
||||||
@@ -211,7 +208,6 @@ final class IfExpressionParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Represents an if condition and the associated nodes when the condition
|
/// Represents an if condition and the associated nodes when the condition
|
||||||
/// evaluates
|
/// evaluates
|
||||||
final class IfCondition {
|
final class IfCondition {
|
||||||
@@ -225,12 +221,11 @@ final class IfCondition {
|
|||||||
|
|
||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
return try context.push {
|
return try context.push {
|
||||||
return try renderNodes(nodes, context)
|
try renderNodes(nodes, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class IfNode: NodeType {
|
class IfNode: NodeType {
|
||||||
let conditions: [IfCondition]
|
let conditions: [IfCondition]
|
||||||
let token: Token?
|
let token: Token?
|
||||||
@@ -291,7 +286,7 @@ class IfNode : NodeType {
|
|||||||
|
|
||||||
return IfNode(conditions: [
|
return IfNode(conditions: [
|
||||||
IfCondition(expression: expression, nodes: trueNodes),
|
IfCondition(expression: expression, nodes: trueNodes),
|
||||||
IfCondition(expression: nil, nodes: falseNodes),
|
IfCondition(expression: nil, nodes: falseNodes)
|
||||||
], token: token)
|
], token: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import PathKit
|
import PathKit
|
||||||
|
|
||||||
|
|
||||||
class IncludeNode: NodeType {
|
class IncludeNode: NodeType {
|
||||||
let templateName: Variable
|
let templateName: Variable
|
||||||
let includeContext: String?
|
let includeContext: String?
|
||||||
@@ -34,9 +33,9 @@ class IncludeNode : NodeType {
|
|||||||
let template = try context.environment.loadTemplate(name: templateName)
|
let template = try context.environment.loadTemplate(name: templateName)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let subContext = includeContext.flatMap { context[$0] as? [String: Any] }
|
let subContext = includeContext.flatMap { context[$0] as? [String: Any] } ?? [:]
|
||||||
return try context.push(dictionary: subContext) {
|
return try context.push(dictionary: subContext) {
|
||||||
return try template.render(context)
|
try template.render(context)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if let error = error as? TemplateSyntaxError {
|
if let error = error as? TemplateSyntaxError {
|
||||||
@@ -47,4 +46,3 @@ class IncludeNode : NodeType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ class BlockContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extension Collection {
|
extension Collection {
|
||||||
func any(_ closure: (Iterator.Element) -> Bool) -> Iterator.Element? {
|
func any(_ closure: (Iterator.Element) -> Bool) -> Iterator.Element? {
|
||||||
for element in self {
|
for element in self {
|
||||||
@@ -46,7 +45,6 @@ extension Collection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ExtendsNode: NodeType {
|
class ExtendsNode: NodeType {
|
||||||
let templateName: Variable
|
let templateName: Variable
|
||||||
let blocks: [String: BlockNode]
|
let blocks: [String: BlockNode]
|
||||||
@@ -66,7 +64,7 @@ class ExtendsNode : NodeType {
|
|||||||
|
|
||||||
let blockNodes = parsedNodes.compactMap { $0 as? BlockNode }
|
let blockNodes = parsedNodes.compactMap { $0 as? BlockNode }
|
||||||
|
|
||||||
let nodes = blockNodes.reduce([String: BlockNode]()) { (accumulator, node) -> [String: BlockNode] in
|
let nodes = blockNodes.reduce([String: BlockNode]()) { accumulator, node -> [String: BlockNode] in
|
||||||
var dict = accumulator
|
var dict = accumulator
|
||||||
dict[node.name] = node
|
dict[node.name] = node
|
||||||
return dict
|
return dict
|
||||||
@@ -102,7 +100,7 @@ class ExtendsNode : NodeType {
|
|||||||
// pushes base template and renders it's content
|
// pushes base template and renders it's content
|
||||||
// block_context contains all blocks from child templates
|
// block_context contains all blocks from child templates
|
||||||
return try context.push(dictionary: [BlockContext.contextKey: blockContext]) {
|
return try context.push(dictionary: [BlockContext.contextKey: blockContext]) {
|
||||||
return try baseTemplate.render(context)
|
try baseTemplate.render(context)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// if error template is already set (see catch in BlockNode)
|
// if error template is already set (see catch in BlockNode)
|
||||||
@@ -117,7 +115,6 @@ class ExtendsNode : NodeType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class BlockNode: NodeType {
|
class BlockNode: NodeType {
|
||||||
let name: String
|
let name: String
|
||||||
let nodes: [NodeType]
|
let nodes: [NodeType]
|
||||||
@@ -148,7 +145,7 @@ class BlockNode : NodeType {
|
|||||||
// render extension node
|
// render extension node
|
||||||
do {
|
do {
|
||||||
return try context.push(dictionary: childContext) {
|
return try context.push(dictionary: childContext) {
|
||||||
return try child.render(context)
|
try child.render(context)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
throw error.withToken(child.token)
|
throw error.withToken(child.token)
|
||||||
@@ -163,8 +160,11 @@ class BlockNode : NodeType {
|
|||||||
var childContext: [String: Any] = [BlockContext.contextKey: blockContext]
|
var childContext: [String: Any] = [BlockContext.contextKey: blockContext]
|
||||||
|
|
||||||
if let blockSuperNode = child.nodes.first(where: {
|
if let blockSuperNode = child.nodes.first(where: {
|
||||||
if let token = $0.token, case .variable = token.kind, token.contents == "block.super" { return true }
|
if let token = $0.token, case .variable = token.kind, token.contents == "block.super" {
|
||||||
else { return false}
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
do {
|
do {
|
||||||
// render base node so that its content can be used as part of child node that extends it
|
// render base node so that its content can be used as part of child node that extends it
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ final class KeyPath {
|
|||||||
subscriptLevel = 0
|
subscriptLevel = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
for c in variable {
|
for character in variable {
|
||||||
switch c {
|
switch character {
|
||||||
case "." where subscriptLevel == 0:
|
case "." where subscriptLevel == 0:
|
||||||
try foundSeparator()
|
try foundSeparator()
|
||||||
case "[":
|
case "[":
|
||||||
@@ -33,7 +33,7 @@ final class KeyPath {
|
|||||||
case "]":
|
case "]":
|
||||||
try closeBracket()
|
try closeBracket()
|
||||||
default:
|
default:
|
||||||
try addCharacter(c)
|
try addCharacter(character)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try finish()
|
try finish()
|
||||||
@@ -90,12 +90,12 @@ final class KeyPath {
|
|||||||
subscriptLevel -= 1
|
subscriptLevel -= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addCharacter(_ c: Character) throws {
|
private func addCharacter(_ character: Character) throws {
|
||||||
guard partialComponents.isEmpty || subscriptLevel > 0 else {
|
guard partialComponents.isEmpty || subscriptLevel > 0 else {
|
||||||
throw TemplateSyntaxError("Unexpected character '\(c)' in variable '\(variable)'")
|
throw TemplateSyntaxError("Unexpected character '\(character)' in variable '\(variable)'")
|
||||||
}
|
}
|
||||||
|
|
||||||
current.append(c)
|
current.append(character)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func finish() throws {
|
private func finish() throws {
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ struct Lexer {
|
|||||||
self.templateString = templateString
|
self.templateString = templateString
|
||||||
|
|
||||||
self.lines = templateString.components(separatedBy: .newlines).enumerated().compactMap {
|
self.lines = templateString.components(separatedBy: .newlines).enumerated().compactMap {
|
||||||
guard !$0.element.isEmpty else { return nil }
|
guard !$0.element.isEmpty,
|
||||||
return (content: $0.element, number: UInt($0.offset + 1), templateString.range(of: $0.element)!)
|
let range = templateString.range(of: $0.element) else { return nil }
|
||||||
|
return (content: $0.element, number: UInt($0.offset + 1), range)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,8 +44,8 @@ struct Lexer {
|
|||||||
guard string.count > 4 else { return "" }
|
guard string.count > 4 else { return "" }
|
||||||
let trimmed = String(string.dropFirst(2).dropLast(2))
|
let trimmed = String(string.dropFirst(2).dropLast(2))
|
||||||
.components(separatedBy: "\n")
|
.components(separatedBy: "\n")
|
||||||
.filter({ !$0.isEmpty })
|
.filter { !$0.isEmpty }
|
||||||
.map({ $0.trim(character: " ") })
|
.map { $0.trim(character: " ") }
|
||||||
.joined(separator: " ")
|
.joined(separator: " ")
|
||||||
return trimmed
|
return trimmed
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import PathKit
|
import PathKit
|
||||||
|
|
||||||
|
|
||||||
public protocol Loader {
|
public protocol Loader {
|
||||||
func loadTemplate(name: String, environment: Environment) throws -> Template
|
func loadTemplate(name: String, environment: Environment) throws -> Template
|
||||||
func loadTemplate(names: [String], environment: Environment) throws -> Template
|
func loadTemplate(names: [String], environment: Environment) throws -> Template
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extension Loader {
|
extension Loader {
|
||||||
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 {
|
||||||
@@ -24,7 +22,6 @@ extension Loader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// A class for loading a template from disk
|
// A class for loading a template from disk
|
||||||
public class FileSystemLoader: Loader, CustomStringConvertible {
|
public class FileSystemLoader: Loader, CustomStringConvertible {
|
||||||
public let paths: [Path]
|
public let paths: [Path]
|
||||||
@@ -35,7 +32,7 @@ public class FileSystemLoader: Loader, CustomStringConvertible {
|
|||||||
|
|
||||||
public init(bundle: [Bundle]) {
|
public init(bundle: [Bundle]) {
|
||||||
self.paths = bundle.map {
|
self.paths = bundle.map {
|
||||||
return Path($0.bundlePath)
|
Path($0.bundlePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +71,6 @@ public class FileSystemLoader: Loader, CustomStringConvertible {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public class DictionaryLoader: Loader {
|
public class DictionaryLoader: Loader {
|
||||||
public let templates: [String: String]
|
public let templates: [String: String]
|
||||||
|
|
||||||
@@ -101,7 +97,6 @@ public class DictionaryLoader: Loader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extension Path {
|
extension Path {
|
||||||
func safeJoin(path: Path) throws -> Path {
|
func safeJoin(path: Path) throws -> Path {
|
||||||
let newPath = self + path
|
let newPath = self + path
|
||||||
@@ -114,7 +109,6 @@ extension Path {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class SuspiciousFileOperation: Error {
|
class SuspiciousFileOperation: Error {
|
||||||
let basePath: Path
|
let basePath: Path
|
||||||
let path: Path
|
let path: Path
|
||||||
|
|||||||
@@ -8,16 +8,17 @@ public protocol NodeType {
|
|||||||
var token: Token? { get }
|
var token: Token? { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// 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.map {
|
return try nodes
|
||||||
|
.map {
|
||||||
do {
|
do {
|
||||||
return try $0.render(context)
|
return try $0.render(context)
|
||||||
} catch {
|
} catch {
|
||||||
throw error.withToken($0.token)
|
throw error.withToken($0.token)
|
||||||
}
|
}
|
||||||
}.joined(separator: "")
|
}
|
||||||
|
.joined()
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SimpleNode: NodeType {
|
public class SimpleNode: NodeType {
|
||||||
@@ -34,7 +35,6 @@ public class SimpleNode : NodeType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public class TextNode: NodeType {
|
public class TextNode: NodeType {
|
||||||
public let text: String
|
public let text: String
|
||||||
public let token: Token?
|
public let token: Token?
|
||||||
@@ -49,12 +49,10 @@ public class TextNode : NodeType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public protocol Resolvable {
|
public protocol Resolvable {
|
||||||
func resolve(_ context: Context) throws -> Any?
|
func resolve(_ context: Context) throws -> Any?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public class VariableNode: NodeType {
|
public class VariableNode: NodeType {
|
||||||
public let variable: Resolvable
|
public let variable: Resolvable
|
||||||
public var token: Token?
|
public var token: Token?
|
||||||
@@ -121,7 +119,6 @@ public class VariableNode : NodeType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func stringify(_ result: Any?) -> String {
|
func stringify(_ result: Any?) -> String {
|
||||||
if let result = result as? String {
|
if let result = result as? String {
|
||||||
return result
|
return result
|
||||||
@@ -144,7 +141,6 @@ func unwrap(_ array: [Any?]) -> [Any] {
|
|||||||
} else {
|
} else {
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
}
|
} else { return item as Any }
|
||||||
else { return item as Any }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#if !os(Linux)
|
#if !os(Linux)
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
class NowNode: NodeType {
|
class NowNode: NodeType {
|
||||||
let format: Variable
|
let format: Variable
|
||||||
let token: Token?
|
let token: Token?
|
||||||
@@ -28,18 +27,18 @@ class NowNode : NodeType {
|
|||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
let date = Date()
|
let date = Date()
|
||||||
let format = try self.format.resolve(context)
|
let format = try self.format.resolve(context)
|
||||||
var formatter:DateFormatter?
|
|
||||||
|
|
||||||
|
var formatter: DateFormatter
|
||||||
if let format = format as? DateFormatter {
|
if let format = format as? DateFormatter {
|
||||||
formatter = format
|
formatter = format
|
||||||
} else if let format = format as? String {
|
} else if let format = format as? String {
|
||||||
formatter = DateFormatter()
|
formatter = DateFormatter()
|
||||||
formatter!.dateFormat = format
|
formatter.dateFormat = format
|
||||||
} else {
|
} else {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatter!.string(from: date)
|
return formatter.string(from: date)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) {
|
public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) {
|
||||||
return { parser, token in
|
return { parser, token in
|
||||||
if let name = token.components.first {
|
if let name = token.components.first {
|
||||||
for tag in tags {
|
for tag in tags where name == tag {
|
||||||
if name == tag {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// 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 {
|
||||||
public typealias TagParser = (TokenParser, Token) throws -> NodeType
|
public typealias TagParser = (TokenParser, Token) throws -> NodeType
|
||||||
@@ -30,11 +27,11 @@ public class TokenParser {
|
|||||||
return try parse(nil)
|
return try parse(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func parse(_ parse_until:((_ parser:TokenParser, _ token:Token) -> (Bool))?) throws -> [NodeType] {
|
public func parse(_ parseUntil: ((_ parser: TokenParser, _ token: Token) -> (Bool))?) throws -> [NodeType] {
|
||||||
var nodes = [NodeType]()
|
var nodes = [NodeType]()
|
||||||
|
|
||||||
while tokens.count > 0 {
|
while !tokens.isEmpty {
|
||||||
let token = nextToken()!
|
guard let token = nextToken() else { break }
|
||||||
|
|
||||||
switch token.kind {
|
switch token.kind {
|
||||||
case .text:
|
case .text:
|
||||||
@@ -42,7 +39,7 @@ public class TokenParser {
|
|||||||
case .variable:
|
case .variable:
|
||||||
try nodes.append(VariableNode.parse(self, token: token))
|
try nodes.append(VariableNode.parse(self, token: token))
|
||||||
case .block:
|
case .block:
|
||||||
if let parse_until = parse_until , parse_until(self, token) {
|
if let parseUntil = parseUntil, parseUntil(self, token) {
|
||||||
prependToken(token)
|
prependToken(token)
|
||||||
return nodes
|
return nodes
|
||||||
}
|
}
|
||||||
@@ -65,7 +62,7 @@ public class TokenParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func nextToken() -> Token? {
|
public func nextToken() -> Token? {
|
||||||
if tokens.count > 0 {
|
if !tokens.isEmpty {
|
||||||
return tokens.remove(at: 0)
|
return tokens.remove(at: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +91,6 @@ public class TokenParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension Environment {
|
extension Environment {
|
||||||
|
|
||||||
func findTag(name: String) throws -> Extension.TagParser {
|
func findTag(name: String) throws -> Extension.TagParser {
|
||||||
for ext in extensions {
|
for ext in extensions {
|
||||||
if let filter = ext.tags[name] {
|
if let filter = ext.tags[name] {
|
||||||
@@ -118,23 +114,23 @@ extension Environment {
|
|||||||
} 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: ", ")).
|
||||||
""")
|
""")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func suggestedFilters(for name: String) -> [String] {
|
private func suggestedFilters(for name: String) -> [String] {
|
||||||
let allFilters = extensions.flatMap({ $0.filters.keys })
|
let allFilters = extensions.flatMap { $0.filters.keys }
|
||||||
|
|
||||||
let filtersWithDistance = allFilters
|
let filtersWithDistance = allFilters
|
||||||
.map({ (filterName: $0, distance: $0.levenshteinDistance(name)) })
|
.map { (filterName: $0, distance: $0.levenshteinDistance(name)) }
|
||||||
// do not suggest filters which names are shorter than the distance
|
// do not suggest filters which names are shorter than the distance
|
||||||
.filter({ $0.filterName.count > $0.distance })
|
.filter { $0.filterName.count > $0.distance }
|
||||||
guard let minDistance = filtersWithDistance.min(by: { $0.distance < $1.distance })?.distance else {
|
guard let minDistance = filtersWithDistance.min(by: { $0.distance < $1.distance })?.distance else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
// suggest all filters with the same distance
|
// suggest all filters with the same distance
|
||||||
return filtersWithDistance.filter({ $0.distance == minDistance }).map({ $0.filterName })
|
return filtersWithDistance.filter { $0.distance == minDistance }.map { $0.filterName }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create filter expression from a string
|
/// Create filter expression from a string
|
||||||
@@ -153,8 +149,14 @@ extension Environment {
|
|||||||
// find offset of filter in the containing token so that only filter is highligted, not the whole token
|
// find offset of filter in the containing token so that only filter is highligted, not the whole token
|
||||||
if let filterTokenRange = containingToken.contents.range(of: filterToken) {
|
if let filterTokenRange = containingToken.contents.range(of: filterToken) {
|
||||||
var location = containingToken.sourceMap.location
|
var location = containingToken.sourceMap.location
|
||||||
location.lineOffset += containingToken.contents.distance(from: containingToken.contents.startIndex, to: filterTokenRange.lowerBound)
|
location.lineOffset += containingToken.contents.distance(
|
||||||
syntaxError.token = .variable(value: filterToken, at: SourceMap(filename: containingToken.sourceMap.filename, location: location))
|
from: containingToken.contents.startIndex,
|
||||||
|
to: filterTokenRange.lowerBound
|
||||||
|
)
|
||||||
|
syntaxError.token = .variable(
|
||||||
|
value: filterToken,
|
||||||
|
at: SourceMap(filename: containingToken.sourceMap.filename, location: location)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
syntaxError.token = containingToken
|
syntaxError.token = containingToken
|
||||||
}
|
}
|
||||||
@@ -183,9 +185,8 @@ extension Environment {
|
|||||||
|
|
||||||
// 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(_ i: Int) -> Character {
|
return self[self.index(self.startIndex, offsetBy: index)]
|
||||||
return self[self.index(self.startIndex, offsetBy: i)]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func levenshteinDistance(_ target: String) -> Int {
|
func levenshteinDistance(_ target: String) -> Int {
|
||||||
@@ -198,19 +199,19 @@ extension String {
|
|||||||
last = [Int](0...target.count)
|
last = [Int](0...target.count)
|
||||||
current = [Int](repeating: 0, count: target.count + 1)
|
current = [Int](repeating: 0, count: target.count + 1)
|
||||||
|
|
||||||
for i in 0..<self.count {
|
for selfIndex in 0..<self.count {
|
||||||
// calculate v1 (current row distances) from the previous row v0
|
// calculate v1 (current row distances) from the previous row v0
|
||||||
|
|
||||||
// first element of v1 is A[i+1][0]
|
// first element of v1 is A[i+1][0]
|
||||||
// edit distance is delete (i+1) chars from s to match empty t
|
// edit distance is delete (i+1) chars from s to match empty t
|
||||||
current[0] = i + 1
|
current[0] = selfIndex + 1
|
||||||
|
|
||||||
// use formula to fill in the rest of the row
|
// use formula to fill in the rest of the row
|
||||||
for j in 0..<target.count {
|
for targetIndex in 0..<target.count {
|
||||||
current[j+1] = Swift.min(
|
current[targetIndex + 1] = Swift.min(
|
||||||
last[j+1] + 1,
|
last[targetIndex + 1] + 1,
|
||||||
current[j] + 1,
|
current[targetIndex] + 1,
|
||||||
last[j] + (self[i] == target[j] ? 0 : 1)
|
last[targetIndex] + (self[selfIndex] == target[targetIndex] ? 0 : 1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,5 +221,4 @@ extension String {
|
|||||||
|
|
||||||
return current[target.count]
|
return current[target.count]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ let NSFileNoSuchFileError = 4
|
|||||||
/// A class representing a template
|
/// A class representing a template
|
||||||
open class Template: ExpressibleByStringLiteral {
|
open class Template: ExpressibleByStringLiteral {
|
||||||
let templateString: String
|
let templateString: String
|
||||||
internal(set) var environment: Environment
|
var environment: Environment
|
||||||
let tokens: [Token]
|
let tokens: [Token]
|
||||||
|
|
||||||
/// The name of the loaded Template if the Template was loaded from a Loader
|
/// The name of the loaded Template if the Template was loaded from a Loader
|
||||||
@@ -72,8 +72,9 @@ open class Template: ExpressibleByStringLiteral {
|
|||||||
return try renderNodes(nodes, context)
|
return try renderNodes(nodes, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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))
|
return try render(Context(dictionary: dictionary ?? [:], environment: environment))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
/// Split a string by a separator leaving quoted phrases together
|
/// Split a string by a separator leaving quoted phrases together
|
||||||
func smartSplit(separator: Character = " ") -> [String] {
|
func smartSplit(separator: Character = " ") -> [String] {
|
||||||
@@ -10,37 +9,18 @@ extension String {
|
|||||||
var singleQuoteCount = 0
|
var singleQuoteCount = 0
|
||||||
var doubleQuoteCount = 0
|
var doubleQuoteCount = 0
|
||||||
|
|
||||||
let specialCharacters = ",|:"
|
|
||||||
func appendWord(_ word: String) {
|
|
||||||
if components.count > 0 {
|
|
||||||
if let precedingChar = components.last?.last, specialCharacters.contains(precedingChar) {
|
|
||||||
components[components.count-1] += word
|
|
||||||
} else if specialCharacters.contains(word) {
|
|
||||||
components[components.count-1] += word
|
|
||||||
} else if word != "(" && word.first == "(" || word != ")" && word.first == ")" {
|
|
||||||
components.append(String(word.prefix(1)))
|
|
||||||
appendWord(String(word.dropFirst()))
|
|
||||||
} else if word != "(" && word.last == "(" || word != ")" && word.last == ")" {
|
|
||||||
appendWord(String(word.dropLast()))
|
|
||||||
components.append(String(word.suffix(1)))
|
|
||||||
} else {
|
|
||||||
components.append(word)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
components.append(word)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for character in self {
|
for character in self {
|
||||||
if character == "'" { singleQuoteCount += 1 }
|
if character == "'" {
|
||||||
else if character == "\"" { doubleQuoteCount += 1 }
|
singleQuoteCount += 1
|
||||||
|
} else if character == "\"" {
|
||||||
|
doubleQuoteCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
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 % 2 == 0 || doubleQuoteCount % 2 == 0) && !word.isEmpty {
|
||||||
appendWord(word)
|
appendWord(word, to: &components)
|
||||||
word = ""
|
word = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,11 +34,33 @@ extension String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !word.isEmpty {
|
if !word.isEmpty {
|
||||||
appendWord(word)
|
appendWord(word, to: &components)
|
||||||
}
|
}
|
||||||
|
|
||||||
return components
|
return components
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func appendWord(_ word: String, to components: inout [String]) {
|
||||||
|
let specialCharacters = ",|:"
|
||||||
|
|
||||||
|
if !components.isEmpty {
|
||||||
|
if let precedingChar = components.last?.last, specialCharacters.contains(precedingChar) {
|
||||||
|
components[components.count - 1] += word
|
||||||
|
} else if specialCharacters.contains(word) {
|
||||||
|
components[components.count - 1] += word
|
||||||
|
} else if word != "(" && word.first == "(" || word != ")" && word.first == ")" {
|
||||||
|
components.append(String(word.prefix(1)))
|
||||||
|
appendWord(String(word.dropFirst()), to: &components)
|
||||||
|
} else if word != "(" && word.last == "(" || word != ")" && word.last == ")" {
|
||||||
|
appendWord(String(word.dropLast()), to: &components)
|
||||||
|
components.append(String(word.suffix(1)))
|
||||||
|
} else {
|
||||||
|
components.append(word)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
components.append(word)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct SourceMap: Equatable {
|
public struct SourceMap: Equatable {
|
||||||
@@ -125,5 +127,4 @@ public class Token: Equatable {
|
|||||||
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
|
return lhs.contents == rhs.contents && lhs.kind == rhs.kind && lhs.sourceMap == rhs.sourceMap
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
typealias Number = Float
|
typealias Number = Float
|
||||||
|
|
||||||
|
|
||||||
class FilterExpression: Resolvable {
|
class FilterExpression: Resolvable {
|
||||||
let filters: [(FilterType, [Variable])]
|
let filters: [(FilterType, [Variable])]
|
||||||
let variable: Variable
|
let variable: Variable
|
||||||
|
|
||||||
init(token: String, environment: Environment) throws {
|
init(token: String, environment: Environment) throws {
|
||||||
let bits = token.smartSplit(separator: "|").map({ String($0).trim(character: " ") })
|
let bits = token.smartSplit(separator: "|").map { String($0).trim(character: " ") }
|
||||||
if bits.isEmpty {
|
if bits.isEmpty {
|
||||||
throw TemplateSyntaxError("Variable tags must include at least 1 argument")
|
throw TemplateSyntaxError("Variable tags must include at least 1 argument")
|
||||||
}
|
}
|
||||||
@@ -32,9 +30,9 @@ class FilterExpression : Resolvable {
|
|||||||
func resolve(_ context: Context) throws -> Any? {
|
func resolve(_ context: Context) throws -> Any? {
|
||||||
let result = try variable.resolve(context)
|
let result = try variable.resolve(context)
|
||||||
|
|
||||||
return try filters.reduce(result) { x, y in
|
return try filters.reduce(result) { value, filter in
|
||||||
let arguments = try y.1.map { try $0.resolve(context) }
|
let arguments = try filter.1.map { try $0.resolve(context) }
|
||||||
return try y.0.invoke(value: x, arguments: arguments, context: context)
|
return try filter.0.invoke(value: value, arguments: arguments, context: context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,16 +46,8 @@ public struct Variable : Equatable, Resolvable {
|
|||||||
self.variable = variable
|
self.variable = variable
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split the lookup string and resolve references if possible
|
|
||||||
fileprivate func lookup(_ context: Context) throws -> [String] {
|
|
||||||
let keyPath = KeyPath(variable, in: context)
|
|
||||||
return try keyPath.parse()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve the variable in the given context
|
/// Resolve the variable in the given context
|
||||||
public func resolve(_ context: Context) throws -> Any? {
|
public func resolve(_ context: Context) throws -> Any? {
|
||||||
var current: Any? = context
|
|
||||||
|
|
||||||
if (variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\"")) {
|
if (variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\"")) {
|
||||||
// String literal
|
// String literal
|
||||||
return String(variable[variable.index(after: variable.startIndex) ..< variable.index(before: variable.endIndex)])
|
return String(variable[variable.index(after: variable.startIndex) ..< variable.index(before: variable.endIndex)])
|
||||||
@@ -75,37 +65,13 @@ public struct Variable : Equatable, Resolvable {
|
|||||||
return bool
|
return bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var current: Any? = context
|
||||||
for bit in try lookup(context) {
|
for bit in try lookup(context) {
|
||||||
current = normalize(current)
|
current = resolve(bit: bit, context: current)
|
||||||
|
|
||||||
if let context = current as? Context {
|
|
||||||
current = context[bit]
|
|
||||||
} else if let dictionary = current as? [String: Any] {
|
|
||||||
if bit == "count" {
|
|
||||||
current = dictionary.count
|
|
||||||
} else {
|
|
||||||
current = dictionary[bit]
|
|
||||||
}
|
|
||||||
} else if let array = current as? [Any] {
|
|
||||||
current = resolveCollection(array, bit: bit)
|
|
||||||
} else if let string = current as? String {
|
|
||||||
current = resolveCollection(string, bit: bit)
|
|
||||||
} else if let object = current as? NSObject { // NSKeyValueCoding
|
|
||||||
#if os(Linux)
|
|
||||||
return nil
|
|
||||||
#else
|
|
||||||
if object.responds(to: Selector(bit)) {
|
|
||||||
current = object.value(forKey: bit)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
} else if let value = current {
|
|
||||||
current = Mirror(reflecting: value).getValue(for: bit)
|
|
||||||
if current == nil {
|
if current == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let resolvable = current as? Resolvable {
|
if let resolvable = current as? Resolvable {
|
||||||
@@ -116,9 +82,51 @@ public struct Variable : Equatable, Resolvable {
|
|||||||
|
|
||||||
return normalize(current)
|
return normalize(current)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Split the lookup string and resolve references if possible
|
||||||
|
private func lookup(_ context: Context) throws -> [String] {
|
||||||
|
let keyPath = KeyPath(variable, in: context)
|
||||||
|
return try keyPath.parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func resolveCollection<T: Collection>(_ collection: T, bit: String) -> Any? {
|
// Try to resolve a partial keypath for the given context
|
||||||
|
private func resolve(bit: String, context: Any?) -> Any? {
|
||||||
|
let context = normalize(context)
|
||||||
|
|
||||||
|
if let context = context as? Context {
|
||||||
|
return context[bit]
|
||||||
|
} else if let dictionary = context as? [String: Any] {
|
||||||
|
return resolve(bit: bit, dictionary: dictionary)
|
||||||
|
} else if let array = context as? [Any] {
|
||||||
|
return resolve(bit: bit, collection: array)
|
||||||
|
} else if let string = context as? String {
|
||||||
|
return resolve(bit: bit, collection: string)
|
||||||
|
} else if let object = context as? NSObject { // NSKeyValueCoding
|
||||||
|
#if os(Linux)
|
||||||
|
return nil
|
||||||
|
#else
|
||||||
|
if object.responds(to: Selector(bit)) {
|
||||||
|
return object.value(forKey: bit)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
} else if let value = context {
|
||||||
|
return Mirror(reflecting: value).getValue(for: bit)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to resolve a partial keypath for the given dictionary
|
||||||
|
private func resolve(bit: String, dictionary: [String: Any]) -> Any? {
|
||||||
|
if bit == "count" {
|
||||||
|
return dictionary.count
|
||||||
|
} else {
|
||||||
|
return dictionary[bit]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to resolve a partial keypath for the given collection
|
||||||
|
private func resolve<T: Collection>(bit: String, collection: T) -> Any? {
|
||||||
if let index = Int(bit) {
|
if let index = Int(bit) {
|
||||||
if index >= 0 && index < collection.count {
|
if index >= 0 && index < collection.count {
|
||||||
return collection[collection.index(collection.startIndex, offsetBy: index)]
|
return collection[collection.index(collection.startIndex, offsetBy: index)]
|
||||||
@@ -135,6 +143,7 @@ private func resolveCollection<T: Collection>(_ collection: T, bit: String) -> A
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A structure used to represet range of two integer values expressed as `from...to`.
|
/// A structure used to represet range of two integer values expressed as `from...to`.
|
||||||
/// Values should be numbers (they will be converted to integers).
|
/// Values should be numbers (they will be converted to integers).
|
||||||
@@ -142,6 +151,7 @@ private func resolveCollection<T: Collection>(_ collection: T, bit: String) -> A
|
|||||||
/// If `from` is more than `to` array will contain values of reversed range.
|
/// If `from` is more than `to` array will contain values of reversed range.
|
||||||
public struct RangeVariable: Resolvable {
|
public struct RangeVariable: Resolvable {
|
||||||
public let from: Resolvable
|
public let from: Resolvable
|
||||||
|
// swiftlint:disable:next identifier_name
|
||||||
public let to: Resolvable
|
public let to: Resolvable
|
||||||
|
|
||||||
public init?(_ token: String, environment: Environment) throws {
|
public init?(_ token: String, environment: Environment) throws {
|
||||||
@@ -165,24 +175,23 @@ public struct RangeVariable: Resolvable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func resolve(_ context: Context) throws -> Any? {
|
public func resolve(_ context: Context) throws -> Any? {
|
||||||
let fromResolved = try from.resolve(context)
|
let lowerResolved = try from.resolve(context)
|
||||||
let toResolved = try to.resolve(context)
|
let upperResolved = try to.resolve(context)
|
||||||
|
|
||||||
guard let from = fromResolved.flatMap(toNumber(value:)).flatMap(Int.init) else {
|
guard let lower = lowerResolved.flatMap(toNumber(value:)).flatMap(Int.init) else {
|
||||||
throw TemplateSyntaxError("'from' value is not an Integer (\(fromResolved ?? "nil"))")
|
throw TemplateSyntaxError("'from' value is not an Integer (\(lowerResolved ?? "nil"))")
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let to = toResolved.flatMap(toNumber(value:)).flatMap(Int.init) else {
|
guard let upper = upperResolved.flatMap(toNumber(value:)).flatMap(Int.init) else {
|
||||||
throw TemplateSyntaxError("'to' value is not an Integer (\(toResolved ?? "nil") )")
|
throw TemplateSyntaxError("'to' value is not an Integer (\(upperResolved ?? "nil") )")
|
||||||
}
|
}
|
||||||
|
|
||||||
let range = min(from, to)...max(from, to)
|
let range = min(lower, upper)...max(lower, upper)
|
||||||
return from > to ? Array(range.reversed()) : Array(range)
|
return lower > upper ? Array(range.reversed()) : Array(range)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func normalize(_ current: Any?) -> Any? {
|
func normalize(_ current: Any?) -> Any? {
|
||||||
if let current = current as? Normalizable {
|
if let current = current as? Normalizable {
|
||||||
return current.normalize()
|
return current.normalize()
|
||||||
@@ -235,7 +244,7 @@ func parseFilterComponents(token: String) -> (String, [Variable]) {
|
|||||||
|
|
||||||
extension Mirror {
|
extension Mirror {
|
||||||
func getValue(for key: String) -> Any? {
|
func getValue(for key: String) -> Any? {
|
||||||
let result = descendant(key) ?? Int(key).flatMap({ descendant($0) })
|
let result = descendant(key) ?? Int(key).flatMap { descendant($0) }
|
||||||
if result == nil {
|
if result == nil {
|
||||||
// go through inheritance chain to reach superclass properties
|
// go through inheritance chain to reach superclass properties
|
||||||
return superclassMirror?.getValue(for: key)
|
return superclassMirror?.getValue(for: key)
|
||||||
@@ -267,5 +276,3 @@ extension Optional: AnyOptional {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import Foundation
|
|||||||
|
|
||||||
#if !swift(>=4.1)
|
#if !swift(>=4.1)
|
||||||
public extension Collection {
|
public extension Collection {
|
||||||
func index(_ i: Self.Index, offsetBy n: Int) -> Self.Index {
|
func index(_ index: Self.Index, offsetBy offset: Int) -> Self.Index {
|
||||||
let indexDistance = Self.IndexDistance(n)
|
let indexDistance = Self.IndexDistance(offset)
|
||||||
return index(i, offsetBy: indexDistance)
|
return self.index(index, offsetBy: indexDistance)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
3
Tests/StencilTests/.swiftlint.yml
Normal file
3
Tests/StencilTests/.swiftlint.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
disabled_rules: # rule identifiers to exclude from running
|
||||||
|
- type_body_length
|
||||||
|
- file_length
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
import XCTest
|
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class ContextTests: XCTestCase {
|
||||||
class ContextTests: XCTestCase {
|
func testContextSubscripting() {
|
||||||
|
describe("Context Subscripting") {
|
||||||
func testContext() {
|
var context = Context()
|
||||||
describe("Context") {
|
|
||||||
var context: Context!
|
|
||||||
|
|
||||||
$0.before {
|
$0.before {
|
||||||
context = Context(dictionary: ["name": "Kyle"])
|
context = Context(dictionary: ["name": "Kyle"])
|
||||||
}
|
}
|
||||||
@@ -41,6 +38,15 @@ class ContextTests: XCTestCase {
|
|||||||
try expect(context["name"] as? String) == "Katie"
|
try expect(context["name"] as? String) == "Katie"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testContextRestoration() {
|
||||||
|
describe("Context Restoration") {
|
||||||
|
var context = Context()
|
||||||
|
$0.before {
|
||||||
|
context = Context(dictionary: ["name": "Kyle"])
|
||||||
|
}
|
||||||
|
|
||||||
$0.it("allows you to pop to restore previous state") {
|
$0.it("allows you to pop to restore previous state") {
|
||||||
context.push {
|
context.push {
|
||||||
|
|||||||
@@ -1,224 +1,223 @@
|
|||||||
import XCTest
|
|
||||||
import Spectre
|
|
||||||
import PathKit
|
import PathKit
|
||||||
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
|
import XCTest
|
||||||
|
|
||||||
class EnvironmentTests: XCTestCase {
|
final class EnvironmentTests: XCTestCase {
|
||||||
func testEnvironment() {
|
var environment = Environment(loader: ExampleLoader())
|
||||||
describe("Environment") {
|
var template: Template = ""
|
||||||
var environment: Environment!
|
|
||||||
var template: Template!
|
override func setUp() {
|
||||||
|
super.setUp()
|
||||||
|
|
||||||
|
let errorExtension = Extension()
|
||||||
|
errorExtension.registerFilter("throw") { (_: Any?) in
|
||||||
|
throw TemplateSyntaxError("filter error")
|
||||||
|
}
|
||||||
|
errorExtension.registerSimpleTag("simpletag") { _ in
|
||||||
|
throw TemplateSyntaxError("simpletag error")
|
||||||
|
}
|
||||||
|
errorExtension.registerTag("customtag") { _, token in
|
||||||
|
ErrorNode(token: token)
|
||||||
|
}
|
||||||
|
|
||||||
$0.before {
|
|
||||||
environment = Environment(loader: ExampleLoader())
|
environment = Environment(loader: ExampleLoader())
|
||||||
template = nil
|
environment.extensions += [errorExtension]
|
||||||
|
template = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can load a template from a name") {
|
func testLoading() {
|
||||||
let template = try environment.loadTemplate(name: "example.html")
|
it("can load a template from a name") {
|
||||||
|
let template = try self.environment.loadTemplate(name: "example.html")
|
||||||
try expect(template.name) == "example.html"
|
try expect(template.name) == "example.html"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can load a template from a names") {
|
it("can load a template from a names") {
|
||||||
let template = try environment.loadTemplate(names: ["first.html", "example.html"])
|
let template = try self.environment.loadTemplate(names: ["first.html", "example.html"])
|
||||||
try expect(template.name) == "example.html"
|
try expect(template.name) == "example.html"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$0.it("can render a template from a string") {
|
func testRendering() {
|
||||||
let result = try environment.renderTemplate(string: "Hello World")
|
it("can render a template from a string") {
|
||||||
|
let result = try self.environment.renderTemplate(string: "Hello World")
|
||||||
try expect(result) == "Hello World"
|
try expect(result) == "Hello World"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can render a template from a file") {
|
it("can render a template from a file") {
|
||||||
let result = try environment.renderTemplate(name: "example.html")
|
let result = try self.environment.renderTemplate(name: "example.html")
|
||||||
try expect(result) == "Hello World!"
|
try expect(result) == "Hello World!"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("allows you to provide a custom template class") {
|
it("allows you to provide a custom template class") {
|
||||||
let environment = Environment(loader: ExampleLoader(), templateClass: CustomTemplate.self)
|
let environment = Environment(loader: ExampleLoader(), templateClass: CustomTemplate.self)
|
||||||
let result = try environment.renderTemplate(string: "Hello World")
|
let result = try environment.renderTemplate(string: "Hello World")
|
||||||
|
|
||||||
try expect(result) == "here"
|
try expect(result) == "here"
|
||||||
}
|
}
|
||||||
|
|
||||||
func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError {
|
|
||||||
guard let range = template.templateString.range(of: token) else {
|
|
||||||
fatalError("Can't find '\(token)' in '\(template)'")
|
|
||||||
}
|
|
||||||
let lexer = Lexer(templateString: template.templateString)
|
|
||||||
let location = lexer.rangeLocation(range)
|
|
||||||
let sourceMap = SourceMap(filename: template.name, location: location)
|
|
||||||
let token = Token.block(value: token, at: sourceMap)
|
|
||||||
return TemplateSyntaxError(reason: description, token: token, stackTrace: [])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func expectError(reason: String, token: String,
|
func testSyntaxError() {
|
||||||
file: String = #file, line: Int = #line, function: String = #function) throws {
|
it("reports syntax error on invalid for tag syntax") {
|
||||||
|
self.template = "Hello {% for name in %}{{ name }}, {% endfor %}!"
|
||||||
|
try self.expectError(
|
||||||
|
reason: "'for' statements should use the syntax: `for <x> in <y> [where <condition>]`.",
|
||||||
|
token: "for name in"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
it("reports syntax error on missing endfor") {
|
||||||
|
self.template = "{% for name in names %}{{ name }}"
|
||||||
|
try self.expectError(reason: "`endfor` was not found.", token: "for name in names")
|
||||||
|
}
|
||||||
|
|
||||||
|
it("reports syntax error on unknown tag") {
|
||||||
|
self.template = "{% for name in names %}{{ name }}{% end %}"
|
||||||
|
try self.expectError(reason: "Unknown template tag 'end'", token: "end")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUnknownFilter() {
|
||||||
|
it("reports syntax error in for tag") {
|
||||||
|
self.template = "{% for name in names|unknown %}{{ name }}{% endfor %}"
|
||||||
|
try self.expectError(
|
||||||
|
reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||||
|
token: "names|unknown"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
it("reports syntax error in for-where tag") {
|
||||||
|
self.template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}"
|
||||||
|
try self.expectError(
|
||||||
|
reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||||
|
token: "name|unknown"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
it("reports syntax error in if tag") {
|
||||||
|
self.template = "{% if name|unknown %}{{ name }}{% endif %}"
|
||||||
|
try self.expectError(
|
||||||
|
reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||||
|
token: "name|unknown"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
it("reports syntax error in elif tag") {
|
||||||
|
self.template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}"
|
||||||
|
try self.expectError(
|
||||||
|
reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||||
|
token: "name|unknown"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
it("reports syntax error in ifnot tag") {
|
||||||
|
self.template = "{% ifnot name|unknown %}{{ name }}{% endif %}"
|
||||||
|
try self.expectError(
|
||||||
|
reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||||
|
token: "name|unknown"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
it("reports syntax error in filter tag") {
|
||||||
|
self.template = "{% filter unknown %}Text{% endfilter %}"
|
||||||
|
try self.expectError(
|
||||||
|
reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||||
|
token: "filter unknown"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
it("reports syntax error in variable tag") {
|
||||||
|
self.template = "{{ name|unknown }}"
|
||||||
|
try self.expectError(
|
||||||
|
reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||||
|
token: "name|unknown"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRenderingError() {
|
||||||
|
it("reports rendering error in variable filter") {
|
||||||
|
self.template = Template(templateString: "{{ name|throw }}", environment: self.environment)
|
||||||
|
try self.expectError(reason: "filter error", token: "name|throw")
|
||||||
|
}
|
||||||
|
|
||||||
|
it("reports rendering error in filter tag") {
|
||||||
|
self.template = Template(templateString: "{% filter throw %}Test{% endfilter %}", environment: self.environment)
|
||||||
|
try self.expectError(reason: "filter error", token: "filter throw")
|
||||||
|
}
|
||||||
|
|
||||||
|
it("reports rendering error in simple tag") {
|
||||||
|
self.template = Template(templateString: "{% simpletag %}", environment: self.environment)
|
||||||
|
try self.expectError(reason: "simpletag error", token: "simpletag")
|
||||||
|
}
|
||||||
|
|
||||||
|
it("reports passing argument to simple filter") {
|
||||||
|
self.template = "{{ name|uppercase:5 }}"
|
||||||
|
try self.expectError(reason: "cannot invoke filter with an argument", token: "name|uppercase:5")
|
||||||
|
}
|
||||||
|
|
||||||
|
it("reports rendering error in custom tag") {
|
||||||
|
self.template = Template(templateString: "{% customtag %}", environment: self.environment)
|
||||||
|
try self.expectError(reason: "Custom Error", token: "customtag")
|
||||||
|
}
|
||||||
|
|
||||||
|
it("reports rendering error in for body") {
|
||||||
|
self.template = Template(templateString: """
|
||||||
|
{% for name in names %}{% customtag %}{% endfor %}
|
||||||
|
""", environment: self.environment)
|
||||||
|
try self.expectError(reason: "Custom Error", token: "customtag")
|
||||||
|
}
|
||||||
|
|
||||||
|
it("reports rendering error in block") {
|
||||||
|
self.template = Template(
|
||||||
|
templateString: "{% block some %}{% customtag %}{% endblock %}",
|
||||||
|
environment: self.environment
|
||||||
|
)
|
||||||
|
try self.expectError(reason: "Custom Error", token: "customtag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func expectError(
|
||||||
|
reason: String,
|
||||||
|
token: String,
|
||||||
|
file: String = #file,
|
||||||
|
line: Int = #line,
|
||||||
|
function: String = #function
|
||||||
|
) throws {
|
||||||
let expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
let expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
||||||
|
|
||||||
let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"]),
|
let error = try expect(
|
||||||
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
self.environment.render(template: self.template, context: ["names": ["Bob", "Alice"], "name": "Bob"]),
|
||||||
|
file: file,
|
||||||
|
line: line,
|
||||||
|
function: function
|
||||||
|
).toThrow() as TemplateSyntaxError
|
||||||
let reporter = SimpleErrorReporter()
|
let reporter = SimpleErrorReporter()
|
||||||
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
try expect(
|
||||||
}
|
reporter.renderError(error),
|
||||||
|
file: file,
|
||||||
$0.context("given syntax error") {
|
line: line,
|
||||||
|
function: function
|
||||||
$0.it("reports syntax error on invalid for tag syntax") {
|
) == reporter.renderError(expectedError)
|
||||||
template = "Hello {% for name in %}{{ name }}, {% endfor %}!"
|
|
||||||
try expectError(reason: "'for' statements should use the syntax: `for <x> in <y> [where <condition>]`.", token: "for name in")
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("reports syntax error on missing endfor") {
|
|
||||||
template = "{% for name in names %}{{ name }}"
|
|
||||||
try expectError(reason: "`endfor` was not found.", token: "for name in names")
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("reports syntax error on unknown tag") {
|
|
||||||
template = "{% for name in names %}{{ name }}{% end %}"
|
|
||||||
try expectError(reason: "Unknown template tag 'end'", token: "end")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.context("given unknown filter") {
|
|
||||||
|
|
||||||
$0.it("reports syntax error in for tag") {
|
|
||||||
template = "{% for name in names|unknown %}{{ name }}{% endfor %}"
|
|
||||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "names|unknown")
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("reports syntax error in for-where tag") {
|
|
||||||
template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}"
|
|
||||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("reports syntax error in if tag") {
|
|
||||||
template = "{% if name|unknown %}{{ name }}{% endif %}"
|
|
||||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("reports syntax error in elif tag") {
|
|
||||||
template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}"
|
|
||||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("reports syntax error in ifnot tag") {
|
|
||||||
template = "{% ifnot name|unknown %}{{ name }}{% endif %}"
|
|
||||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("reports syntax error in filter tag") {
|
|
||||||
template = "{% filter unknown %}Text{% endfilter %}"
|
|
||||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "filter unknown")
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("reports syntax error in variable tag") {
|
|
||||||
template = "{{ name|unknown }}"
|
|
||||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.context("given rendering error") {
|
|
||||||
|
|
||||||
$0.it("reports rendering error in variable filter") {
|
|
||||||
let filterExtension = Extension()
|
|
||||||
filterExtension.registerFilter("throw") { (value: Any?) in
|
|
||||||
throw TemplateSyntaxError("filter error")
|
|
||||||
}
|
|
||||||
environment.extensions += [filterExtension]
|
|
||||||
|
|
||||||
template = Template(templateString: "{{ name|throw }}", environment: environment)
|
|
||||||
try expectError(reason: "filter error", token: "name|throw")
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("reports rendering error in filter tag") {
|
|
||||||
let filterExtension = Extension()
|
|
||||||
filterExtension.registerFilter("throw") { (value: Any?) in
|
|
||||||
throw TemplateSyntaxError("filter error")
|
|
||||||
}
|
|
||||||
environment.extensions += [filterExtension]
|
|
||||||
|
|
||||||
template = Template(templateString: "{% filter throw %}Test{% endfilter %}", environment: environment)
|
|
||||||
try expectError(reason: "filter error", token: "filter throw")
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("reports rendering error in simple tag") {
|
|
||||||
let tagExtension = Extension()
|
|
||||||
tagExtension.registerSimpleTag("simpletag") { context in
|
|
||||||
throw TemplateSyntaxError("simpletag error")
|
|
||||||
}
|
|
||||||
environment.extensions += [tagExtension]
|
|
||||||
|
|
||||||
template = Template(templateString: "{% simpletag %}", environment: environment)
|
|
||||||
try expectError(reason: "simpletag error", token: "simpletag")
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("reporsts passing argument to simple filter") {
|
|
||||||
template = "{{ name|uppercase:5 }}"
|
|
||||||
try expectError(reason: "cannot invoke filter with an argument", token: "name|uppercase:5")
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("reports rendering error in custom tag") {
|
|
||||||
let tagExtension = Extension()
|
|
||||||
tagExtension.registerTag("customtag") { parser, token in
|
|
||||||
return ErrorNode(token: token)
|
|
||||||
}
|
|
||||||
environment.extensions += [tagExtension]
|
|
||||||
|
|
||||||
template = Template(templateString: "{% customtag %}", environment: environment)
|
|
||||||
try expectError(reason: "Custom Error", token: "customtag")
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("reports rendering error in for body") {
|
|
||||||
let tagExtension = Extension()
|
|
||||||
tagExtension.registerTag("customtag") { parser, token in
|
|
||||||
return ErrorNode(token: token)
|
|
||||||
}
|
|
||||||
environment.extensions += [tagExtension]
|
|
||||||
|
|
||||||
template = Template(templateString: "{% for name in names %}{% customtag %}{% endfor %}", environment: environment)
|
|
||||||
try expectError(reason: "Custom Error", token: "customtag")
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("reports rendering error in block") {
|
|
||||||
let tagExtension = Extension()
|
|
||||||
tagExtension.registerTag("customtag") { parser, token in
|
|
||||||
return ErrorNode(token: token)
|
|
||||||
}
|
|
||||||
environment.extensions += [tagExtension]
|
|
||||||
|
|
||||||
template = Template(templateString: "{% block some %}{% customtag %}{% endblock %}", environment: environment)
|
|
||||||
try expectError(reason: "Custom Error", token: "customtag")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.context("given included template") {
|
final class EnvironmentIncludeTemplateTests: XCTestCase {
|
||||||
|
var environment = Environment(loader: ExampleLoader())
|
||||||
|
var template: Template = ""
|
||||||
|
var includedTemplate: Template = ""
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
super.setUp()
|
||||||
|
|
||||||
let path = Path(#file) + ".." + "fixtures"
|
let path = Path(#file) + ".." + "fixtures"
|
||||||
let loader = FileSystemLoader(paths: [path])
|
let loader = FileSystemLoader(paths: [path])
|
||||||
var environment = Environment(loader: loader)
|
|
||||||
var template: Template!
|
|
||||||
var includedTemplate: Template!
|
|
||||||
|
|
||||||
$0.before {
|
|
||||||
environment = Environment(loader: loader)
|
environment = Environment(loader: loader)
|
||||||
template = nil
|
template = ""
|
||||||
includedTemplate = nil
|
includedTemplate = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func expectError(reason: String, token: String, includedToken: String,
|
func testSyntaxError() throws {
|
||||||
file: String = #file, line: Int = #line, function: String = #function) throws {
|
|
||||||
var expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
|
||||||
expectedError.stackTrace = [expectedSyntaxError(token: includedToken, template: includedTemplate, description: reason).token!]
|
|
||||||
|
|
||||||
let error = try expect(environment.render(template: template, context: ["target": "World"]),
|
|
||||||
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
|
||||||
let reporter = SimpleErrorReporter()
|
|
||||||
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("reports syntax error in included template") {
|
|
||||||
template = Template(templateString: """
|
template = Template(templateString: """
|
||||||
{% include "invalid-include.html" %}
|
{% include "invalid-include.html" %}
|
||||||
""", environment: environment)
|
""", environment: environment)
|
||||||
@@ -231,11 +230,11 @@ class EnvironmentTests: XCTestCase {
|
|||||||
includedToken: "target|unknown")
|
includedToken: "target|unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("reports runtime error in included template") {
|
func testRuntimeError() throws {
|
||||||
let filterExtension = Extension()
|
let filterExtension = Extension()
|
||||||
filterExtension.registerFilter("unknown", filter: { (_: Any?) in
|
filterExtension.registerFilter("unknown") { (_: Any?) in
|
||||||
throw TemplateSyntaxError("filter error")
|
throw TemplateSyntaxError("filter error")
|
||||||
})
|
}
|
||||||
environment.extensions += [filterExtension]
|
environment.extensions += [filterExtension]
|
||||||
|
|
||||||
template = Template(templateString: """
|
template = Template(templateString: """
|
||||||
@@ -248,34 +247,53 @@ class EnvironmentTests: XCTestCase {
|
|||||||
includedToken: "target|unknown")
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.context("given base and child templates") {
|
final class EnvironmentBaseAndChildTemplateTests: XCTestCase {
|
||||||
|
var environment = Environment(loader: ExampleLoader())
|
||||||
|
var childTemplate: Template = ""
|
||||||
|
var baseTemplate: Template = ""
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
super.setUp()
|
||||||
|
|
||||||
let path = Path(#file) + ".." + "fixtures"
|
let path = Path(#file) + ".." + "fixtures"
|
||||||
let loader = FileSystemLoader(paths: [path])
|
let loader = FileSystemLoader(paths: [path])
|
||||||
var environment: Environment!
|
|
||||||
var childTemplate: Template!
|
|
||||||
var baseTemplate: Template!
|
|
||||||
|
|
||||||
$0.before {
|
|
||||||
environment = Environment(loader: loader)
|
environment = Environment(loader: loader)
|
||||||
childTemplate = nil
|
childTemplate = ""
|
||||||
baseTemplate = nil
|
baseTemplate = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func expectError(reason: String, childToken: String, baseToken: String?,
|
func testSyntaxErrorInBaseTemplate() throws {
|
||||||
file: String = #file, line: Int = #line, function: String = #function) throws {
|
|
||||||
var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason)
|
|
||||||
if let baseToken = baseToken {
|
|
||||||
expectedError.stackTrace = [expectedSyntaxError(token: baseToken, template: baseTemplate, description: reason).token!]
|
|
||||||
}
|
|
||||||
let error = try expect(environment.render(template: childTemplate, context: ["target": "World"]),
|
|
||||||
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
|
||||||
let reporter = SimpleErrorReporter()
|
|
||||||
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("reports syntax error in base template") {
|
|
||||||
childTemplate = try environment.loadTemplate(name: "invalid-child-super.html")
|
childTemplate = try environment.loadTemplate(name: "invalid-child-super.html")
|
||||||
baseTemplate = try environment.loadTemplate(name: "invalid-base.html")
|
baseTemplate = try environment.loadTemplate(name: "invalid-base.html")
|
||||||
|
|
||||||
@@ -284,11 +302,11 @@ class EnvironmentTests: XCTestCase {
|
|||||||
baseToken: "target|unknown")
|
baseToken: "target|unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("reports runtime error in base template") {
|
func testRuntimeErrorInBaseTemplate() throws {
|
||||||
let filterExtension = Extension()
|
let filterExtension = Extension()
|
||||||
filterExtension.registerFilter("unknown", filter: { (_: Any?) in
|
filterExtension.registerFilter("unknown") { (_: Any?) in
|
||||||
throw TemplateSyntaxError("filter error")
|
throw TemplateSyntaxError("filter error")
|
||||||
})
|
}
|
||||||
environment.extensions += [filterExtension]
|
environment.extensions += [filterExtension]
|
||||||
|
|
||||||
childTemplate = try environment.loadTemplate(name: "invalid-child-super.html")
|
childTemplate = try environment.loadTemplate(name: "invalid-child-super.html")
|
||||||
@@ -299,44 +317,78 @@ class EnvironmentTests: XCTestCase {
|
|||||||
baseToken: "target|unknown")
|
baseToken: "target|unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("reports syntax error in child template") {
|
func testSyntaxErrorInChildTemplate() throws {
|
||||||
childTemplate = Template(templateString: """
|
childTemplate = Template(
|
||||||
|
templateString: """
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block body %}Child {{ target|unknown }}{% endblock %}
|
{% block body %}Child {{ target|unknown }}{% endblock %}
|
||||||
""", environment: environment, name: nil)
|
""",
|
||||||
|
environment: environment,
|
||||||
|
name: nil
|
||||||
|
)
|
||||||
|
|
||||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||||
childToken: "target|unknown",
|
childToken: "target|unknown",
|
||||||
baseToken: nil)
|
baseToken: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("reports runtime error in child template") {
|
func testRuntimeErrorInChildTemplate() throws {
|
||||||
let filterExtension = Extension()
|
let filterExtension = Extension()
|
||||||
filterExtension.registerFilter("unknown", filter: { (_: Any?) in
|
filterExtension.registerFilter("unknown") { (_: Any?) in
|
||||||
throw TemplateSyntaxError("filter error")
|
throw TemplateSyntaxError("filter error")
|
||||||
})
|
}
|
||||||
environment.extensions += [filterExtension]
|
environment.extensions += [filterExtension]
|
||||||
|
|
||||||
childTemplate = Template(templateString: """
|
childTemplate = Template(
|
||||||
|
templateString: """
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block body %}Child {{ target|unknown }}{% endblock %}
|
{% block body %}Child {{ target|unknown }}{% endblock %}
|
||||||
""", environment: environment, name: nil)
|
""",
|
||||||
|
environment: environment,
|
||||||
|
name: nil
|
||||||
|
)
|
||||||
|
|
||||||
try expectError(reason: "filter error",
|
try expectError(reason: "filter error",
|
||||||
childToken: "target|unknown",
|
childToken: "target|unknown",
|
||||||
baseToken: nil)
|
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 {
|
extension Expectation {
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func toThrow<T: Error>() throws -> T {
|
func toThrow<T: Error>() throws -> T {
|
||||||
var thrownError: Error? = nil
|
var thrownError: Error?
|
||||||
|
|
||||||
do {
|
do {
|
||||||
_ = try expression()
|
_ = try expression()
|
||||||
@@ -356,7 +408,20 @@ extension Expectation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate class ExampleLoader: Loader {
|
extension XCTestCase {
|
||||||
|
func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError {
|
||||||
|
guard let range = template.templateString.range(of: token) else {
|
||||||
|
fatalError("Can't find '\(token)' in '\(template)'")
|
||||||
|
}
|
||||||
|
let lexer = Lexer(templateString: template.templateString)
|
||||||
|
let location = lexer.rangeLocation(range)
|
||||||
|
let sourceMap = SourceMap(filename: template.name, location: location)
|
||||||
|
let token = Token.block(value: token, at: sourceMap)
|
||||||
|
return TemplateSyntaxError(reason: description, token: token, stackTrace: [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ExampleLoader: Loader {
|
||||||
func loadTemplate(name: String, environment: Environment) throws -> Template {
|
func loadTemplate(name: String, environment: Environment) throws -> Template {
|
||||||
if name == "example.html" {
|
if name == "example.html" {
|
||||||
return Template(templateString: "Hello World!", environment: environment, name: name)
|
return Template(templateString: "Hello World!", environment: environment, name: name)
|
||||||
@@ -366,8 +431,8 @@ fileprivate class ExampleLoader: Loader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class CustomTemplate: Template {
|
||||||
class CustomTemplate: Template {
|
// 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"
|
return "here"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,345 +1,355 @@
|
|||||||
import XCTest
|
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
|
import XCTest
|
||||||
|
|
||||||
class ExpressionsTests: XCTestCase {
|
final class ExpressionsTests: XCTestCase {
|
||||||
func testExpressions() {
|
let parser = TokenParser(tokens: [], environment: Environment())
|
||||||
describe("Expression") {
|
|
||||||
|
|
||||||
func parseExpression(components: [String]) throws -> Expression {
|
private func makeExpression(_ components: [String]) -> Expression {
|
||||||
let parser = try IfExpressionParser.parser(components: components, environment: Environment(), token: .text(value: "", at: .unknown))
|
do {
|
||||||
|
let parser = try IfExpressionParser.parser(
|
||||||
|
components: components,
|
||||||
|
environment: Environment(),
|
||||||
|
token: .text(value: "", at: .unknown)
|
||||||
|
)
|
||||||
return try parser.parse()
|
return try parser.parse()
|
||||||
|
} catch {
|
||||||
|
fatalError(error.localizedDescription)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("VariableExpression") {
|
func testTrueExpressions() {
|
||||||
let expression = VariableExpression(variable: Variable("value"))
|
let expression = VariableExpression(variable: Variable("value"))
|
||||||
|
|
||||||
$0.it("evaluates to true when value is not nil") {
|
it("evaluates to true when value is not nil") {
|
||||||
let context = Context(dictionary: ["value": "known"])
|
let context = Context(dictionary: ["value": "known"])
|
||||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false when value is unset") {
|
it("evaluates to true when array variable is not empty") {
|
||||||
let context = Context()
|
let items: [[String: Any]] = [["key": "key1", "value": 42], ["key": "key2", "value": 1_337]]
|
||||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("evaluates to true when array variable is not empty") {
|
|
||||||
let items: [[String: Any]] = [["key":"key1","value":42],["key":"key2","value":1337]]
|
|
||||||
let context = Context(dictionary: ["value": [items]])
|
let context = Context(dictionary: ["value": [items]])
|
||||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false when array value is empty") {
|
it("evaluates to false when dictionary value is empty") {
|
||||||
let emptyItems = [[String: Any]]()
|
|
||||||
let context = Context(dictionary: ["value": emptyItems])
|
|
||||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("evaluates to false when dictionary value is empty") {
|
|
||||||
let emptyItems = [String: Any]()
|
let emptyItems = [String: Any]()
|
||||||
let context = Context(dictionary: ["value": emptyItems])
|
let context = Context(dictionary: ["value": emptyItems])
|
||||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false when Array<Any> value is empty") {
|
it("evaluates to true when integer value is above 0") {
|
||||||
let context = Context(dictionary: ["value": ([] as [Any])])
|
|
||||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("evaluates to true when integer value is above 0") {
|
|
||||||
let context = Context(dictionary: ["value": 1])
|
let context = Context(dictionary: ["value": 1])
|
||||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to true with string") {
|
it("evaluates to true with string") {
|
||||||
let context = Context(dictionary: ["value": "test"])
|
let context = Context(dictionary: ["value": "test"])
|
||||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false when empty string") {
|
it("evaluates to true when float value is above 0") {
|
||||||
let context = Context(dictionary: ["value": ""])
|
|
||||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("evaluates to false when integer value is below 0 or below") {
|
|
||||||
let context = Context(dictionary: ["value": 0])
|
|
||||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
|
||||||
|
|
||||||
let negativeContext = Context(dictionary: ["value": 0])
|
|
||||||
try expect(try expression.evaluate(context: negativeContext)).to.beFalse()
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("evaluates to true when float value is above 0") {
|
|
||||||
let context = Context(dictionary: ["value": Float(0.5)])
|
let context = Context(dictionary: ["value": Float(0.5)])
|
||||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false when float is 0 or below") {
|
it("evaluates to true when double value is above 0") {
|
||||||
|
let context = Context(dictionary: ["value": Double(0.5)])
|
||||||
|
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFalseExpressions() {
|
||||||
|
let expression = VariableExpression(variable: Variable("value"))
|
||||||
|
|
||||||
|
it("evaluates to false when value is unset") {
|
||||||
|
let context = Context()
|
||||||
|
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
it("evaluates to false when array value is empty") {
|
||||||
|
let emptyItems = [[String: Any]]()
|
||||||
|
let context = Context(dictionary: ["value": emptyItems])
|
||||||
|
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
it("evaluates to false when dictionary value is empty") {
|
||||||
|
let emptyItems = [String: Any]()
|
||||||
|
let context = Context(dictionary: ["value": emptyItems])
|
||||||
|
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
it("evaluates to false when Array<Any> value is empty") {
|
||||||
|
let context = Context(dictionary: ["value": ([] as [Any])])
|
||||||
|
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
it("evaluates to false when empty string") {
|
||||||
|
let context = Context(dictionary: ["value": ""])
|
||||||
|
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
it("evaluates to false when integer value is below 0 or below") {
|
||||||
|
let context = Context(dictionary: ["value": 0])
|
||||||
|
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||||
|
|
||||||
|
let negativeContext = Context(dictionary: ["value": -1])
|
||||||
|
try expect(try expression.evaluate(context: negativeContext)).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
it("evaluates to false when float is 0 or below") {
|
||||||
let context = Context(dictionary: ["value": Float(0)])
|
let context = Context(dictionary: ["value": Float(0)])
|
||||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to true when double value is above 0") {
|
it("evaluates to false when double is 0 or below") {
|
||||||
let context = Context(dictionary: ["value": Double(0.5)])
|
|
||||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("evaluates to false when double is 0 or below") {
|
|
||||||
let context = Context(dictionary: ["value": Double(0)])
|
let context = Context(dictionary: ["value": Double(0)])
|
||||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false when uint is 0") {
|
it("evaluates to false when uint is 0") {
|
||||||
let context = Context(dictionary: ["value": UInt(0)])
|
let context = Context(dictionary: ["value": UInt(0)])
|
||||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("NotExpression") {
|
func testNotExpression() {
|
||||||
$0.it("returns truthy for positive expressions") {
|
it("returns truthy for positive expressions") {
|
||||||
let expression = NotExpression(expression: StaticExpression(value: true))
|
let expression = NotExpression(expression: StaticExpression(value: true))
|
||||||
try expect(expression.evaluate(context: Context())).to.beFalse()
|
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("returns falsy for negative expressions") {
|
it("returns falsy for negative expressions") {
|
||||||
let expression = NotExpression(expression: StaticExpression(value: false))
|
let expression = NotExpression(expression: StaticExpression(value: false))
|
||||||
try expect(expression.evaluate(context: Context())).to.beTrue()
|
try expect(expression.evaluate(context: Context())).to.beTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("expression parsing") {
|
func testExpressionParsing() {
|
||||||
$0.it("can parse a variable expression") {
|
it("can parse a variable expression") {
|
||||||
let expression = try parseExpression(components: ["value"])
|
let expression = self.makeExpression(["value"])
|
||||||
try expect(expression.evaluate(context: Context())).to.beFalse()
|
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can parse a not expression") {
|
it("can parse a not expression") {
|
||||||
let expression = try parseExpression(components: ["not", "value"])
|
let expression = self.makeExpression(["not", "value"])
|
||||||
try expect(expression.evaluate(context: Context())).to.beTrue()
|
try expect(expression.evaluate(context: Context())).to.beTrue()
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$0.describe("and expression") {
|
func testAndExpression() {
|
||||||
let expression = try! parseExpression(components: ["lhs", "and", "rhs"])
|
let expression = makeExpression(["lhs", "and", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to false with lhs false") {
|
it("evaluates to false with lhs false") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false with rhs false") {
|
it("evaluates to false with rhs false") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false with lhs and rhs false") {
|
it("evaluates to false with lhs and rhs false") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to true with lhs and rhs true") {
|
it("evaluates to true with lhs and rhs true") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("or expression") {
|
func testOrExpression() {
|
||||||
let expression = try! parseExpression(components: ["lhs", "or", "rhs"])
|
let expression = makeExpression(["lhs", "or", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with lhs true") {
|
it("evaluates to true with lhs true") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to true with rhs true") {
|
it("evaluates to true with rhs true") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to true with lhs and rhs true") {
|
it("evaluates to true with lhs and rhs true") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false with lhs and rhs false") {
|
it("evaluates to false with lhs and rhs false") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("equality expression") {
|
func testEqualityExpression() {
|
||||||
let expression = try! parseExpression(components: ["lhs", "==", "rhs"])
|
let expression = makeExpression(["lhs", "==", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with equal lhs/rhs") {
|
it("evaluates to true with equal lhs/rhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false with non equal lhs/rhs") {
|
it("evaluates to false with non equal lhs/rhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to true with nils") {
|
it("evaluates to true with nils") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: [:]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: [:]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to true with numbers") {
|
it("evaluates to true with numbers") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.0]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.0]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false with non equal numbers") {
|
it("evaluates to false with non equal numbers") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.1]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.1]))).to.beFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to true with booleans") {
|
it("evaluates to true with booleans") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false with falsy booleans") {
|
it("evaluates to false with falsy booleans") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false with different types") {
|
it("evaluates to false with different types") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": 1]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": 1]))).to.beFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("inequality expression") {
|
func testInequalityExpression() {
|
||||||
let expression = try! parseExpression(components: ["lhs", "!=", "rhs"])
|
let expression = makeExpression(["lhs", "!=", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with inequal lhs/rhs") {
|
it("evaluates to true with inequal lhs/rhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false with equal lhs/rhs") {
|
it("evaluates to false with equal lhs/rhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "b", "rhs": "b"]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "b", "rhs": "b"]))).to.beFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("more than expression") {
|
func testMoreThanExpression() {
|
||||||
let expression = try! parseExpression(components: ["lhs", ">", "rhs"])
|
let expression = makeExpression(["lhs", ">", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with lhs > rhs") {
|
it("evaluates to true with lhs > rhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false with lhs == rhs") {
|
it("evaluates to false with lhs == rhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("more than equal expression") {
|
func testMoreThanEqualExpression() {
|
||||||
let expression = try! parseExpression(components: ["lhs", ">=", "rhs"])
|
let expression = makeExpression(["lhs", ">=", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with lhs == rhs") {
|
it("evaluates to true with lhs == rhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false with lhs < rhs") {
|
it("evaluates to false with lhs < rhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.1]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.1]))).to.beFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("less than expression") {
|
func testLessThanExpression() {
|
||||||
let expression = try! parseExpression(components: ["lhs", "<", "rhs"])
|
let expression = makeExpression(["lhs", "<", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with lhs < rhs") {
|
it("evaluates to true with lhs < rhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false with lhs == rhs") {
|
it("evaluates to false with lhs == rhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("less than equal expression") {
|
func testLessThanEqualExpression() {
|
||||||
let expression = try! parseExpression(components: ["lhs", "<=", "rhs"])
|
let expression = makeExpression(["lhs", "<=", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with lhs == rhs") {
|
it("evaluates to true with lhs == rhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false with lhs > rhs") {
|
it("evaluates to false with lhs > rhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.1, "rhs": 5.0]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.1, "rhs": 5.0]))).to.beFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("multiple expression") {
|
func testMultipleExpressions() {
|
||||||
let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"])
|
let expression = makeExpression(["one", "or", "two", "and", "not", "three"])
|
||||||
|
|
||||||
$0.it("evaluates to true with one") {
|
it("evaluates to true with one") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to true with one and three") {
|
it("evaluates to true with one and three") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["one": true, "three": true]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["one": true, "three": true]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to true with two") {
|
it("evaluates to true with two") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["two": true]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["two": true]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false with two and three") {
|
it("evaluates to false with two and three") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false with two and three") {
|
it("evaluates to false with two and three") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false with nothing") {
|
it("evaluates to false with nothing") {
|
||||||
try expect(expression.evaluate(context: Context())).to.beFalse()
|
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("in expression") {
|
func testTrueInExpression() throws {
|
||||||
let expression = try! parseExpression(components: ["lhs", "in", "rhs"])
|
let expression = makeExpression(["lhs", "in", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true when rhs contains lhs") {
|
try expect(expression.evaluate(context: Context(dictionary: [
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue()
|
"lhs": 1,
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["a", "b", "c"]]))).to.beTrue()
|
"rhs": [1, 2, 3]
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "abc"]))).to.beTrue()
|
]))).to.beTrue()
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1...3]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: [
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1..<3]))).to.beTrue()
|
"lhs": "a",
|
||||||
|
"rhs": ["a", "b", "c"]
|
||||||
|
]))).to.beTrue()
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: [
|
||||||
|
"lhs": "a",
|
||||||
|
"rhs": "abc"
|
||||||
|
]))).to.beTrue()
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: [
|
||||||
|
"lhs": 1,
|
||||||
|
"rhs": 1...3
|
||||||
|
]))).to.beTrue()
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: [
|
||||||
|
"lhs": 1,
|
||||||
|
"rhs": 1..<3
|
||||||
|
]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false when rhs does not contain lhs") {
|
func testFalseInExpression() throws {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [2, 3, 4]]))).to.beFalse()
|
let expression = makeExpression(["lhs", "in", "rhs"])
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["b", "c", "d"]]))).to.beFalse()
|
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "bcd"]))).to.beFalse()
|
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 1...3]))).to.beFalse()
|
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 3, "rhs": 1..<3]))).to.beFalse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.describe("sub expression") {
|
try expect(expression.evaluate(context: Context(dictionary: [
|
||||||
$0.it("evaluates correctly") {
|
"lhs": 1,
|
||||||
let context = Context(dictionary: ["one": false, "two": false, "three": true, "four": true])
|
"rhs": [2, 3, 4]
|
||||||
|
]))).to.beFalse()
|
||||||
let expression = try! parseExpression(components: ["one", "and", "two", "or", "three", "and", "four"])
|
try expect(expression.evaluate(context: Context(dictionary: [
|
||||||
let expressionWithBrackets = try! parseExpression(components: ["one", "and", "(", "(", "two", ")", "or", "(", "three", "and", "four", ")", ")"])
|
"lhs": "a",
|
||||||
|
"rhs": ["b", "c", "d"]
|
||||||
try expect(expression.evaluate(context: context)).to.beTrue()
|
]))).to.beFalse()
|
||||||
try expect(expressionWithBrackets.evaluate(context: context)).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: [
|
||||||
|
"lhs": "a",
|
||||||
let notExpression = try! parseExpression(components: ["not", "one", "or", "three"])
|
"rhs": "bcd"
|
||||||
let notExpressionWithBrackets = try! parseExpression(components: ["not", "(", "one", "or", "three", ")"])
|
]))).to.beFalse()
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: [
|
||||||
try expect(notExpression.evaluate(context: context)).to.beTrue()
|
"lhs": 4,
|
||||||
try expect(notExpressionWithBrackets.evaluate(context: context)).to.beFalse()
|
"rhs": 1...3
|
||||||
}
|
]))).to.beFalse()
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: [
|
||||||
$0.it("fails when brackets are not balanced") {
|
"lhs": 3,
|
||||||
try expect(parseExpression(components: ["(", "lhs", "and", "rhs"]))
|
"rhs": 1..<3
|
||||||
.toThrow(TemplateSyntaxError("'if' expression error: missing closing bracket"))
|
]))).to.beFalse()
|
||||||
try expect(parseExpression(components: [")", "lhs", "and", "rhs"]))
|
|
||||||
.toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket"))
|
|
||||||
try expect(parseExpression(components: ["lhs", "and", "rhs", ")"]))
|
|
||||||
.toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket"))
|
|
||||||
try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", "("]))
|
|
||||||
.toThrow(TemplateSyntaxError("'if' expression error: missing closing bracket"))
|
|
||||||
try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", ")"]))
|
|
||||||
.toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket"))
|
|
||||||
try expect(parseExpression(components: ["(", "lhs", "and", ")"]))
|
|
||||||
.toThrow(TemplateSyntaxError("'if' expression error: end"))
|
|
||||||
try expect(parseExpression(components: ["(", "and", "rhs", ")"]))
|
|
||||||
.toThrow(TemplateSyntaxError("'if' expression error: infix operator 'and' doesn't have a left hand side"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import XCTest
|
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
|
import XCTest
|
||||||
|
|
||||||
class FilterTests: XCTestCase {
|
final class FilterTests: XCTestCase {
|
||||||
func testFilter() {
|
func testRegistration() {
|
||||||
describe("template filters") {
|
|
||||||
let context: [String: Any] = ["name": "Kyle"]
|
let context: [String: Any] = ["name": "Kyle"]
|
||||||
|
|
||||||
$0.it("allows you to register a custom filter") {
|
it("allows you to register a custom filter") {
|
||||||
let template = Template(templateString: "{{ name|repeat }}")
|
let template = Template(templateString: "{{ name|repeat }}")
|
||||||
|
|
||||||
let repeatExtension = Extension()
|
let repeatExtension = Extension()
|
||||||
@@ -19,11 +18,14 @@ class FilterTests: XCTestCase {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
let result = try template.render(Context(
|
||||||
|
dictionary: context,
|
||||||
|
environment: Environment(extensions: [repeatExtension])
|
||||||
|
))
|
||||||
try expect(result) == "Kyle Kyle"
|
try expect(result) == "Kyle Kyle"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("allows you to register boolean filters") {
|
it("allows you to register boolean filters") {
|
||||||
let repeatExtension = Extension()
|
let repeatExtension = Extension()
|
||||||
repeatExtension.registerFilter(name: "isPositive", negativeFilterName: "isNotPositive") { (value: Any?) in
|
repeatExtension.registerFilter(name: "isPositive", negativeFilterName: "isNotPositive") { (value: Any?) in
|
||||||
if let value = value as? Int {
|
if let value = value as? Int {
|
||||||
@@ -41,106 +43,117 @@ class FilterTests: XCTestCase {
|
|||||||
try expect(negativeResult) == "true"
|
try expect(negativeResult) == "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("allows you to register a custom filter which accepts single argument") {
|
it("allows you to register a custom which throws") {
|
||||||
|
let template = Template(templateString: "{{ name|repeat }}")
|
||||||
|
let repeatExtension = Extension()
|
||||||
|
repeatExtension.registerFilter("repeat") { (_: Any?) in
|
||||||
|
throw TemplateSyntaxError("No Repeat")
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))
|
||||||
|
try expect(try template.render(context))
|
||||||
|
.toThrow(TemplateSyntaxError(reason: "No Repeat", token: template.tokens.first))
|
||||||
|
}
|
||||||
|
|
||||||
|
it("throws when you pass arguments to simple filter") {
|
||||||
|
let template = Template(templateString: "{{ name|uppercase:5 }}")
|
||||||
|
try expect(try template.render(Context(dictionary: ["name": "kyle"]))).toThrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRegistrationOverrideDefault() throws {
|
||||||
|
let template = Template(templateString: "{{ name|join }}")
|
||||||
|
let context: [String: Any] = ["name": "Kyle"]
|
||||||
|
|
||||||
|
let repeatExtension = Extension()
|
||||||
|
repeatExtension.registerFilter("join") { (_: Any?) in
|
||||||
|
"joined"
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = try template.render(Context(
|
||||||
|
dictionary: context,
|
||||||
|
environment: Environment(extensions: [repeatExtension])
|
||||||
|
))
|
||||||
|
try expect(result) == "joined"
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRegistrationWithArguments() {
|
||||||
|
let context: [String: Any] = ["name": "Kyle"]
|
||||||
|
|
||||||
|
it("allows you to register a custom filter which accepts single argument") {
|
||||||
let template = Template(templateString: """
|
let template = Template(templateString: """
|
||||||
{{ name|repeat:'value1, "value2"' }}
|
{{ name|repeat:'value1, "value2"' }}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
let repeatExtension = Extension()
|
let repeatExtension = Extension()
|
||||||
repeatExtension.registerFilter("repeat") { value, arguments in
|
repeatExtension.registerFilter("repeat") { value, arguments in
|
||||||
if !arguments.isEmpty {
|
guard let value = value,
|
||||||
return "\(value!) \(value!) with args \(arguments.first!!)"
|
let argument = arguments.first else { return nil }
|
||||||
|
|
||||||
|
return "\(value) \(value) with args \(argument ?? "")"
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
let result = try template.render(Context(
|
||||||
}
|
dictionary: context,
|
||||||
|
environment: Environment(extensions: [repeatExtension])
|
||||||
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
))
|
||||||
try expect(result) == """
|
try expect(result) == """
|
||||||
Kyle Kyle with args value1, "value2"
|
Kyle Kyle with args value1, "value2"
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("allows you to register a custom filter which accepts several arguments") {
|
it("allows you to register a custom filter which accepts several arguments") {
|
||||||
let template = Template(templateString: """
|
let template = Template(templateString: """
|
||||||
{{ name|repeat:'value"1"',"value'2'",'(key, value)' }}
|
{{ name|repeat:'value"1"',"value'2'",'(key, value)' }}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
let repeatExtension = Extension()
|
let repeatExtension = Extension()
|
||||||
repeatExtension.registerFilter("repeat") { value, arguments in
|
repeatExtension.registerFilter("repeat") { value, arguments in
|
||||||
if !arguments.isEmpty {
|
guard let value = value else { return nil }
|
||||||
return "\(value!) \(value!) with args 0: \(arguments[0]!), 1: \(arguments[1]!), 2: \(arguments[2]!)"
|
let args = arguments.compactMap { $0 }
|
||||||
|
return "\(value) \(value) with args 0: \(args[0]), 1: \(args[1]), 2: \(args[2])"
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
let result = try template.render(Context(
|
||||||
}
|
dictionary: context,
|
||||||
|
environment: Environment(extensions: [repeatExtension])
|
||||||
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
))
|
||||||
try expect(result) == """
|
try expect(result) == """
|
||||||
Kyle Kyle with args 0: value"1", 1: value'2', 2: (key, value)
|
Kyle Kyle with args 0: value"1", 1: value'2', 2: (key, value)
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("allows you to register a custom which throws") {
|
it("allows whitespace in expression") {
|
||||||
let template = Template(templateString: "{{ name|repeat }}")
|
|
||||||
let repeatExtension = Extension()
|
|
||||||
repeatExtension.registerFilter("repeat") { (value: Any?) in
|
|
||||||
throw TemplateSyntaxError("No Repeat")
|
|
||||||
}
|
|
||||||
|
|
||||||
let context = Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))
|
|
||||||
try expect(try template.render(context)).toThrow(TemplateSyntaxError(reason: "No Repeat", token: template.tokens.first))
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("allows you to override a default filter") {
|
|
||||||
let template = Template(templateString: "{{ name|join }}")
|
|
||||||
|
|
||||||
let repeatExtension = Extension()
|
|
||||||
repeatExtension.registerFilter("join") { (value: Any?) in
|
|
||||||
return "joined"
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
|
||||||
try expect(result) == "joined"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("allows whitespace in expression") {
|
|
||||||
let template = Template(templateString: """
|
let template = Template(templateString: """
|
||||||
{{ value | join : ", " }}
|
{{ value | join : ", " }}
|
||||||
""")
|
""")
|
||||||
let result = try template.render(Context(dictionary: ["value": ["One", "Two"]]))
|
let result = try template.render(Context(dictionary: ["value": ["One", "Two"]]))
|
||||||
try expect(result) == "One, Two"
|
try expect(result) == "One, Two"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws when you pass arguments to simple filter") {
|
|
||||||
let template = Template(templateString: "{{ name|uppercase:5 }}")
|
|
||||||
try expect(try template.render(Context(dictionary: ["name": "kyle"]))).toThrow()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("string filters") {
|
func testStringFilters() {
|
||||||
$0.context("given string") {
|
it("transforms a string to be capitalized") {
|
||||||
$0.it("transforms a string to be capitalized") {
|
|
||||||
let template = Template(templateString: "{{ name|capitalize }}")
|
let template = Template(templateString: "{{ name|capitalize }}")
|
||||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
||||||
try expect(result) == "Kyle"
|
try expect(result) == "Kyle"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("transforms a string to be uppercase") {
|
it("transforms a string to be uppercase") {
|
||||||
let template = Template(templateString: "{{ name|uppercase }}")
|
let template = Template(templateString: "{{ name|uppercase }}")
|
||||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
||||||
try expect(result) == "KYLE"
|
try expect(result) == "KYLE"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("transforms a string to be lowercase") {
|
it("transforms a string to be lowercase") {
|
||||||
let template = Template(templateString: "{{ name|lowercase }}")
|
let template = Template(templateString: "{{ name|lowercase }}")
|
||||||
let result = try template.render(Context(dictionary: ["name": "Kyle"]))
|
let result = try template.render(Context(dictionary: ["name": "Kyle"]))
|
||||||
try expect(result) == "kyle"
|
try expect(result) == "kyle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.context("given array of strings") {
|
func testStringFiltersWithArrays() {
|
||||||
$0.it("transforms a string to be capitalized") {
|
it("transforms a string to be capitalized") {
|
||||||
let template = Template(templateString: "{{ names|capitalize }}")
|
let template = Template(templateString: "{{ names|capitalize }}")
|
||||||
let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]]))
|
let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]]))
|
||||||
try expect(result) == """
|
try expect(result) == """
|
||||||
@@ -148,7 +161,7 @@ class FilterTests: XCTestCase {
|
|||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("transforms a string to be uppercase") {
|
it("transforms a string to be uppercase") {
|
||||||
let template = Template(templateString: "{{ names|uppercase }}")
|
let template = Template(templateString: "{{ names|uppercase }}")
|
||||||
let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]]))
|
let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]]))
|
||||||
try expect(result) == """
|
try expect(result) == """
|
||||||
@@ -156,7 +169,7 @@ class FilterTests: XCTestCase {
|
|||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("transforms a string to be lowercase") {
|
it("transforms a string to be lowercase") {
|
||||||
let template = Template(templateString: "{{ names|lowercase }}")
|
let template = Template(templateString: "{{ names|lowercase }}")
|
||||||
let result = try template.render(Context(dictionary: ["names": ["Kyle", "Kyle"]]))
|
let result = try template.render(Context(dictionary: ["names": ["Kyle", "Kyle"]]))
|
||||||
try expect(result) == """
|
try expect(result) == """
|
||||||
@@ -164,24 +177,23 @@ class FilterTests: XCTestCase {
|
|||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
describe("default filter") {
|
func testDefaultFilter() {
|
||||||
let template = Template(templateString: """
|
let template = Template(templateString: """
|
||||||
Hello {{ name|default:"World" }}
|
Hello {{ name|default:"World" }}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
$0.it("shows the variable value") {
|
it("shows the variable value") {
|
||||||
let result = try template.render(Context(dictionary: ["name": "Kyle"]))
|
let result = try template.render(Context(dictionary: ["name": "Kyle"]))
|
||||||
try expect(result) == "Hello Kyle"
|
try expect(result) == "Hello Kyle"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("shows the default value") {
|
it("shows the default value") {
|
||||||
let result = try template.render(Context(dictionary: [:]))
|
let result = try template.render(Context(dictionary: [:]))
|
||||||
try expect(result) == "Hello World"
|
try expect(result) == "Hello World"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("supports multiple defaults") {
|
it("supports multiple defaults") {
|
||||||
let template = Template(templateString: """
|
let template = Template(templateString: """
|
||||||
Hello {{ name|default:a,b,c,"World" }}
|
Hello {{ name|default:a,b,c,"World" }}
|
||||||
""")
|
""")
|
||||||
@@ -189,19 +201,19 @@ class FilterTests: XCTestCase {
|
|||||||
try expect(result) == "Hello World"
|
try expect(result) == "Hello World"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can use int as default") {
|
it("can use int as default") {
|
||||||
let template = Template(templateString: "{{ value|default:1 }}")
|
let template = Template(templateString: "{{ value|default:1 }}")
|
||||||
let result = try template.render(Context(dictionary: [:]))
|
let result = try template.render(Context(dictionary: [:]))
|
||||||
try expect(result) == "1"
|
try expect(result) == "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can use float as default") {
|
it("can use float as default") {
|
||||||
let template = Template(templateString: "{{ value|default:1.5 }}")
|
let template = Template(templateString: "{{ value|default:1.5 }}")
|
||||||
let result = try template.render(Context(dictionary: [:]))
|
let result = try template.render(Context(dictionary: [:]))
|
||||||
try expect(result) == "1.5"
|
try expect(result) == "1.5"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("checks for underlying nil value correctly") {
|
it("checks for underlying nil value correctly") {
|
||||||
let template = Template(templateString: """
|
let template = Template(templateString: """
|
||||||
Hello {{ user.name|default:"anonymous" }}
|
Hello {{ user.name|default:"anonymous" }}
|
||||||
""")
|
""")
|
||||||
@@ -212,22 +224,22 @@ class FilterTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("join filter") {
|
func testJoinFilter() {
|
||||||
let template = Template(templateString: """
|
let template = Template(templateString: """
|
||||||
{{ value|join:", " }}
|
{{ value|join:", " }}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
$0.it("joins a collection of strings") {
|
it("joins a collection of strings") {
|
||||||
let result = try template.render(Context(dictionary: ["value": ["One", "Two"]]))
|
let result = try template.render(Context(dictionary: ["value": ["One", "Two"]]))
|
||||||
try expect(result) == "One, Two"
|
try expect(result) == "One, Two"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("joins a mixed-type collection") {
|
it("joins a mixed-type collection") {
|
||||||
let result = try template.render(Context(dictionary: ["value": ["One", 2, true, 10.5, "Five"]]))
|
let result = try template.render(Context(dictionary: ["value": ["One", 2, true, 10.5, "Five"]]))
|
||||||
try expect(result) == "One, 2, true, 10.5, Five"
|
try expect(result) == "One, 2, true, 10.5, Five"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can join by non string") {
|
it("can join by non string") {
|
||||||
let template = Template(templateString: """
|
let template = Template(templateString: """
|
||||||
{{ value|join:separator }}
|
{{ value|join:separator }}
|
||||||
""")
|
""")
|
||||||
@@ -235,7 +247,7 @@ class FilterTests: XCTestCase {
|
|||||||
try expect(result) == "OnetrueTwo"
|
try expect(result) == "OnetrueTwo"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can join without arguments") {
|
it("can join without arguments") {
|
||||||
let template = Template(templateString: """
|
let template = Template(templateString: """
|
||||||
{{ value|join }}
|
{{ value|join }}
|
||||||
""")
|
""")
|
||||||
@@ -244,19 +256,19 @@ class FilterTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("split filter") {
|
func testSplitFilter() {
|
||||||
let template = Template(templateString: """
|
let template = Template(templateString: """
|
||||||
{{ value|split:", " }}
|
{{ value|split:", " }}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
$0.it("split a string into array") {
|
it("split a string into array") {
|
||||||
let result = try template.render(Context(dictionary: ["value": "One, Two"]))
|
let result = try template.render(Context(dictionary: ["value": "One, Two"]))
|
||||||
try expect(result) == """
|
try expect(result) == """
|
||||||
["One", "Two"]
|
["One", "Two"]
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can split without arguments") {
|
it("can split without arguments") {
|
||||||
let template = Template(templateString: """
|
let template = Template(templateString: """
|
||||||
{{ value|split }}
|
{{ value|split }}
|
||||||
""")
|
""")
|
||||||
@@ -267,62 +279,49 @@ class FilterTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testFilterSuggestion() {
|
||||||
describe("filter suggestion") {
|
it("made for unknown filter") {
|
||||||
var template: Template!
|
let template = Template(templateString: "{{ value|unknownFilter }}")
|
||||||
var filterExtension: Extension!
|
let filterExtension = Extension()
|
||||||
|
|
||||||
func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError {
|
|
||||||
guard let range = template.templateString.range(of: token) else {
|
|
||||||
fatalError("Can't find '\(token)' in '\(template)'")
|
|
||||||
}
|
|
||||||
let lexer = Lexer(templateString: template.templateString)
|
|
||||||
let location = lexer.rangeLocation(range)
|
|
||||||
let sourceMap = SourceMap(filename: template.name, location: location)
|
|
||||||
let token = Token.block(value: token, at: sourceMap)
|
|
||||||
return TemplateSyntaxError(reason: description, token: token, stackTrace: [])
|
|
||||||
}
|
|
||||||
|
|
||||||
func expectError(reason: String, token: String,
|
|
||||||
file: String = #file, line: Int = #line, function: String = #function) throws {
|
|
||||||
let expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
|
||||||
let environment = Environment(extensions: [filterExtension])
|
|
||||||
|
|
||||||
let error = try expect(environment.render(template: template, context: [:]),
|
|
||||||
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
|
||||||
let reporter = SimpleErrorReporter()
|
|
||||||
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("made for unknown filter") {
|
|
||||||
template = Template(templateString: "{{ value|unknownFilter }}")
|
|
||||||
|
|
||||||
filterExtension = Extension()
|
|
||||||
filterExtension.registerFilter("knownFilter") { value, _ in value }
|
filterExtension.registerFilter("knownFilter") { value, _ in value }
|
||||||
|
|
||||||
try expectError(reason: "Unknown filter 'unknownFilter'. Found similar filters: 'knownFilter'.", token: "value|unknownFilter")
|
try self.expectError(
|
||||||
|
reason: "Unknown filter 'unknownFilter'. Found similar filters: 'knownFilter'.",
|
||||||
|
token: "value|unknownFilter",
|
||||||
|
template: template,
|
||||||
|
extension: filterExtension
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("made for multiple similar filters") {
|
it("made for multiple similar filters") {
|
||||||
template = Template(templateString: "{{ value|lowerFirst }}")
|
let template = Template(templateString: "{{ value|lowerFirst }}")
|
||||||
|
let filterExtension = Extension()
|
||||||
filterExtension = Extension()
|
|
||||||
filterExtension.registerFilter("lowerFirstWord") { value, _ in value }
|
filterExtension.registerFilter("lowerFirstWord") { value, _ in value }
|
||||||
filterExtension.registerFilter("lowerFirstLetter") { value, _ in value }
|
filterExtension.registerFilter("lowerFirstLetter") { value, _ in value }
|
||||||
|
|
||||||
try expectError(reason: "Unknown filter 'lowerFirst'. Found similar filters: 'lowerFirstWord', 'lowercase'.", token: "value|lowerFirst")
|
try self.expectError(
|
||||||
|
reason: "Unknown filter 'lowerFirst'. Found similar filters: 'lowerFirstWord', 'lowercase'.",
|
||||||
|
token: "value|lowerFirst",
|
||||||
|
template: template,
|
||||||
|
extension: filterExtension
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("not made when can't find similar filter") {
|
it("not made when can't find similar filter") {
|
||||||
template = Template(templateString: "{{ value|unknownFilter }}")
|
let template = Template(templateString: "{{ value|unknownFilter }}")
|
||||||
try expectError(reason: "Unknown filter 'unknownFilter'. Found similar filters: 'lowerFirstWord'.", token: "value|unknownFilter")
|
let filterExtension = Extension()
|
||||||
|
filterExtension.registerFilter("lowerFirstWord") { value, _ in value }
|
||||||
|
|
||||||
|
try self.expectError(
|
||||||
|
reason: "Unknown filter 'unknownFilter'. Found similar filters: 'lowerFirstWord'.",
|
||||||
|
token: "value|unknownFilter",
|
||||||
|
template: template,
|
||||||
|
extension: filterExtension
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
func testIndentContent() throws {
|
||||||
|
|
||||||
|
|
||||||
describe("indent filter") {
|
|
||||||
$0.it("indents content") {
|
|
||||||
let template = Template(templateString: """
|
let template = Template(templateString: """
|
||||||
{{ value|indent:2 }}
|
{{ value|indent:2 }}
|
||||||
""")
|
""")
|
||||||
@@ -336,7 +335,7 @@ class FilterTests: XCTestCase {
|
|||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can indent with arbitrary character") {
|
func testIndentWithArbitraryCharacter() throws {
|
||||||
let template = Template(templateString: """
|
let template = Template(templateString: """
|
||||||
{{ value|indent:2,"\t" }}
|
{{ value|indent:2,"\t" }}
|
||||||
""")
|
""")
|
||||||
@@ -350,7 +349,7 @@ class FilterTests: XCTestCase {
|
|||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can indent first line") {
|
func testIndentFirstLine() throws {
|
||||||
let template = Template(templateString: """
|
let template = Template(templateString: """
|
||||||
{{ value|indent:2," ",true }}
|
{{ value|indent:2," ",true }}
|
||||||
""")
|
""")
|
||||||
@@ -364,7 +363,7 @@ class FilterTests: XCTestCase {
|
|||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("does not indent empty lines") {
|
func testIndentNotEmptyLines() throws {
|
||||||
let template = Template(templateString: """
|
let template = Template(templateString: """
|
||||||
{{ value|indent }}
|
{{ value|indent }}
|
||||||
""")
|
""")
|
||||||
@@ -385,29 +384,62 @@ class FilterTests: XCTestCase {
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
describe("dynamic filter") {
|
func testDynamicFilters() throws {
|
||||||
|
it("can apply dynamic filter") {
|
||||||
$0.it("can apply dynamic filter") {
|
|
||||||
let template = Template(templateString: "{{ name|filter:somefilter }}")
|
let template = Template(templateString: "{{ name|filter:somefilter }}")
|
||||||
let result = try template.render(Context(dictionary: ["name": "Jhon", "somefilter": "uppercase"]))
|
let result = try template.render(Context(dictionary: ["name": "Jhon", "somefilter": "uppercase"]))
|
||||||
try expect(result) == "JHON"
|
try expect(result) == "JHON"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can apply dynamic filter on array") {
|
it("can apply dynamic filter on array") {
|
||||||
let template = Template(templateString: "{{ values|filter:joinfilter }}")
|
let template = Template(templateString: "{{ values|filter:joinfilter }}")
|
||||||
let result = try template.render(Context(dictionary: ["values": [1, 2, 3], "joinfilter": "join:\", \""]))
|
let result = try template.render(Context(dictionary: ["values": [1, 2, 3], "joinfilter": "join:\", \""]))
|
||||||
try expect(result) == "1, 2, 3"
|
try expect(result) == "1, 2, 3"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws on unknown dynamic filter") {
|
it("throws on unknown dynamic filter") {
|
||||||
let template = Template(templateString: "{{ values|filter:unknown }}")
|
let template = Template(templateString: "{{ values|filter:unknown }}")
|
||||||
let context = Context(dictionary: ["values": [1, 2, 3], "unknown": "absurd"])
|
let context = Context(dictionary: ["values": [1, 2, 3], "unknown": "absurd"])
|
||||||
try expect(try template.render(context)).toThrow()
|
try expect(try template.render(context)).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func expectError(
|
||||||
|
reason: String,
|
||||||
|
token: String,
|
||||||
|
template: Template,
|
||||||
|
extension: Extension,
|
||||||
|
file: String = #file,
|
||||||
|
line: Int = #line,
|
||||||
|
function: String = #function
|
||||||
|
) throws {
|
||||||
|
guard let range = template.templateString.range(of: token) else {
|
||||||
|
fatalError("Can't find '\(token)' in '\(template)'")
|
||||||
|
}
|
||||||
|
|
||||||
|
let environment = Environment(extensions: [`extension`])
|
||||||
|
let expectedError: Error = {
|
||||||
|
let lexer = Lexer(templateString: template.templateString)
|
||||||
|
let location = lexer.rangeLocation(range)
|
||||||
|
let sourceMap = SourceMap(filename: template.name, location: location)
|
||||||
|
let token = Token.block(value: token, at: sourceMap)
|
||||||
|
return TemplateSyntaxError(reason: reason, token: token, stackTrace: [])
|
||||||
|
}()
|
||||||
|
|
||||||
|
let error = try expect(
|
||||||
|
environment.render(template: template, context: [:]),
|
||||||
|
file: file,
|
||||||
|
line: line,
|
||||||
|
function: function
|
||||||
|
).toThrow() as TemplateSyntaxError
|
||||||
|
let reporter = SimpleErrorReporter()
|
||||||
|
|
||||||
|
try expect(
|
||||||
|
reporter.renderError(error),
|
||||||
|
file: file,
|
||||||
|
line: line,
|
||||||
|
function: function
|
||||||
|
) == reporter.renderError(expectedError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,33 @@
|
|||||||
import XCTest
|
|
||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
import Stencil
|
||||||
|
import XCTest
|
||||||
|
|
||||||
class FilterTagTests: XCTestCase {
|
final class FilterTagTests: XCTestCase {
|
||||||
func testFilterTag() {
|
func testFilterTag() {
|
||||||
describe("Filter Tag") {
|
it("allows you to use a filter") {
|
||||||
$0.it("allows you to use a filter") {
|
|
||||||
let template = Template(templateString: "{% filter uppercase %}Test{% endfilter %}")
|
let template = Template(templateString: "{% filter uppercase %}Test{% endfilter %}")
|
||||||
let result = try template.render()
|
let result = try template.render()
|
||||||
try expect(result) == "TEST"
|
try expect(result) == "TEST"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("allows you to chain filters") {
|
it("allows you to chain filters") {
|
||||||
let template = Template(templateString: "{% filter lowercase|capitalize %}TEST{% endfilter %}")
|
let template = Template(templateString: "{% filter lowercase|capitalize %}TEST{% endfilter %}")
|
||||||
let result = try template.render()
|
let result = try template.render()
|
||||||
try expect(result) == "Test"
|
try expect(result) == "Test"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("errors without a filter") {
|
it("errors without a filter") {
|
||||||
let template = Template(templateString: "Some {% filter %}Test{% endfilter %}")
|
let template = Template(templateString: "Some {% filter %}Test{% endfilter %}")
|
||||||
try expect(try template.render()).toThrow()
|
try expect(try template.render()).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can render filters with arguments") {
|
it("can render filters with arguments") {
|
||||||
let ext = Extension()
|
let ext = Extension()
|
||||||
ext.registerFilter("split", filter: {
|
ext.registerFilter("split") {
|
||||||
return ($0 as! String).components(separatedBy: $1[0] as! String)
|
guard let value = $0 as? String,
|
||||||
})
|
let argument = $1.first as? String else { return $0 }
|
||||||
|
return value.components(separatedBy: argument)
|
||||||
|
}
|
||||||
let env = Environment(extensions: [ext])
|
let env = Environment(extensions: [ext])
|
||||||
let result = try env.renderTemplate(string: """
|
let result = try env.renderTemplate(string: """
|
||||||
{% filter split:","|join:";" %}{{ items|join:"," }}{% endfilter %}
|
{% filter split:","|join:";" %}{{ items|join:"," }}{% endfilter %}
|
||||||
@@ -34,12 +35,15 @@ class FilterTagTests: XCTestCase {
|
|||||||
try expect(result) == "1;2"
|
try expect(result) == "1;2"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.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", filter: {
|
ext.registerFilter("replace") {
|
||||||
print($1[0] as! String)
|
guard let value = $0 as? String,
|
||||||
return ($0 as! String).replacingOccurrences(of: $1[0] as! String, with: $1[1] as! String)
|
$1.count == 2,
|
||||||
})
|
let search = $1.first as? String,
|
||||||
|
let replacement = $1.last as? String else { return $0 }
|
||||||
|
return value.replacingOccurrences(of: search, with: replacement)
|
||||||
|
}
|
||||||
let env = Environment(extensions: [ext])
|
let env = Environment(extensions: [ext])
|
||||||
let result = try env.renderTemplate(string: """
|
let result = try env.renderTemplate(string: """
|
||||||
{% filter replace:'"',"" %}{{ items|join:"," }}{% endfilter %}
|
{% filter replace:'"',"" %}{{ items|join:"," }}{% endfilter %}
|
||||||
@@ -48,4 +52,3 @@ class FilterTagTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,134 +1,63 @@
|
|||||||
import XCTest
|
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
import Foundation
|
import XCTest
|
||||||
|
|
||||||
class ForNodeTests: XCTestCase {
|
final class ForNodeTests: XCTestCase {
|
||||||
func testForNode() {
|
|
||||||
describe("ForNode") {
|
|
||||||
let context = Context(dictionary: [
|
let context = Context(dictionary: [
|
||||||
"items": [1, 2, 3],
|
"items": [1, 2, 3],
|
||||||
|
"anyItems": [1, 2, 3] as [Any],
|
||||||
|
"nsItems": NSArray(array: [1, 2, 3]),
|
||||||
"emptyItems": [Int](),
|
"emptyItems": [Int](),
|
||||||
"dict": [
|
"dict": [
|
||||||
"one": "I",
|
"one": "I",
|
||||||
"two": "II",
|
"two": "II"
|
||||||
],
|
],
|
||||||
"tuples": [(1, 2, 3), (4, 5, 6)]
|
"tuples": [(1, 2, 3), (4, 5, 6)]
|
||||||
])
|
])
|
||||||
|
|
||||||
$0.it("renders the given nodes for each item") {
|
func testForNode() {
|
||||||
|
it("renders the given nodes for each item") {
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||||
try expect(try node.render(context)) == "123"
|
try expect(try node.render(self.context)) == "123"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("renders the given empty nodes when no items found item") {
|
it("renders the given empty nodes when no items found item") {
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
let node = ForNode(
|
||||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
resolvable: Variable("emptyItems"),
|
||||||
let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes)
|
loopVariables: ["item"],
|
||||||
try expect(try node.render(context)) == "empty"
|
nodes: [VariableNode(variable: "item")],
|
||||||
|
emptyNodes: [TextNode(text: "empty")]
|
||||||
|
)
|
||||||
|
try expect(try node.render(self.context)) == "empty"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("renders a context variable of type Array<Any>") {
|
it("renders a context variable of type Array<Any>") {
|
||||||
let any_context = Context(dictionary: [
|
|
||||||
"items": ([1, 2, 3] as [Any])
|
|
||||||
])
|
|
||||||
|
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
let node = ForNode(resolvable: Variable("anyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||||
try expect(try node.render(any_context)) == "123"
|
try expect(try node.render(self.context)) == "123"
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("renders a context variable of type CountableClosedRange<Int>") {
|
|
||||||
let context = Context(dictionary: ["range": 1...3])
|
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
|
||||||
let node = ForNode(resolvable: Variable("range"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
|
||||||
|
|
||||||
try expect(try node.render(context)) == "123"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("renders a context variable of type CountableRange<Int>") {
|
|
||||||
let context = Context(dictionary: ["range": 1..<4])
|
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
|
||||||
let node = ForNode(resolvable: Variable("range"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
|
||||||
|
|
||||||
try expect(try node.render(context)) == "123"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(OSX)
|
#if os(OSX)
|
||||||
$0.it("renders a context variable of type NSArray") {
|
it("renders a context variable of type NSArray") {
|
||||||
let nsarray_context = Context(dictionary: [
|
|
||||||
"items": NSArray(array: [1, 2, 3])
|
|
||||||
])
|
|
||||||
|
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
let node = ForNode(resolvable: Variable("nsItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||||
try expect(try node.render(nsarray_context)) == "123"
|
try expect(try node.render(self.context)) == "123"
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
$0.it("renders the given nodes while providing if the item is first in the context") {
|
it("can render a filter with spaces") {
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.first")]
|
let template = Template(templateString: """
|
||||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
|
||||||
try expect(try node.render(context)) == "1true2false3false"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("renders the given nodes while providing if the item is last in the context") {
|
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.last")]
|
|
||||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
|
||||||
try expect(try node.render(context)) == "1false2false3true"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("renders the given nodes while providing item counter") {
|
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
|
|
||||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
|
||||||
try expect(try node.render(context)) == "112233"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("renders the given nodes while providing item counter") {
|
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter0")]
|
|
||||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
|
||||||
try expect(try node.render(context)) == "102132"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("renders the given nodes while providing loop length") {
|
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.length")]
|
|
||||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
|
||||||
try expect(try node.render(context)) == "132333"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("renders the given nodes while filtering items using where expression") {
|
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
|
|
||||||
let parser = TokenParser(tokens: [], environment: Environment())
|
|
||||||
let `where` = try parser.compileExpression(components: ["item", ">", "1"], token: .text(value: "", at: .unknown))
|
|
||||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`)
|
|
||||||
try expect(try node.render(context)) == "2132"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("renders the given empty nodes when all items filtered out with where expression") {
|
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
|
||||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
|
||||||
let parser = TokenParser(tokens: [], environment: Environment())
|
|
||||||
let `where` = try parser.compileExpression(components: ["item", "==", "0"], token: .text(value: "", at: .unknown))
|
|
||||||
let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`)
|
|
||||||
try expect(try node.render(context)) == "empty"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("can render a filter with spaces") {
|
|
||||||
let templateString = """
|
|
||||||
{% for article in ars | default: a, b , articles %}\
|
{% for article in ars | default: a, b , articles %}\
|
||||||
- {{ article.title }} by {{ article.author }}.
|
- {{ article.title }} by {{ article.author }}.
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
"""
|
""")
|
||||||
|
|
||||||
let context = Context(dictionary: [
|
let context = Context(dictionary: [
|
||||||
"articles": [
|
"articles": [
|
||||||
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
|
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
|
||||||
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
|
Article(title: "Memory Management with ARC", author: "Kyle Fuller")
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
|
|
||||||
let template = Template(templateString: templateString)
|
|
||||||
let result = try template.render(context)
|
let result = try template.render(context)
|
||||||
|
|
||||||
try expect(result) == """
|
try expect(result) == """
|
||||||
@@ -137,173 +66,264 @@ class ForNodeTests: XCTestCase {
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$0.context("given array of tuples") {
|
func testLoopMetadata() {
|
||||||
$0.it("can iterate over all tuple values") {
|
it("renders the given nodes while providing if the item is first in the context") {
|
||||||
let templateString = """
|
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.first")]
|
||||||
|
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||||
|
try expect(try node.render(self.context)) == "1true2false3false"
|
||||||
|
}
|
||||||
|
|
||||||
|
it("renders the given nodes while providing if the item is last in the context") {
|
||||||
|
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.last")]
|
||||||
|
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||||
|
try expect(try node.render(self.context)) == "1false2false3true"
|
||||||
|
}
|
||||||
|
|
||||||
|
it("renders the given nodes while providing item counter") {
|
||||||
|
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
|
||||||
|
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||||
|
try expect(try node.render(self.context)) == "112233"
|
||||||
|
}
|
||||||
|
|
||||||
|
it("renders the given nodes while providing item counter") {
|
||||||
|
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter0")]
|
||||||
|
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||||
|
try expect(try node.render(self.context)) == "102132"
|
||||||
|
}
|
||||||
|
|
||||||
|
it("renders the given nodes while providing loop length") {
|
||||||
|
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.length")]
|
||||||
|
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||||
|
try expect(try node.render(self.context)) == "132333"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWhereExpression() {
|
||||||
|
it("renders the given nodes while filtering items using where expression") {
|
||||||
|
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
|
||||||
|
let parser = TokenParser(tokens: [], environment: Environment())
|
||||||
|
let `where` = try parser.compileExpression(components: ["item", ">", "1"], token: .text(value: "", at: .unknown))
|
||||||
|
let node = ForNode(
|
||||||
|
resolvable: Variable("items"),
|
||||||
|
loopVariables: ["item"],
|
||||||
|
nodes: nodes,
|
||||||
|
emptyNodes: [],
|
||||||
|
where: `where`
|
||||||
|
)
|
||||||
|
try expect(try node.render(self.context)) == "2132"
|
||||||
|
}
|
||||||
|
|
||||||
|
it("renders the given empty nodes when all items filtered out with where expression") {
|
||||||
|
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||||
|
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||||
|
let parser = TokenParser(tokens: [], environment: Environment())
|
||||||
|
let `where` = try parser.compileExpression(components: ["item", "==", "0"], token: .text(value: "", at: .unknown))
|
||||||
|
let node = ForNode(
|
||||||
|
resolvable: Variable("emptyItems"),
|
||||||
|
loopVariables: ["item"],
|
||||||
|
nodes: nodes,
|
||||||
|
emptyNodes: emptyNodes,
|
||||||
|
where: `where`
|
||||||
|
)
|
||||||
|
try expect(try node.render(self.context)) == "empty"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testArrayOfTuples() {
|
||||||
|
it("can iterate over all tuple values") {
|
||||||
|
let template = Template(templateString: """
|
||||||
{% for first,second,third in tuples %}\
|
{% for first,second,third in tuples %}\
|
||||||
{{ first }}, {{ second }}, {{ third }}
|
{{ first }}, {{ second }}, {{ third }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
"""
|
""")
|
||||||
|
try expect(template.render(self.context)) == """
|
||||||
let template = Template(templateString: templateString)
|
|
||||||
let result = try template.render(context)
|
|
||||||
|
|
||||||
try expect(result) == """
|
|
||||||
1, 2, 3
|
1, 2, 3
|
||||||
4, 5, 6
|
4, 5, 6
|
||||||
|
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can iterate with less number of variables") {
|
it("can iterate with less number of variables") {
|
||||||
let templateString = """
|
let template = Template(templateString: """
|
||||||
{% for first,second in tuples %}\
|
{% for first,second in tuples %}\
|
||||||
{{ first }}, {{ second }}
|
{{ first }}, {{ second }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
"""
|
""")
|
||||||
|
try expect(template.render(self.context)) == """
|
||||||
let template = Template(templateString: templateString)
|
|
||||||
let result = try template.render(context)
|
|
||||||
|
|
||||||
try expect(result) == """
|
|
||||||
1, 2
|
1, 2
|
||||||
4, 5
|
4, 5
|
||||||
|
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can use _ to skip variables") {
|
it("can use _ to skip variables") {
|
||||||
let templateString = """
|
let template = Template(templateString: """
|
||||||
{% for first,_,third in tuples %}\
|
{% for first,_,third in tuples %}\
|
||||||
{{ first }}, {{ third }}
|
{{ first }}, {{ third }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
"""
|
""")
|
||||||
|
try expect(template.render(self.context)) == """
|
||||||
let template = Template(templateString: templateString)
|
|
||||||
let result = try template.render(context)
|
|
||||||
|
|
||||||
try expect(result) == """
|
|
||||||
1, 3
|
1, 3
|
||||||
4, 6
|
4, 6
|
||||||
|
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws when number of variables is more than number of tuple values") {
|
it("throws when number of variables is more than number of tuple values") {
|
||||||
let templateString = """
|
let template = Template(templateString: """
|
||||||
{% for key,value,smth in dict %}
|
{% for key,value,smth in dict %}{% endfor %}
|
||||||
{% endfor %}
|
""")
|
||||||
"""
|
try expect(template.render(self.context)).toThrow()
|
||||||
|
}
|
||||||
let template = Template(templateString: templateString)
|
|
||||||
try expect(template.render(context)).toThrow()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
func testIterateDictionary() {
|
||||||
|
it("can iterate over dictionary") {
|
||||||
$0.it("can iterate over dictionary") {
|
let template = Template(templateString: """
|
||||||
let templateString = """
|
|
||||||
{% for key, value in dict %}\
|
{% for key, value in dict %}\
|
||||||
{{ key }}: {{ value }},\
|
{{ key }}: {{ value }},\
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
"""
|
""")
|
||||||
|
try expect(template.render(self.context)) == """
|
||||||
let template = Template(templateString: templateString)
|
|
||||||
let result = try template.render(context)
|
|
||||||
|
|
||||||
try expect(result) == """
|
|
||||||
one: I,two: II,
|
one: I,two: II,
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("renders supports iterating over dictionary") {
|
it("renders supports iterating over dictionary") {
|
||||||
let nodes: [NodeType] = [
|
let nodes: [NodeType] = [
|
||||||
VariableNode(variable: "key"),
|
VariableNode(variable: "key"),
|
||||||
TextNode(text: ","),
|
TextNode(text: ",")
|
||||||
]
|
]
|
||||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||||
let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key"], nodes: nodes, emptyNodes: emptyNodes, where: nil)
|
let node = ForNode(
|
||||||
let result = try node.render(context)
|
resolvable: Variable("dict"),
|
||||||
|
loopVariables: ["key"],
|
||||||
|
nodes: nodes,
|
||||||
|
emptyNodes: emptyNodes
|
||||||
|
)
|
||||||
|
|
||||||
try expect(result) == """
|
try expect(node.render(self.context)) == """
|
||||||
one,two,
|
one,two,
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("renders supports iterating over dictionary") {
|
it("renders supports iterating over dictionary with values") {
|
||||||
let nodes: [NodeType] = [
|
let nodes: [NodeType] = [
|
||||||
VariableNode(variable: "key"),
|
VariableNode(variable: "key"),
|
||||||
TextNode(text: "="),
|
TextNode(text: "="),
|
||||||
VariableNode(variable: "value"),
|
VariableNode(variable: "value"),
|
||||||
TextNode(text: ","),
|
TextNode(text: ",")
|
||||||
]
|
]
|
||||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||||
let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key", "value"], nodes: nodes, emptyNodes: emptyNodes, where: nil)
|
let node = ForNode(
|
||||||
let result = try node.render(context)
|
resolvable: Variable("dict"),
|
||||||
|
loopVariables: ["key", "value"],
|
||||||
|
nodes: nodes,
|
||||||
|
emptyNodes: emptyNodes
|
||||||
|
)
|
||||||
|
|
||||||
try expect(result) == """
|
try expect(node.render(self.context)) == """
|
||||||
one=I,two=II,
|
one=I,two=II,
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("handles invalid input") {
|
|
||||||
let token = Token.block(value: "for i", at: .unknown)
|
|
||||||
let parser = TokenParser(tokens: [token], environment: Environment())
|
|
||||||
let error = TemplateSyntaxError(reason: "'for' statements should use the syntax: `for <x> in <y> [where <condition>]`.", token: token)
|
|
||||||
try expect(try parser.parse()).toThrow(error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can iterate over struct properties") {
|
func testIterateUsingMirroring() {
|
||||||
struct MyStruct {
|
|
||||||
let string: String
|
|
||||||
let number: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
let context = Context(dictionary: [
|
|
||||||
"struct": MyStruct(string: "abc", number: 123)
|
|
||||||
])
|
|
||||||
|
|
||||||
let nodes: [NodeType] = [
|
let nodes: [NodeType] = [
|
||||||
VariableNode(variable: "property"),
|
VariableNode(variable: "label"),
|
||||||
TextNode(text: "="),
|
TextNode(text: "="),
|
||||||
VariableNode(variable: "value"),
|
VariableNode(variable: "value"),
|
||||||
TextNode(text: "\n"),
|
TextNode(text: "\n")
|
||||||
]
|
]
|
||||||
let node = ForNode(resolvable: Variable("struct"), loopVariables: ["property", "value"], nodes: nodes, emptyNodes: [])
|
let node = ForNode(
|
||||||
let result = try node.render(context)
|
resolvable: Variable("item"),
|
||||||
|
loopVariables: ["label", "value"],
|
||||||
|
nodes: nodes,
|
||||||
|
emptyNodes: []
|
||||||
|
)
|
||||||
|
|
||||||
try expect(result) == """
|
it("can iterate over struct properties") {
|
||||||
|
let context = Context(dictionary: [
|
||||||
|
"item": MyStruct(string: "abc", number: 123)
|
||||||
|
])
|
||||||
|
try expect(node.render(context)) == """
|
||||||
string=abc
|
string=abc
|
||||||
number=123
|
number=123
|
||||||
|
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can iterate tuple items") {
|
it("can iterate tuple items") {
|
||||||
let context = Context(dictionary: [
|
let context = Context(dictionary: [
|
||||||
"tuple": (one: 1, two: "dva"),
|
"item": (one: 1, two: "dva")
|
||||||
])
|
])
|
||||||
|
try expect(node.render(context)) == """
|
||||||
let nodes: [NodeType] = [
|
|
||||||
VariableNode(variable: "label"),
|
|
||||||
TextNode(text: "="),
|
|
||||||
VariableNode(variable: "value"),
|
|
||||||
TextNode(text: "\n"),
|
|
||||||
]
|
|
||||||
|
|
||||||
let node = ForNode(resolvable: Variable("tuple"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: [])
|
|
||||||
let result = try node.render(context)
|
|
||||||
|
|
||||||
try expect(result) == """
|
|
||||||
one=1
|
one=1
|
||||||
two=dva
|
two=dva
|
||||||
|
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can iterate over class properties") {
|
it("can iterate over class properties") {
|
||||||
class MyClass {
|
let context = Context(dictionary: [
|
||||||
|
"item": MySubclass("child", "base", 1)
|
||||||
|
])
|
||||||
|
try expect(node.render(context)) == """
|
||||||
|
childString=child
|
||||||
|
baseString=base
|
||||||
|
baseInt=1
|
||||||
|
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIterateRange() {
|
||||||
|
it("renders a context variable of type CountableClosedRange<Int>") {
|
||||||
|
let context = Context(dictionary: ["range": 1...3])
|
||||||
|
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||||
|
let node = ForNode(resolvable: Variable("range"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||||
|
|
||||||
|
try expect(try node.render(context)) == "123"
|
||||||
|
}
|
||||||
|
|
||||||
|
it("renders a context variable of type CountableRange<Int>") {
|
||||||
|
let context = Context(dictionary: ["range": 1..<4])
|
||||||
|
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||||
|
let node = ForNode(resolvable: Variable("range"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||||
|
|
||||||
|
try expect(try node.render(context)) == "123"
|
||||||
|
}
|
||||||
|
|
||||||
|
it("can iterate in range of variables") {
|
||||||
|
let template: Template = "{% for i in 1...j %}{{ i }}{% endfor %}"
|
||||||
|
try expect(try template.render(Context(dictionary: ["j": 3]))) == "123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHandleInvalidInput() throws {
|
||||||
|
let token = Token.block(value: "for i", at: .unknown)
|
||||||
|
let parser = TokenParser(tokens: [token], environment: Environment())
|
||||||
|
let error = TemplateSyntaxError(
|
||||||
|
reason: "'for' statements should use the syntax: `for <x> in <y> [where <condition>]`.",
|
||||||
|
token: token
|
||||||
|
)
|
||||||
|
try expect(try parser.parse()).toThrow(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct MyStruct {
|
||||||
|
let string: String
|
||||||
|
let number: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Article {
|
||||||
|
let title: String
|
||||||
|
let author: String
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MyClass {
|
||||||
var baseString: String
|
var baseString: String
|
||||||
var baseInt: Int
|
var baseInt: Int
|
||||||
init(_ string: String, _ int: Int) {
|
init(_ string: String, _ int: Int) {
|
||||||
@@ -312,47 +332,10 @@ class ForNodeTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MySubclass: MyClass {
|
private class MySubclass: MyClass {
|
||||||
var childString: String
|
var childString: String
|
||||||
init(_ childString: String, _ string: String, _ int: Int) {
|
init(_ childString: String, _ string: String, _ int: Int) {
|
||||||
self.childString = childString
|
self.childString = childString
|
||||||
super.init(string, int)
|
super.init(string, int)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let context = Context(dictionary: [
|
|
||||||
"class": MySubclass("child", "base", 1)
|
|
||||||
])
|
|
||||||
|
|
||||||
let nodes: [NodeType] = [
|
|
||||||
VariableNode(variable: "label"),
|
|
||||||
TextNode(text: "="),
|
|
||||||
VariableNode(variable: "value"),
|
|
||||||
TextNode(text: "\n"),
|
|
||||||
]
|
|
||||||
|
|
||||||
let node = ForNode(resolvable: Variable("class"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: [])
|
|
||||||
let result = try node.render(context)
|
|
||||||
|
|
||||||
try expect(result) == """
|
|
||||||
childString=child
|
|
||||||
baseString=base
|
|
||||||
baseInt=1
|
|
||||||
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("can iterate in range of variables") {
|
|
||||||
let template: Template = "{% for i in 1...j %}{{ i }}{% endfor %}"
|
|
||||||
try expect(try template.render(Context(dictionary: ["j": 3]))) == "123"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate struct Article {
|
|
||||||
let title: String
|
|
||||||
let author: String
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import XCTest
|
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
|
import XCTest
|
||||||
|
|
||||||
class IfNodeTests: XCTestCase {
|
private struct SomeType {
|
||||||
func testIfNode() {
|
let value: String? = nil
|
||||||
describe("IfNode") {
|
}
|
||||||
$0.describe("parsing") {
|
|
||||||
$0.it("can parse an if block") {
|
final class IfNodeTests: XCTestCase {
|
||||||
|
func testParseIf() {
|
||||||
|
it("can parse an if block") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value", at: .unknown),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true", at: .unknown),
|
.text(value: "true", at: .unknown),
|
||||||
@@ -24,7 +26,22 @@ class IfNodeTests: XCTestCase {
|
|||||||
try expect(trueNode?.text) == "true"
|
try expect(trueNode?.text) == "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can parse an if with else block") {
|
it("can parse an if with complex expression") {
|
||||||
|
let tokens: [Token] = [
|
||||||
|
.block(value: """
|
||||||
|
if value == \"test\" and (not name or not (name and surname) or( some )and other )
|
||||||
|
""", at: .unknown),
|
||||||
|
.text(value: "true", at: .unknown),
|
||||||
|
.block(value: "endif", at: .unknown)
|
||||||
|
]
|
||||||
|
|
||||||
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
|
let nodes = try parser.parse()
|
||||||
|
try expect(nodes.first is IfNode).beTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testParseIfWithElse() throws {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value", at: .unknown),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true", at: .unknown),
|
.text(value: "true", at: .unknown),
|
||||||
@@ -49,7 +66,7 @@ class IfNodeTests: XCTestCase {
|
|||||||
try expect(falseNode?.text) == "false"
|
try expect(falseNode?.text) == "false"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can parse an if with elif block") {
|
func testParseIfWithElif() throws {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value", at: .unknown),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true", at: .unknown),
|
.text(value: "true", at: .unknown),
|
||||||
@@ -80,7 +97,7 @@ class IfNodeTests: XCTestCase {
|
|||||||
try expect(falseNode?.text) == "false"
|
try expect(falseNode?.text) == "false"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can parse an if with elif block without else") {
|
func testParseIfWithElifWithoutElse() throws {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value", at: .unknown),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true", at: .unknown),
|
.text(value: "true", at: .unknown),
|
||||||
@@ -105,7 +122,7 @@ class IfNodeTests: XCTestCase {
|
|||||||
try expect(elifNode?.text) == "some"
|
try expect(elifNode?.text) == "some"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can parse an if with multiple elif block") {
|
func testParseMultipleElif() throws {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value", at: .unknown),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true", at: .unknown),
|
.text(value: "true", at: .unknown),
|
||||||
@@ -142,20 +159,7 @@ class IfNodeTests: XCTestCase {
|
|||||||
try expect(falseNode?.text) == "false"
|
try expect(falseNode?.text) == "false"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testParseIfnot() throws {
|
||||||
$0.it("can parse an if with complex expression") {
|
|
||||||
let tokens: [Token] = [
|
|
||||||
.block(value: "if value == \"test\" and (not name or not (name and surname) or( some )and other )", at: .unknown),
|
|
||||||
.text(value: "true", at: .unknown),
|
|
||||||
.block(value: "endif", at: .unknown)
|
|
||||||
]
|
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
|
||||||
let nodes = try parser.parse()
|
|
||||||
try expect(nodes.first is IfNode).beTrue()
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("can parse an ifnot block") {
|
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "ifnot value", at: .unknown),
|
.block(value: "ifnot value", at: .unknown),
|
||||||
.text(value: "false", at: .unknown),
|
.text(value: "false", at: .unknown),
|
||||||
@@ -179,7 +183,8 @@ class IfNodeTests: XCTestCase {
|
|||||||
try expect(falseNode?.text) == "false"
|
try expect(falseNode?.text) == "false"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws an error when parsing an if block without an endif") {
|
func testParsingErrors() {
|
||||||
|
it("throws an error when parsing an if block without an endif") {
|
||||||
let tokens: [Token] = [.block(value: "if value", at: .unknown)]
|
let tokens: [Token] = [.block(value: "if value", at: .unknown)]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -187,7 +192,7 @@ class IfNodeTests: XCTestCase {
|
|||||||
try expect(try parser.parse()).toThrow(error)
|
try expect(try parser.parse()).toThrow(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws an error when parsing an ifnot without an endif") {
|
it("throws an error when parsing an ifnot without an endif") {
|
||||||
let tokens: [Token] = [.block(value: "ifnot value", at: .unknown)]
|
let tokens: [Token] = [.block(value: "ifnot value", at: .unknown)]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -196,48 +201,48 @@ class IfNodeTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("rendering") {
|
func testRendering() {
|
||||||
$0.it("renders a true expression") {
|
it("renders a true expression") {
|
||||||
let node = IfNode(conditions: [
|
let node = IfNode(conditions: [
|
||||||
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "1")]),
|
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "1")]),
|
||||||
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]),
|
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]),
|
||||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")]),
|
IfCondition(expression: nil, nodes: [TextNode(text: "3")])
|
||||||
])
|
])
|
||||||
|
|
||||||
try expect(try node.render(Context())) == "1"
|
try expect(try node.render(Context())) == "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("renders the first true expression") {
|
it("renders the first true expression") {
|
||||||
let node = IfNode(conditions: [
|
let node = IfNode(conditions: [
|
||||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
||||||
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]),
|
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]),
|
||||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")]),
|
IfCondition(expression: nil, nodes: [TextNode(text: "3")])
|
||||||
])
|
])
|
||||||
|
|
||||||
try expect(try node.render(Context())) == "2"
|
try expect(try node.render(Context())) == "2"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("renders the empty expression when other conditions are falsy") {
|
it("renders the empty expression when other conditions are falsy") {
|
||||||
let node = IfNode(conditions: [
|
let node = IfNode(conditions: [
|
||||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
||||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]),
|
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]),
|
||||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")]),
|
IfCondition(expression: nil, nodes: [TextNode(text: "3")])
|
||||||
])
|
])
|
||||||
|
|
||||||
try expect(try node.render(Context())) == "3"
|
try expect(try node.render(Context())) == "3"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("renders empty when no truthy conditions") {
|
it("renders empty when no truthy conditions") {
|
||||||
let node = IfNode(conditions: [
|
let node = IfNode(conditions: [
|
||||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
||||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]),
|
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")])
|
||||||
])
|
])
|
||||||
|
|
||||||
try expect(try node.render(Context())) == ""
|
try expect(try node.render(Context())) == ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("supports variable filters in the if expression") {
|
func testSupportVariableFilters() throws {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value|uppercase == \"TEST\"", at: .unknown),
|
.block(value: "if value|uppercase == \"TEST\"", at: .unknown),
|
||||||
.text(value: "true", at: .unknown),
|
.text(value: "true", at: .unknown),
|
||||||
@@ -251,7 +256,7 @@ class IfNodeTests: XCTestCase {
|
|||||||
try expect(result) == "true"
|
try expect(result) == "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates nil properties as false") {
|
func testEvaluatesNilAsFalse() throws {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if instance.value", at: .unknown),
|
.block(value: "if instance.value", at: .unknown),
|
||||||
.text(value: "true", at: .unknown),
|
.text(value: "true", at: .unknown),
|
||||||
@@ -261,14 +266,11 @@ class IfNodeTests: XCTestCase {
|
|||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
|
|
||||||
struct SomeType {
|
|
||||||
let value: String? = nil
|
|
||||||
}
|
|
||||||
let result = try renderNodes(nodes, Context(dictionary: ["instance": SomeType()]))
|
let result = try renderNodes(nodes, Context(dictionary: ["instance": SomeType()]))
|
||||||
try expect(result) == ""
|
try expect(result) == ""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("supports closed range variables") {
|
func testSupportsRangeVariables() throws {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value in 1...3", at: .unknown),
|
.block(value: "if value in 1...3", at: .unknown),
|
||||||
.text(value: "true", at: .unknown),
|
.text(value: "true", at: .unknown),
|
||||||
@@ -283,7 +285,4 @@ class IfNodeTests: XCTestCase {
|
|||||||
try expect(renderNodes(nodes, Context(dictionary: ["value": 3]))) == "true"
|
try expect(renderNodes(nodes, Context(dictionary: ["value": 3]))) == "true"
|
||||||
try expect(renderNodes(nodes, Context(dictionary: ["value": 4]))) == "false"
|
try expect(renderNodes(nodes, Context(dictionary: ["value": 4]))) == "false"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
import XCTest
|
import PathKit
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
import PathKit
|
import XCTest
|
||||||
|
|
||||||
class IncludeTests: XCTestCase {
|
final class IncludeTests: XCTestCase {
|
||||||
func testInclude() {
|
|
||||||
describe("Include") {
|
|
||||||
let path = Path(#file) + ".." + "fixtures"
|
let path = Path(#file) + ".." + "fixtures"
|
||||||
let loader = FileSystemLoader(paths: [path])
|
lazy var loader = FileSystemLoader(paths: [path])
|
||||||
let environment = Environment(loader: loader)
|
lazy var environment = Environment(loader: loader)
|
||||||
|
|
||||||
$0.describe("parsing") {
|
func testParsing() {
|
||||||
$0.it("throws an error when no template is given") {
|
it("throws an error when no template is given") {
|
||||||
let tokens: [Token] = [ .block(value: "include", at: .unknown) ]
|
let tokens: [Token] = [ .block(value: "include", at: .unknown) ]
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
|
|
||||||
let error = TemplateSyntaxError(reason: "'include' tag requires one argument, the template file to be included. A second optional argument can be used to specify the context that will be passed to the included file", token: tokens.first)
|
let error = TemplateSyntaxError(reason: """
|
||||||
|
'include' tag requires one argument, the template file to be included. \
|
||||||
|
A second optional argument can be used to specify the context that will \
|
||||||
|
be passed to the included file
|
||||||
|
""", token: tokens.first)
|
||||||
try expect(try parser.parse()).toThrow(error)
|
try expect(try parser.parse()).toThrow(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can parse a valid include block") {
|
it("can parse a valid include block") {
|
||||||
let tokens: [Token] = [ .block(value: "include \"test.html\"", at: .unknown) ]
|
let tokens: [Token] = [ .block(value: "include \"test.html\"", at: .unknown) ]
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
|
|
||||||
@@ -30,8 +32,8 @@ class IncludeTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("rendering") {
|
func testRendering() {
|
||||||
$0.it("throws an error when rendering without a loader") {
|
it("throws an error when rendering without a loader") {
|
||||||
let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown))
|
let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown))
|
||||||
|
|
||||||
do {
|
do {
|
||||||
@@ -41,32 +43,30 @@ class IncludeTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws an error when it cannot find the included template") {
|
it("throws an error when it cannot find the included template") {
|
||||||
let node = IncludeNode(templateName: Variable("\"unknown.html\""), token: .block(value: "", at: .unknown))
|
let node = IncludeNode(templateName: Variable("\"unknown.html\""), token: .block(value: "", at: .unknown))
|
||||||
|
|
||||||
do {
|
do {
|
||||||
_ = try node.render(Context(environment: environment))
|
_ = try node.render(Context(environment: self.environment))
|
||||||
} catch {
|
} catch {
|
||||||
try expect("\(error)".hasPrefix("Template named `unknown.html` does not exist in loader")).to.beTrue()
|
try expect("\(error)".hasPrefix("Template named `unknown.html` does not exist in loader")).to.beTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("successfully renders a found included template") {
|
it("successfully renders a found included template") {
|
||||||
let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown))
|
let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown))
|
||||||
let context = Context(dictionary: ["target": "World"], environment: environment)
|
let context = Context(dictionary: ["target": "World"], environment: self.environment)
|
||||||
let value = try node.render(context)
|
let value = try node.render(context)
|
||||||
try expect(value) == "Hello World!"
|
try expect(value) == "Hello World!"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("successfully passes context") {
|
it("successfully passes context") {
|
||||||
let template = Template(templateString: """
|
let template = Template(templateString: """
|
||||||
{% include "test.html" child %}
|
{% include "test.html" child %}
|
||||||
""")
|
""")
|
||||||
let context = Context(dictionary: ["child": ["target": "World"]], environment: environment)
|
let context = Context(dictionary: ["child": ["target": "World"]], environment: self.environment)
|
||||||
let value = try template.render(context)
|
let value = try template.render(context)
|
||||||
try expect(value) == "Hello World!"
|
try expect(value) == "Hello World!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
36
Tests/StencilTests/InheritanceSpec.swift
Normal file
36
Tests/StencilTests/InheritanceSpec.swift
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import PathKit
|
||||||
|
import Spectre
|
||||||
|
import Stencil
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class InheritanceTests: XCTestCase {
|
||||||
|
let path = Path(#file) + ".." + "fixtures"
|
||||||
|
lazy var loader = FileSystemLoader(paths: [path])
|
||||||
|
lazy var environment = Environment(loader: loader)
|
||||||
|
|
||||||
|
func testInheritance() {
|
||||||
|
it("can inherit from another template") {
|
||||||
|
let template = try self.environment.loadTemplate(name: "child.html")
|
||||||
|
try expect(try template.render()) == """
|
||||||
|
Super_Header Child_Header
|
||||||
|
Child_Body
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
it("can inherit from another template inheriting from another template") {
|
||||||
|
let template = try self.environment.loadTemplate(name: "child-child.html")
|
||||||
|
try expect(try template.render()) == """
|
||||||
|
Super_Header Child_Header Child_Child_Header
|
||||||
|
Child_Body
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
it("can inherit from a template that calls a super block") {
|
||||||
|
let template = try self.environment.loadTemplate(name: "child-super.html")
|
||||||
|
try expect(try template.render()) == """
|
||||||
|
Header
|
||||||
|
Child_Body
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import XCTest
|
|
||||||
import Spectre
|
|
||||||
import Stencil
|
|
||||||
import PathKit
|
|
||||||
|
|
||||||
class InheritenceTests: XCTestCase {
|
|
||||||
func testInheritence() {
|
|
||||||
describe("Inheritence") {
|
|
||||||
let path = Path(#file) + ".." + "fixtures"
|
|
||||||
let loader = FileSystemLoader(paths: [path])
|
|
||||||
let environment = Environment(loader: loader)
|
|
||||||
|
|
||||||
$0.it("can inherit from another template") {
|
|
||||||
let template = try environment.loadTemplate(name: "child.html")
|
|
||||||
try expect(try template.render()) == """
|
|
||||||
Super_Header Child_Header
|
|
||||||
Child_Body
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("can inherit from another template inheriting from another template") {
|
|
||||||
let template = try environment.loadTemplate(name: "child-child.html")
|
|
||||||
try expect(try template.render()) == """
|
|
||||||
Super_Header Child_Header Child_Child_Header
|
|
||||||
Child_Body
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("can inherit from a template that calls a super block") {
|
|
||||||
let template = try environment.loadTemplate(name: "child-super.html")
|
|
||||||
try expect(try template.render()) == """
|
|
||||||
Header
|
|
||||||
Child_Body
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,15 +3,8 @@ import Spectre
|
|||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
class LexerTests: XCTestCase {
|
final class LexerTests: XCTestCase {
|
||||||
func testLexer() {
|
func testText() throws {
|
||||||
describe("Lexer") {
|
|
||||||
func makeSourceMap(_ token: String, for lexer: Lexer, options: String.CompareOptions = []) -> SourceMap {
|
|
||||||
guard let range = lexer.templateString.range(of: token, options: options) else { fatalError("Token not found") }
|
|
||||||
return SourceMap(location: lexer.rangeLocation(range))
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("can tokenize text") {
|
|
||||||
let lexer = Lexer(templateString: "Hello World")
|
let lexer = Lexer(templateString: "Hello World")
|
||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
@@ -19,7 +12,7 @@ class LexerTests: XCTestCase {
|
|||||||
try expect(tokens.first) == .text(value: "Hello World", at: makeSourceMap("Hello World", for: lexer))
|
try expect(tokens.first) == .text(value: "Hello World", at: makeSourceMap("Hello World", for: lexer))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize a comment") {
|
func testComment() throws {
|
||||||
let lexer = Lexer(templateString: "{# Comment #}")
|
let lexer = Lexer(templateString: "{# Comment #}")
|
||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
@@ -27,7 +20,7 @@ class LexerTests: XCTestCase {
|
|||||||
try expect(tokens.first) == .comment(value: "Comment", at: makeSourceMap("Comment", for: lexer))
|
try expect(tokens.first) == .comment(value: "Comment", at: makeSourceMap("Comment", for: lexer))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize a variable") {
|
func testVariable() throws {
|
||||||
let lexer = Lexer(templateString: "{{ Variable }}")
|
let lexer = Lexer(templateString: "{{ Variable }}")
|
||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
@@ -35,7 +28,7 @@ class LexerTests: XCTestCase {
|
|||||||
try expect(tokens.first) == .variable(value: "Variable", at: makeSourceMap("Variable", for: lexer))
|
try expect(tokens.first) == .variable(value: "Variable", at: makeSourceMap("Variable", for: lexer))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize a token without spaces") {
|
func testTokenWithoutSpaces() throws {
|
||||||
let lexer = Lexer(templateString: "{{Variable}}")
|
let lexer = Lexer(templateString: "{{Variable}}")
|
||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
@@ -43,7 +36,7 @@ class LexerTests: XCTestCase {
|
|||||||
try expect(tokens.first) == .variable(value: "Variable", at: makeSourceMap("Variable", for: lexer))
|
try expect(tokens.first) == .variable(value: "Variable", at: makeSourceMap("Variable", for: lexer))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize unclosed tag by ignoring it") {
|
func testUnclosedTag() throws {
|
||||||
let templateString = "{{ thing"
|
let templateString = "{{ thing"
|
||||||
let lexer = Lexer(templateString: templateString)
|
let lexer = Lexer(templateString: templateString)
|
||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
@@ -52,7 +45,7 @@ class LexerTests: XCTestCase {
|
|||||||
try expect(tokens.first) == .text(value: "", at: makeSourceMap("{{ thing", for: lexer))
|
try expect(tokens.first) == .text(value: "", at: makeSourceMap("{{ thing", for: lexer))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize a mixture of content") {
|
func testContentMixture() throws {
|
||||||
let templateString = "My name is {{ myname }}."
|
let templateString = "My name is {{ myname }}."
|
||||||
let lexer = Lexer(templateString: templateString)
|
let lexer = Lexer(templateString: templateString)
|
||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
@@ -63,7 +56,7 @@ class LexerTests: XCTestCase {
|
|||||||
try expect(tokens[2]) == Token.text(value: ".", at: makeSourceMap(".", for: lexer))
|
try expect(tokens[2]) == Token.text(value: ".", at: makeSourceMap(".", for: lexer))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize two variables without being greedy") {
|
func testVariablesWithoutBeingGreedy() throws {
|
||||||
let templateString = "{{ thing }}{{ name }}"
|
let templateString = "{{ thing }}{{ name }}"
|
||||||
let lexer = Lexer(templateString: templateString)
|
let lexer = Lexer(templateString: templateString)
|
||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
@@ -73,22 +66,22 @@ class LexerTests: XCTestCase {
|
|||||||
try expect(tokens[1]) == Token.variable(value: "name", at: makeSourceMap("name", for: lexer))
|
try expect(tokens[1]) == Token.variable(value: "name", at: makeSourceMap("name", for: lexer))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize an unclosed block") {
|
func testUnclosedBlock() throws {
|
||||||
let lexer = Lexer(templateString: "{%}")
|
let lexer = Lexer(templateString: "{%}")
|
||||||
_ = lexer.tokenize()
|
_ = lexer.tokenize()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize incorrect syntax without crashing") {
|
func testTokenizeIncorrectSyntaxWithoutCrashing() throws {
|
||||||
let lexer = Lexer(templateString: "func some() {{% if %}")
|
let lexer = Lexer(templateString: "func some() {{% if %}")
|
||||||
_ = lexer.tokenize()
|
_ = lexer.tokenize()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize an empty variable") {
|
func testEmptyVariable() throws {
|
||||||
let lexer = Lexer(templateString: "{{}}")
|
let lexer = Lexer(templateString: "{{}}")
|
||||||
_ = lexer.tokenize()
|
_ = lexer.tokenize()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize with new lines") {
|
func testNewlines() throws {
|
||||||
let templateString = """
|
let templateString = """
|
||||||
My name is {%
|
My name is {%
|
||||||
if name
|
if name
|
||||||
@@ -110,7 +103,7 @@ class LexerTests: XCTestCase {
|
|||||||
try expect(tokens[4]) == Token.text(value: ".", at: makeSourceMap(".", for: lexer))
|
try expect(tokens[4]) == Token.text(value: ".", at: makeSourceMap(".", for: lexer))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize escape sequences") {
|
func testEscapeSequence() throws {
|
||||||
let templateString = "class Some {{ '{' }}{% if true %}{{ stuff }}{% endif %}"
|
let templateString = "class Some {{ '{' }}{% if true %}{{ stuff }}{% endif %}"
|
||||||
let lexer = Lexer(templateString: templateString)
|
let lexer = Lexer(templateString: templateString)
|
||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
@@ -122,8 +115,6 @@ class LexerTests: XCTestCase {
|
|||||||
try expect(tokens[3]) == Token.variable(value: "stuff", at: makeSourceMap("stuff", for: lexer))
|
try expect(tokens[3]) == Token.variable(value: "stuff", at: makeSourceMap("stuff", for: lexer))
|
||||||
try expect(tokens[4]) == Token.block(value: "endif", at: makeSourceMap("endif", for: lexer))
|
try expect(tokens[4]) == Token.block(value: "endif", at: makeSourceMap("endif", for: lexer))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPerformance() throws {
|
func testPerformance() throws {
|
||||||
let path = Path(#file) + ".." + "fixtures" + "huge.html"
|
let path = Path(#file) + ".." + "fixtures" + "huge.html"
|
||||||
@@ -134,4 +125,9 @@ class LexerTests: XCTestCase {
|
|||||||
_ = lexer.tokenize()
|
_ = lexer.tokenize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func makeSourceMap(_ token: String, for lexer: Lexer, options: String.CompareOptions = []) -> SourceMap {
|
||||||
|
guard let range = lexer.templateString.range(of: token, options: options) else { fatalError("Token not found") }
|
||||||
|
return SourceMap(location: lexer.rangeLocation(range))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,55 @@
|
|||||||
import XCTest
|
import PathKit
|
||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
import Stencil
|
||||||
import PathKit
|
import XCTest
|
||||||
|
|
||||||
class TemplateLoaderTests: XCTestCase {
|
final class TemplateLoaderTests: XCTestCase {
|
||||||
func testTemplateLoader() {
|
func testFileSystemLoader() {
|
||||||
describe("FileSystemLoader") {
|
|
||||||
let path = Path(#file) + ".." + "fixtures"
|
let path = Path(#file) + ".." + "fixtures"
|
||||||
let loader = FileSystemLoader(paths: [path])
|
let loader = FileSystemLoader(paths: [path])
|
||||||
let environment = Environment(loader: loader)
|
let environment = Environment(loader: loader)
|
||||||
|
|
||||||
$0.it("errors when a template cannot be found") {
|
it("errors when a template cannot be found") {
|
||||||
try expect(try environment.loadTemplate(name: "unknown.html")).toThrow()
|
try expect(try environment.loadTemplate(name: "unknown.html")).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("errors when an array of templates cannot be found") {
|
it("errors when an array of templates cannot be found") {
|
||||||
try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow()
|
try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can load a template from a file") {
|
it("can load a template from a file") {
|
||||||
_ = try environment.loadTemplate(name: "test.html")
|
_ = try environment.loadTemplate(name: "test.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("errors when loading absolute file outside of the selected path") {
|
it("errors when loading absolute file outside of the selected path") {
|
||||||
try expect(try environment.loadTemplate(name: "/etc/hosts")).toThrow()
|
try expect(try environment.loadTemplate(name: "/etc/hosts")).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("errors when loading relative file outside of the selected path") {
|
it("errors when loading relative file outside of the selected path") {
|
||||||
try expect(try environment.loadTemplate(name: "../LoaderSpec.swift")).toThrow()
|
try expect(try environment.loadTemplate(name: "../LoaderSpec.swift")).toThrow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("DictionaryLoader") {
|
func testDictionaryLoader() {
|
||||||
let loader = DictionaryLoader(templates: [
|
let loader = DictionaryLoader(templates: [
|
||||||
"index.html": "Hello World"
|
"index.html": "Hello World"
|
||||||
])
|
])
|
||||||
let environment = Environment(loader: loader)
|
let environment = Environment(loader: loader)
|
||||||
|
|
||||||
$0.it("errors when a template cannot be found") {
|
it("errors when a template cannot be found") {
|
||||||
try expect(try environment.loadTemplate(name: "unknown.html")).toThrow()
|
try expect(try environment.loadTemplate(name: "unknown.html")).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("errors when an array of templates cannot be found") {
|
it("errors when an array of templates cannot be found") {
|
||||||
try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow()
|
try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can load a template from a known templates") {
|
it("can load a template from a known templates") {
|
||||||
_ = try environment.loadTemplate(name: "index.html")
|
_ = try environment.loadTemplate(name: "index.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can load a known template from a collection of templates") {
|
it("can load a known template from a collection of templates") {
|
||||||
_ = try environment.loadTemplate(names: ["unknown.html", "index.html"])
|
_ = try environment.loadTemplate(names: ["unknown.html", "index.html"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import XCTest
|
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
|
import XCTest
|
||||||
|
|
||||||
class ErrorNode: NodeType {
|
class ErrorNode: NodeType {
|
||||||
let token: Token?
|
let token: Token?
|
||||||
@@ -13,54 +13,50 @@ class ErrorNode : NodeType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NodeTests: XCTestCase {
|
final class NodeTests: XCTestCase {
|
||||||
func testNode() {
|
|
||||||
describe("Node") {
|
|
||||||
let context = Context(dictionary: [
|
let context = Context(dictionary: [
|
||||||
"name": "Kyle",
|
"name": "Kyle",
|
||||||
"age": 27,
|
"age": 27,
|
||||||
"items": [1, 2, 3],
|
"items": [1, 2, 3]
|
||||||
])
|
])
|
||||||
|
|
||||||
$0.describe("TextNode") {
|
func testTextNode() {
|
||||||
$0.it("renders the given text") {
|
it("renders the given text") {
|
||||||
let node = TextNode(text: "Hello World")
|
let node = TextNode(text: "Hello World")
|
||||||
try expect(try node.render(context)) == "Hello World"
|
try expect(try node.render(self.context)) == "Hello World"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("VariableNode") {
|
func testVariableNode() {
|
||||||
$0.it("resolves and renders the variable") {
|
it("resolves and renders the variable") {
|
||||||
let node = VariableNode(variable: Variable("name"))
|
let node = VariableNode(variable: Variable("name"))
|
||||||
try expect(try node.render(context)) == "Kyle"
|
try expect(try node.render(self.context)) == "Kyle"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("resolves and renders a non string variable") {
|
it("resolves and renders a non string variable") {
|
||||||
let node = VariableNode(variable: Variable("age"))
|
let node = VariableNode(variable: Variable("age"))
|
||||||
try expect(try node.render(context)) == "27"
|
try expect(try node.render(self.context)) == "27"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("rendering nodes") {
|
func testRendering() {
|
||||||
$0.it("renders the nodes") {
|
it("renders the nodes") {
|
||||||
|
let nodes: [NodeType] = [
|
||||||
|
TextNode(text: "Hello "),
|
||||||
|
VariableNode(variable: "name")
|
||||||
|
]
|
||||||
|
|
||||||
|
try expect(try renderNodes(nodes, self.context)) == "Hello Kyle"
|
||||||
|
}
|
||||||
|
|
||||||
|
it("correctly throws a nodes failure") {
|
||||||
let nodes: [NodeType] = [
|
let nodes: [NodeType] = [
|
||||||
TextNode(text: "Hello "),
|
TextNode(text: "Hello "),
|
||||||
VariableNode(variable: "name"),
|
VariableNode(variable: "name"),
|
||||||
|
ErrorNode()
|
||||||
]
|
]
|
||||||
|
|
||||||
try expect(try renderNodes(nodes, context)) == "Hello Kyle"
|
try expect(try renderNodes(nodes, self.context)).toThrow(TemplateSyntaxError("Custom Error"))
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("correctly throws a nodes failure") {
|
|
||||||
let nodes: [NodeType] = [
|
|
||||||
TextNode(text:"Hello "),
|
|
||||||
VariableNode(variable: "name"),
|
|
||||||
ErrorNode(),
|
|
||||||
]
|
|
||||||
|
|
||||||
try expect(try renderNodes(nodes, context)).toThrow(TemplateSyntaxError("Custom Error"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import XCTest
|
|
||||||
import Foundation
|
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class NowNodeTests: XCTestCase {
|
||||||
class NowNodeTests: XCTestCase {
|
func testParsing() {
|
||||||
func testNowNode() {
|
it("parses default format without any now arguments") {
|
||||||
#if !os(Linux)
|
#if os(Linux)
|
||||||
describe("NowNode") {
|
throw skip()
|
||||||
$0.describe("parsing") {
|
#else
|
||||||
$0.it("parses default format without any now arguments") {
|
|
||||||
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())
|
||||||
|
|
||||||
@@ -17,20 +15,28 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("parses now with a format") {
|
it("parses now with a format") {
|
||||||
|
#if os(Linux)
|
||||||
|
throw skip()
|
||||||
|
#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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("rendering") {
|
func testRendering() {
|
||||||
$0.it("renders the date") {
|
it("renders the date") {
|
||||||
|
#if os(Linux)
|
||||||
|
throw skip()
|
||||||
|
#else
|
||||||
let node = NowNode(format: Variable("\"yyyy-MM-dd\""))
|
let node = NowNode(format: Variable("\"yyyy-MM-dd\""))
|
||||||
|
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
@@ -38,9 +44,7 @@ class NowNodeTests: XCTestCase {
|
|||||||
let date = formatter.string(from: NSDate() as Date)
|
let date = formatter.string(from: NSDate() as Date)
|
||||||
|
|
||||||
try expect(try node.render(Context())) == date
|
try expect(try node.render(Context())) == date
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import XCTest
|
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
|
import XCTest
|
||||||
|
|
||||||
class TokenParserTests: XCTestCase {
|
final class TokenParserTests: XCTestCase {
|
||||||
func testTokenParser() {
|
func testTokenParser() {
|
||||||
describe("TokenParser") {
|
it("can parse a text token") {
|
||||||
$0.it("can parse a text token") {
|
|
||||||
let parser = TokenParser(tokens: [
|
let parser = TokenParser(tokens: [
|
||||||
.text(value: "Hello World", at: .unknown)
|
.text(value: "Hello World", at: .unknown)
|
||||||
], environment: Environment())
|
], environment: Environment())
|
||||||
@@ -17,7 +16,7 @@ class TokenParserTests: XCTestCase {
|
|||||||
try expect(node?.text) == "Hello World"
|
try expect(node?.text) == "Hello World"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can parse a variable token") {
|
it("can parse a variable token") {
|
||||||
let parser = TokenParser(tokens: [
|
let parser = TokenParser(tokens: [
|
||||||
.variable(value: "'name'", at: .unknown)
|
.variable(value: "'name'", at: .unknown)
|
||||||
], environment: Environment())
|
], environment: Environment())
|
||||||
@@ -29,7 +28,7 @@ class TokenParserTests: XCTestCase {
|
|||||||
try expect(result) == "name"
|
try expect(result) == "name"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can parse a comment token") {
|
it("can parse a comment token") {
|
||||||
let parser = TokenParser(tokens: [
|
let parser = TokenParser(tokens: [
|
||||||
.comment(value: "Secret stuff!", at: .unknown)
|
.comment(value: "Secret stuff!", at: .unknown)
|
||||||
], environment: Environment())
|
], environment: Environment())
|
||||||
@@ -38,26 +37,28 @@ class TokenParserTests: XCTestCase {
|
|||||||
try expect(nodes.count) == 0
|
try expect(nodes.count) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can parse a tag token") {
|
it("can parse a tag token") {
|
||||||
let simpleExtension = Extension()
|
let simpleExtension = Extension()
|
||||||
simpleExtension.registerSimpleTag("known") { _ in
|
simpleExtension.registerSimpleTag("known") { _ in
|
||||||
return ""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
let parser = TokenParser(tokens: [
|
let parser = TokenParser(tokens: [
|
||||||
.block(value: "known", at: .unknown),
|
.block(value: "known", at: .unknown)
|
||||||
], environment: Environment(extensions: [simpleExtension]))
|
], environment: Environment(extensions: [simpleExtension]))
|
||||||
|
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
try expect(nodes.count) == 1
|
try expect(nodes.count) == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("errors when parsing an unknown tag") {
|
it("errors when parsing an unknown tag") {
|
||||||
let tokens: [Token] = [.block(value: "unknown", at: .unknown)]
|
let tokens: [Token] = [.block(value: "unknown", at: .unknown)]
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
|
|
||||||
try expect(try parser.parse()).toThrow(TemplateSyntaxError(reason: "Unknown template tag 'unknown'", token: tokens.first))
|
try expect(try parser.parse()).toThrow(TemplateSyntaxError(
|
||||||
}
|
reason: "Unknown template tag 'unknown'",
|
||||||
|
token: tokens.first)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,33 @@
|
|||||||
import XCTest
|
|
||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
import Stencil
|
||||||
|
import XCTest
|
||||||
|
|
||||||
fileprivate struct CustomNode : NodeType {
|
private struct CustomNode: NodeType {
|
||||||
let token: Token?
|
let token: Token?
|
||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
return "Hello World"
|
return "Hello World"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate struct Article {
|
private struct Article {
|
||||||
let title: String
|
let title: String
|
||||||
let author: String
|
let author: String
|
||||||
}
|
}
|
||||||
|
|
||||||
class StencilTests: XCTestCase {
|
final class StencilTests: XCTestCase {
|
||||||
func testStencil() {
|
lazy var environment: Environment = {
|
||||||
describe("Stencil") {
|
|
||||||
let exampleExtension = Extension()
|
let exampleExtension = Extension()
|
||||||
|
exampleExtension.registerSimpleTag("simpletag") { _ in
|
||||||
exampleExtension.registerSimpleTag("simpletag") { context in
|
"Hello World"
|
||||||
return "Hello World"
|
|
||||||
}
|
}
|
||||||
|
exampleExtension.registerTag("customtag") { _, token in
|
||||||
exampleExtension.registerTag("customtag") { parser, token in
|
CustomNode(token: token)
|
||||||
return CustomNode(token: token)
|
|
||||||
}
|
}
|
||||||
|
return Environment(extensions: [exampleExtension])
|
||||||
|
}()
|
||||||
|
|
||||||
let environment = Environment(extensions: [exampleExtension])
|
func testStencil() {
|
||||||
|
it("can render the README example") {
|
||||||
$0.it("can render the README example") {
|
|
||||||
|
|
||||||
let templateString = """
|
let templateString = """
|
||||||
There are {{ articles.count }} articles.
|
There are {{ articles.count }} articles.
|
||||||
|
|
||||||
@@ -42,7 +39,7 @@ class StencilTests: XCTestCase {
|
|||||||
let context = [
|
let context = [
|
||||||
"articles": [
|
"articles": [
|
||||||
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
|
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
|
||||||
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
|
Article(title: "Memory Management with ARC", author: "Kyle Fuller")
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -58,15 +55,14 @@ class StencilTests: XCTestCase {
|
|||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can render a custom template tag") {
|
it("can render a custom template tag") {
|
||||||
let result = try environment.renderTemplate(string: "{% customtag %}")
|
let result = try self.environment.renderTemplate(string: "{% customtag %}")
|
||||||
try expect(result) == "Hello World"
|
try expect(result) == "Hello World"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can render a simple custom tag") {
|
it("can render a simple custom tag") {
|
||||||
let result = try environment.renderTemplate(string: "{% simpletag %}")
|
let result = try self.environment.renderTemplate(string: "{% simpletag %}")
|
||||||
try expect(result) == "Hello World"
|
try expect(result) == "Hello World"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
import XCTest
|
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
|
import XCTest
|
||||||
|
|
||||||
class TemplateTests: XCTestCase {
|
final class TemplateTests: XCTestCase {
|
||||||
func testTemplate() {
|
func testTemplate() {
|
||||||
describe("Template") {
|
it("can render a template from a string") {
|
||||||
$0.it("can render a template from a string") {
|
|
||||||
let template = Template(templateString: "Hello World")
|
let template = Template(templateString: "Hello World")
|
||||||
let result = try template.render([ "name": "Kyle" ])
|
let result = try template.render([ "name": "Kyle" ])
|
||||||
try expect(result) == "Hello World"
|
try expect(result) == "Hello World"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can render a template from a string literal") {
|
it("can render a template from a string literal") {
|
||||||
let template: Template = "Hello World"
|
let template: Template = "Hello World"
|
||||||
let result = try template.render([ "name": "Kyle" ])
|
let result = try template.render([ "name": "Kyle" ])
|
||||||
try expect(result) == "Hello World"
|
try expect(result) == "Hello World"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import XCTest
|
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
|
import XCTest
|
||||||
|
|
||||||
class TokenTests: XCTestCase {
|
final class TokenTests: XCTestCase {
|
||||||
func testToken() {
|
func testToken() {
|
||||||
describe("Token") {
|
it("can split the contents into components") {
|
||||||
$0.it("can split the contents into components") {
|
|
||||||
let token = Token.text(value: "hello world", at: .unknown)
|
let token = Token.text(value: "hello world", at: .unknown)
|
||||||
let components = token.components
|
let components = token.components
|
||||||
|
|
||||||
@@ -14,7 +13,7 @@ class TokenTests: XCTestCase {
|
|||||||
try expect(components[1]) == "world"
|
try expect(components[1]) == "world"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can split the contents into components with single quoted strings") {
|
it("can split the contents into components with single quoted strings") {
|
||||||
let token = Token.text(value: "hello 'kyle fuller'", at: .unknown)
|
let token = Token.text(value: "hello 'kyle fuller'", at: .unknown)
|
||||||
let components = token.components
|
let components = token.components
|
||||||
|
|
||||||
@@ -23,7 +22,7 @@ class TokenTests: XCTestCase {
|
|||||||
try expect(components[1]) == "'kyle fuller'"
|
try expect(components[1]) == "'kyle fuller'"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can split the contents into components with double quoted strings") {
|
it("can split the contents into components with double quoted strings") {
|
||||||
let token = Token.text(value: "hello \"kyle fuller\"", at: .unknown)
|
let token = Token.text(value: "hello \"kyle fuller\"", at: .unknown)
|
||||||
let components = token.components
|
let components = token.components
|
||||||
|
|
||||||
@@ -33,4 +32,3 @@ class TokenTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,232 +1,215 @@
|
|||||||
import XCTest
|
|
||||||
import Foundation
|
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
|
import XCTest
|
||||||
|
|
||||||
#if os(OSX)
|
#if os(OSX)
|
||||||
@objc class Superclass: NSObject {
|
@objc
|
||||||
|
class Superclass: NSObject {
|
||||||
@objc let name = "Foo"
|
@objc let name = "Foo"
|
||||||
}
|
}
|
||||||
@objc class Object : Superclass {
|
@objc
|
||||||
|
class Object: Superclass {
|
||||||
@objc let title = "Hello World"
|
@objc let title = "Hello World"
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
fileprivate struct Person {
|
private struct Person {
|
||||||
let name: String
|
let name: String
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate struct Article {
|
private struct Article {
|
||||||
let author: Person
|
let author: Person
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate class WebSite {
|
private class WebSite {
|
||||||
let url: String = "blog.com"
|
let url: String = "blog.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate class Blog: WebSite {
|
private class Blog: WebSite {
|
||||||
let articles: [Article] = [Article(author: Person(name: "Kyle"))]
|
let articles: [Article] = [Article(author: Person(name: "Kyle"))]
|
||||||
let featuring: Article? = Article(author: Person(name: "Jhon"))
|
let featuring: Article? = Article(author: Person(name: "Jhon"))
|
||||||
}
|
}
|
||||||
|
|
||||||
class VariableTests: XCTestCase {
|
final class VariableTests: XCTestCase {
|
||||||
func testVariable() {
|
let context: Context = {
|
||||||
describe("Variable") {
|
let ext = Extension()
|
||||||
let context = Context(dictionary: [
|
ext.registerFilter("incr") { arg in
|
||||||
|
(arg.flatMap { toNumber(value: $0) } ?? 0) + 1
|
||||||
|
}
|
||||||
|
let environment = Environment(extensions: [ext])
|
||||||
|
|
||||||
|
var context = Context(dictionary: [
|
||||||
"name": "Kyle",
|
"name": "Kyle",
|
||||||
"contacts": ["Katie", "Carlton"],
|
"contacts": ["Katie", "Carlton"],
|
||||||
"profiles": [
|
"profiles": [
|
||||||
"github": "kylef",
|
"github": "kylef"
|
||||||
],
|
],
|
||||||
"counter": [
|
"counter": [
|
||||||
"count": "kylef",
|
"count": "kylef"
|
||||||
],
|
],
|
||||||
"article": Article(author: Person(name: "Kyle")),
|
"article": Article(author: Person(name: "Kyle")),
|
||||||
|
"blog": Blog(),
|
||||||
"tuple": (one: 1, two: 2)
|
"tuple": (one: 1, two: 2)
|
||||||
])
|
], environment: environment)
|
||||||
|
|
||||||
#if os(OSX)
|
#if os(OSX)
|
||||||
context["object"] = Object()
|
context["object"] = Object()
|
||||||
#endif
|
#endif
|
||||||
context["blog"] = Blog()
|
return context
|
||||||
|
}()
|
||||||
|
|
||||||
$0.it("can resolve a string literal with double quotes") {
|
func testLiterals() {
|
||||||
|
it("can resolve a string literal with double quotes") {
|
||||||
let variable = Variable("\"name\"")
|
let variable = Variable("\"name\"")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(self.context) as? String
|
||||||
try expect(result) == "name"
|
try expect(result) == "name"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve a string literal with single quotes") {
|
it("can resolve a string literal with single quotes") {
|
||||||
let variable = Variable("'name'")
|
let variable = Variable("'name'")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(self.context) as? String
|
||||||
try expect(result) == "name"
|
try expect(result) == "name"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve an integer literal") {
|
it("can resolve an integer literal") {
|
||||||
let variable = Variable("5")
|
let variable = Variable("5")
|
||||||
let result = try variable.resolve(context) as? Int
|
let result = try variable.resolve(self.context) as? Int
|
||||||
try expect(result) == 5
|
try expect(result) == 5
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve an float literal") {
|
it("can resolve an float literal") {
|
||||||
let variable = Variable("3.14")
|
let variable = Variable("3.14")
|
||||||
let result = try variable.resolve(context) as? Number
|
let result = try variable.resolve(self.context) as? Number
|
||||||
try expect(result) == 3.14
|
try expect(result) == 3.14
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve boolean literal") {
|
it("can resolve boolean literal") {
|
||||||
try expect(Variable("true").resolve(context) as? Bool) == true
|
try expect(Variable("true").resolve(self.context) as? Bool) == true
|
||||||
try expect(Variable("false").resolve(context) as? Bool) == false
|
try expect(Variable("false").resolve(self.context) as? Bool) == false
|
||||||
try expect(Variable("0").resolve(context) as? Int) == 0
|
try expect(Variable("0").resolve(self.context) as? Int) == 0
|
||||||
try expect(Variable("1").resolve(context) as? Int) == 1
|
try expect(Variable("1").resolve(self.context) as? Int) == 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve a string variable") {
|
func testVariable() {
|
||||||
|
it("can resolve a string variable") {
|
||||||
let variable = Variable("name")
|
let variable = Variable("name")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(self.context) as? String
|
||||||
try expect(result) == "Kyle"
|
try expect(result) == "Kyle"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.context("given string") {
|
|
||||||
$0.it("can resolve an item via it's index") {
|
|
||||||
let variable = Variable("name.0")
|
|
||||||
let result = try variable.resolve(context) as? Character
|
|
||||||
try expect(result) == "K"
|
|
||||||
|
|
||||||
let variable1 = Variable("name.1")
|
|
||||||
let result1 = try variable1.resolve(context) as? Character
|
|
||||||
try expect(result1) == "y"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve an item via unknown index") {
|
func testDictionary() {
|
||||||
let variable = Variable("name.5")
|
it("can resolve an item from a dictionary") {
|
||||||
let result = try variable.resolve(context) as? Character
|
|
||||||
try expect(result).to.beNil()
|
|
||||||
|
|
||||||
let variable1 = Variable("name.-5")
|
|
||||||
let result1 = try variable1.resolve(context) as? Character
|
|
||||||
try expect(result1).to.beNil()
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("can resolve the first item") {
|
|
||||||
let variable = Variable("name.first")
|
|
||||||
let result = try variable.resolve(context) as? Character
|
|
||||||
try expect(result) == "K"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("can resolve the last item") {
|
|
||||||
let variable = Variable("name.last")
|
|
||||||
let result = try variable.resolve(context) as? Character
|
|
||||||
try expect(result) == "e"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("can get the characters count") {
|
|
||||||
let variable = Variable("name.count")
|
|
||||||
let result = try variable.resolve(context) as? Int
|
|
||||||
try expect(result) == 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.context("given dictionary") {
|
|
||||||
$0.it("can resolve an item") {
|
|
||||||
let variable = Variable("profiles.github")
|
let variable = Variable("profiles.github")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(self.context) as? String
|
||||||
try expect(result) == "kylef"
|
try expect(result) == "kylef"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can get the count") {
|
it("can get the count of a dictionary") {
|
||||||
let variable = Variable("profiles.count")
|
let variable = Variable("profiles.count")
|
||||||
let result = try variable.resolve(context) as? Int
|
let result = try variable.resolve(self.context) as? Int
|
||||||
try expect(result) == 1
|
try expect(result) == 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.context("given array") {
|
func testArray() {
|
||||||
$0.it("can resolve an item via it's index") {
|
it("can resolve an item from an array via it's index") {
|
||||||
let variable = Variable("contacts.0")
|
let variable = Variable("contacts.0")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(self.context) as? String
|
||||||
try expect(result) == "Katie"
|
try expect(result) == "Katie"
|
||||||
|
|
||||||
let variable1 = Variable("contacts.1")
|
let variable1 = Variable("contacts.1")
|
||||||
let result1 = try variable1.resolve(context) as? String
|
let result1 = try variable1.resolve(self.context) as? String
|
||||||
try expect(result1) == "Carlton"
|
try expect(result1) == "Carlton"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve an item via unknown index") {
|
it("can resolve an item from an array via unknown index") {
|
||||||
let variable = Variable("contacts.5")
|
let variable = Variable("contacts.5")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(self.context) as? String
|
||||||
try expect(result).to.beNil()
|
try expect(result).to.beNil()
|
||||||
|
|
||||||
let variable1 = Variable("contacts.-5")
|
let variable1 = Variable("contacts.-5")
|
||||||
let result1 = try variable1.resolve(context) as? String
|
let result1 = try variable1.resolve(self.context) as? String
|
||||||
try expect(result1).to.beNil()
|
try expect(result1).to.beNil()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve the first item") {
|
it("can resolve the first item from an array") {
|
||||||
let variable = Variable("contacts.first")
|
let variable = Variable("contacts.first")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(self.context) as? String
|
||||||
try expect(result) == "Katie"
|
try expect(result) == "Katie"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve the last item") {
|
it("can resolve the last item from an array") {
|
||||||
let variable = Variable("contacts.last")
|
let variable = Variable("contacts.last")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(self.context) as? String
|
||||||
try expect(result) == "Carlton"
|
try expect(result) == "Carlton"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$0.it("can get the count") {
|
func testReflection() {
|
||||||
let variable = Variable("contacts.count")
|
it("can resolve a property with reflection") {
|
||||||
let result = try variable.resolve(context) as? Int
|
let variable = Variable("article.author.name")
|
||||||
|
let result = try variable.resolve(self.context) as? String
|
||||||
|
try expect(result) == "Kyle"
|
||||||
|
}
|
||||||
|
|
||||||
|
it("can resolve a value via reflection") {
|
||||||
|
let variable = Variable("blog.articles.0.author.name")
|
||||||
|
let result = try variable.resolve(self.context) as? String
|
||||||
|
try expect(result) == "Kyle"
|
||||||
|
}
|
||||||
|
|
||||||
|
it("can resolve a superclass value via reflection") {
|
||||||
|
let variable = Variable("blog.url")
|
||||||
|
let result = try variable.resolve(self.context) as? String
|
||||||
|
try expect(result) == "blog.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
it("can resolve optional variable property using reflection") {
|
||||||
|
let variable = Variable("blog.featuring.author.name")
|
||||||
|
let result = try variable.resolve(self.context) as? String
|
||||||
|
try expect(result) == "Jhon"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testKVO() {
|
||||||
|
#if os(OSX)
|
||||||
|
it("can resolve a value via KVO") {
|
||||||
|
let variable = Variable("object.title")
|
||||||
|
let result = try variable.resolve(self.context) as? String
|
||||||
|
try expect(result) == "Hello World"
|
||||||
|
}
|
||||||
|
|
||||||
|
it("can resolve a superclass value via KVO") {
|
||||||
|
let variable = Variable("object.name")
|
||||||
|
let result = try variable.resolve(self.context) as? String
|
||||||
|
try expect(result) == "Foo"
|
||||||
|
}
|
||||||
|
|
||||||
|
it("does not crash on KVO") {
|
||||||
|
let variable = Variable("object.fullname")
|
||||||
|
let result = try variable.resolve(self.context) as? String
|
||||||
|
try expect(result).to.beNil()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTuple() {
|
||||||
|
it("can resolve tuple by index") {
|
||||||
|
let variable = Variable("tuple.0")
|
||||||
|
let result = try variable.resolve(self.context) as? Int
|
||||||
|
try expect(result) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
it("can resolve tuple by label") {
|
||||||
|
let variable = Variable("tuple.two")
|
||||||
|
let result = try variable.resolve(self.context) as? Int
|
||||||
try expect(result) == 2
|
try expect(result) == 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve a property with reflection") {
|
func testOptional() {
|
||||||
let variable = Variable("article.author.name")
|
it("does not render Optional") {
|
||||||
let result = try variable.resolve(context) as? String
|
|
||||||
try expect(result) == "Kyle"
|
|
||||||
}
|
|
||||||
|
|
||||||
#if os(OSX)
|
|
||||||
$0.it("can resolve a value via KVO") {
|
|
||||||
let variable = Variable("object.title")
|
|
||||||
let result = try variable.resolve(context) as? String
|
|
||||||
try expect(result) == "Hello World"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("can resolve a superclass value via KVO") {
|
|
||||||
let variable = Variable("object.name")
|
|
||||||
let result = try variable.resolve(context) as? String
|
|
||||||
try expect(result) == "Foo"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("does not crash on KVO") {
|
|
||||||
let variable = Variable("object.fullname")
|
|
||||||
let result = try variable.resolve(context) as? String
|
|
||||||
try expect(result).to.beNil()
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
$0.it("can resolve a value via reflection") {
|
|
||||||
let variable = Variable("blog.articles.0.author.name")
|
|
||||||
let result = try variable.resolve(context) as? String
|
|
||||||
try expect(result) == "Kyle"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("can resolve a superclass value via reflection") {
|
|
||||||
let variable = Variable("blog.url")
|
|
||||||
let result = try variable.resolve(context) as? String
|
|
||||||
try expect(result) == "blog.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("can resolve optional variable property using reflection") {
|
|
||||||
let variable = Variable("blog.featuring.author.name")
|
|
||||||
let result = try variable.resolve(context) as? String
|
|
||||||
try expect(result) == "Jhon"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("does not render Optional") {
|
|
||||||
var array: [Any?] = [1, nil]
|
var array: [Any?] = [1, nil]
|
||||||
array.append(array)
|
array.append(array)
|
||||||
let context = Context(dictionary: ["values": array])
|
let context = Context(dictionary: ["values": array])
|
||||||
@@ -234,87 +217,78 @@ class VariableTests: XCTestCase {
|
|||||||
try expect(VariableNode(variable: "values").render(context)) == "[1, nil, [1, nil]]"
|
try expect(VariableNode(variable: "values").render(context)) == "[1, nil, [1, nil]]"
|
||||||
try expect(VariableNode(variable: "values.1").render(context)) == ""
|
try expect(VariableNode(variable: "values.1").render(context)) == ""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can subscript tuple by index") {
|
|
||||||
let variable = Variable("tuple.0")
|
|
||||||
let result = try variable.resolve(context) as? Int
|
|
||||||
try expect(result) == 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can subscript tuple by label") {
|
func testSubscripting() {
|
||||||
let variable = Variable("tuple.two")
|
it("can resolve a property subscript via reflection") {
|
||||||
let result = try variable.resolve(context) as? Int
|
try self.context.push(dictionary: ["property": "name"]) {
|
||||||
try expect(result) == 2
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.describe("Subscripting") {
|
|
||||||
$0.it("can resolve a property subscript via reflection") {
|
|
||||||
try context.push(dictionary: ["property": "name"]) {
|
|
||||||
let variable = Variable("article.author[property]")
|
let variable = Variable("article.author[property]")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(self.context) as? String
|
||||||
try expect(result) == "Kyle"
|
try expect(result) == "Kyle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can subscript an array with a valid index") {
|
it("can subscript an array with a valid index") {
|
||||||
try context.push(dictionary: ["property": 0]) {
|
try self.context.push(dictionary: ["property": 0]) {
|
||||||
let variable = Variable("contacts[property]")
|
let variable = Variable("contacts[property]")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(self.context) as? String
|
||||||
try expect(result) == "Katie"
|
try expect(result) == "Katie"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can subscript an array with an unknown index") {
|
it("can subscript an array with an unknown index") {
|
||||||
try context.push(dictionary: ["property": 5]) {
|
try self.context.push(dictionary: ["property": 5]) {
|
||||||
let variable = Variable("contacts[property]")
|
let variable = Variable("contacts[property]")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(self.context) as? String
|
||||||
try expect(result).to.beNil()
|
try expect(result).to.beNil()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(OSX)
|
#if os(OSX)
|
||||||
$0.it("can resolve a subscript via KVO") {
|
it("can resolve a subscript via KVO") {
|
||||||
try context.push(dictionary: ["property": "name"]) {
|
try self.context.push(dictionary: ["property": "name"]) {
|
||||||
let variable = Variable("object[property]")
|
let variable = Variable("object[property]")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(self.context) as? String
|
||||||
try expect(result) == "Foo"
|
try expect(result) == "Foo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
$0.it("can resolve an optional subscript via reflection") {
|
it("can resolve an optional subscript via reflection") {
|
||||||
try context.push(dictionary: ["property": "featuring"]) {
|
try self.context.push(dictionary: ["property": "featuring"]) {
|
||||||
let variable = Variable("blog[property].author.name")
|
let variable = Variable("blog[property].author.name")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(self.context) as? String
|
||||||
try expect(result) == "Jhon"
|
try expect(result) == "Jhon"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$0.it("can resolve multiple subscripts") {
|
func testMultipleSubscripting() {
|
||||||
try context.push(dictionary: [
|
it("can resolve multiple subscripts") {
|
||||||
|
try self.context.push(dictionary: [
|
||||||
"prop1": "articles",
|
"prop1": "articles",
|
||||||
"prop2": 0,
|
"prop2": 0,
|
||||||
"prop3": "name"
|
"prop3": "name"
|
||||||
]) {
|
]) {
|
||||||
let variable = Variable("blog[prop1][prop2].author[prop3]")
|
let variable = Variable("blog[prop1][prop2].author[prop3]")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(self.context) as? String
|
||||||
try expect(result) == "Kyle"
|
try expect(result) == "Kyle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve nested subscripts") {
|
it("can resolve nested subscripts") {
|
||||||
try context.push(dictionary: [
|
try self.context.push(dictionary: [
|
||||||
"prop1": "prop2",
|
"prop1": "prop2",
|
||||||
"ref": ["prop2": "name"]
|
"ref": ["prop2": "name"]
|
||||||
]) {
|
]) {
|
||||||
let variable = Variable("article.author[ref[prop1]]")
|
let variable = Variable("article.author[ref[prop1]]")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(self.context) as? String
|
||||||
try expect(result) == "Kyle"
|
try expect(result) == "Kyle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws for invalid keypath syntax") {
|
it("throws for invalid keypath syntax") {
|
||||||
try context.push(dictionary: ["prop": "name"]) {
|
try self.context.push(dictionary: ["prop": "name"]) {
|
||||||
let samples = [
|
let samples = [
|
||||||
".",
|
".",
|
||||||
"..",
|
"..",
|
||||||
@@ -333,79 +307,49 @@ class VariableTests: XCTestCase {
|
|||||||
|
|
||||||
for lookup in samples {
|
for lookup in samples {
|
||||||
let variable = Variable(lookup)
|
let variable = Variable(lookup)
|
||||||
try expect(variable.resolve(context)).toThrow()
|
try expect(variable.resolve(self.context)).toThrow()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("RangeVariable") {
|
func testRangeVariable() {
|
||||||
|
|
||||||
let context: Context = {
|
|
||||||
let ext = Extension()
|
|
||||||
ext.registerFilter("incr", filter: { (arg: Any?) in toNumber(value: arg!)! + 1 })
|
|
||||||
let environment = Environment(extensions: [ext])
|
|
||||||
return Context(dictionary: [:], environment: environment)
|
|
||||||
}()
|
|
||||||
|
|
||||||
func makeVariable(_ token: String) throws -> RangeVariable? {
|
func makeVariable(_ token: String) throws -> RangeVariable? {
|
||||||
let token = Token.variable(value: token, at: .unknown)
|
let token = Token.variable(value: token, at: .unknown)
|
||||||
return try RangeVariable(token.contents, environment: context.environment, containedIn: token)
|
return try RangeVariable(token.contents, environment: context.environment, containedIn: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve closed range as array") {
|
it("can resolve closed range as array") {
|
||||||
let result = try makeVariable("1...3")?.resolve(context) as? [Int]
|
let result = try makeVariable("1...3")?.resolve(self.context) as? [Int]
|
||||||
try expect(result) == [1, 2, 3]
|
try expect(result) == [1, 2, 3]
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve decreasing closed range as reversed array") {
|
it("can resolve decreasing closed range as reversed array") {
|
||||||
let result = try makeVariable("3...1")?.resolve(context) as? [Int]
|
let result = try makeVariable("3...1")?.resolve(self.context) as? [Int]
|
||||||
try expect(result) == [3, 2, 1]
|
try expect(result) == [3, 2, 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can use filter on range variables") {
|
it("can use filter on range variables") {
|
||||||
let result = try makeVariable("1|incr...3|incr")?.resolve(context) as? [Int]
|
let result = try makeVariable("1|incr...3|incr")?.resolve(self.context) as? [Int]
|
||||||
try expect(result) == [2, 3, 4]
|
try expect(result) == [2, 3, 4]
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws when left value is not int") {
|
it("throws when left value is not int") {
|
||||||
let template: Template = "{% for i in k...j %}{{ i }}{% endfor %}"
|
let template: Template = "{% for i in k...j %}{{ i }}{% endfor %}"
|
||||||
try expect(try template.render(Context(dictionary: ["j": 3, "k": "1"]))).toThrow()
|
try expect(try template.render(Context(dictionary: ["j": 3, "k": "1"]))).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws when right value is not int") {
|
it("throws when right value is not int") {
|
||||||
let variable = try makeVariable("k...j")
|
let variable = try makeVariable("k...j")
|
||||||
try expect(try variable?.resolve(Context(dictionary: ["j": "3", "k": 1]))).toThrow()
|
try expect(try variable?.resolve(Context(dictionary: ["j": "3", "k": 1]))).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws is left range value is missing") {
|
it("throws is left range value is missing") {
|
||||||
try expect(makeVariable("...1")).toThrow()
|
try expect(makeVariable("...1")).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws is right range value is missing") {
|
it("throws is right range value is missing") {
|
||||||
try expect(makeVariable("1...")).toThrow()
|
try expect(makeVariable("1...")).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("inline if expression") {
|
|
||||||
|
|
||||||
$0.it("can conditionally render variable") {
|
|
||||||
let template: Template = "{{ variable if variable|uppercase == \"A\" }}"
|
|
||||||
try expect(template.render(Context(dictionary: ["variable": "a"]))) == "a"
|
|
||||||
try expect(template.render(Context(dictionary: ["variable": "b"]))) == ""
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("can render with else expression") {
|
|
||||||
let template: Template = "{{ variable if variable|uppercase == \"A\" else fallback|uppercase }}"
|
|
||||||
try expect(template.render(Context(dictionary: ["variable": "b", "fallback": "c"]))) == "C"
|
|
||||||
}
|
|
||||||
|
|
||||||
$0.it("throws when used invalid condition") {
|
|
||||||
let template: Template = "{{ variable if variable \"A\" }}"
|
|
||||||
try expect(template.render(Context(dictionary: ["variable": "a"]))).toThrow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,54 @@ import XCTest
|
|||||||
|
|
||||||
extension ContextTests {
|
extension ContextTests {
|
||||||
static let __allTests = [
|
static let __allTests = [
|
||||||
("testContext", testContext),
|
("testContextRestoration", testContextRestoration),
|
||||||
|
("testContextSubscripting", testContextSubscripting),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EnvironmentBaseAndChildTemplateTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testRuntimeErrorInBaseTemplate", testRuntimeErrorInBaseTemplate),
|
||||||
|
("testRuntimeErrorInChildTemplate", testRuntimeErrorInChildTemplate),
|
||||||
|
("testSyntaxErrorInBaseTemplate", testSyntaxErrorInBaseTemplate),
|
||||||
|
("testSyntaxErrorInChildTemplate", testSyntaxErrorInChildTemplate),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EnvironmentIncludeTemplateTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testRuntimeError", testRuntimeError),
|
||||||
|
("testSyntaxError", testSyntaxError),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
extension EnvironmentTests {
|
extension EnvironmentTests {
|
||||||
static let __allTests = [
|
static let __allTests = [
|
||||||
("testEnvironment", testEnvironment),
|
("testLoading", testLoading),
|
||||||
|
("testRendering", testRendering),
|
||||||
|
("testRenderingError", testRenderingError),
|
||||||
|
("testSyntaxError", testSyntaxError),
|
||||||
|
("testUnknownFilter", testUnknownFilter),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ExpressionsTests {
|
extension ExpressionsTests {
|
||||||
static let __allTests = [
|
static let __allTests = [
|
||||||
("testExpressions", testExpressions),
|
("testAndExpression", testAndExpression),
|
||||||
|
("testEqualityExpression", testEqualityExpression),
|
||||||
|
("testExpressionParsing", testExpressionParsing),
|
||||||
|
("testFalseExpressions", testFalseExpressions),
|
||||||
|
("testFalseInExpression", testFalseInExpression),
|
||||||
|
("testInequalityExpression", testInequalityExpression),
|
||||||
|
("testLessThanEqualExpression", testLessThanEqualExpression),
|
||||||
|
("testLessThanExpression", testLessThanExpression),
|
||||||
|
("testMoreThanEqualExpression", testMoreThanEqualExpression),
|
||||||
|
("testMoreThanExpression", testMoreThanExpression),
|
||||||
|
("testMultipleExpressions", testMultipleExpressions),
|
||||||
|
("testNotExpression", testNotExpression),
|
||||||
|
("testOrExpression", testOrExpression),
|
||||||
|
("testTrueExpressions", testTrueExpressions),
|
||||||
|
("testTrueInExpression", testTrueInExpression),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,50 +61,95 @@ extension FilterTagTests {
|
|||||||
|
|
||||||
extension FilterTests {
|
extension FilterTests {
|
||||||
static let __allTests = [
|
static let __allTests = [
|
||||||
("testFilter", testFilter),
|
("testDefaultFilter", testDefaultFilter),
|
||||||
|
("testDynamicFilters", testDynamicFilters),
|
||||||
|
("testFilterSuggestion", testFilterSuggestion),
|
||||||
|
("testIndentContent", testIndentContent),
|
||||||
|
("testIndentFirstLine", testIndentFirstLine),
|
||||||
|
("testIndentNotEmptyLines", testIndentNotEmptyLines),
|
||||||
|
("testIndentWithArbitraryCharacter", testIndentWithArbitraryCharacter),
|
||||||
|
("testJoinFilter", testJoinFilter),
|
||||||
|
("testRegistration", testRegistration),
|
||||||
|
("testRegistrationOverrideDefault", testRegistrationOverrideDefault),
|
||||||
|
("testRegistrationWithArguments", testRegistrationWithArguments),
|
||||||
|
("testSplitFilter", testSplitFilter),
|
||||||
|
("testStringFilters", testStringFilters),
|
||||||
|
("testStringFiltersWithArrays", testStringFiltersWithArrays),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ForNodeTests {
|
extension ForNodeTests {
|
||||||
static let __allTests = [
|
static let __allTests = [
|
||||||
|
("testArrayOfTuples", testArrayOfTuples),
|
||||||
("testForNode", testForNode),
|
("testForNode", testForNode),
|
||||||
|
("testHandleInvalidInput", testHandleInvalidInput),
|
||||||
|
("testIterateDictionary", testIterateDictionary),
|
||||||
|
("testIterateRange", testIterateRange),
|
||||||
|
("testIterateUsingMirroring", testIterateUsingMirroring),
|
||||||
|
("testLoopMetadata", testLoopMetadata),
|
||||||
|
("testWhereExpression", testWhereExpression),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
extension IfNodeTests {
|
extension IfNodeTests {
|
||||||
static let __allTests = [
|
static let __allTests = [
|
||||||
("testIfNode", testIfNode),
|
("testEvaluatesNilAsFalse", testEvaluatesNilAsFalse),
|
||||||
|
("testParseIf", testParseIf),
|
||||||
|
("testParseIfnot", testParseIfnot),
|
||||||
|
("testParseIfWithElif", testParseIfWithElif),
|
||||||
|
("testParseIfWithElifWithoutElse", testParseIfWithElifWithoutElse),
|
||||||
|
("testParseIfWithElse", testParseIfWithElse),
|
||||||
|
("testParseMultipleElif", testParseMultipleElif),
|
||||||
|
("testParsingErrors", testParsingErrors),
|
||||||
|
("testRendering", testRendering),
|
||||||
|
("testSupportsRangeVariables", testSupportsRangeVariables),
|
||||||
|
("testSupportVariableFilters", testSupportVariableFilters),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
extension IncludeTests {
|
extension IncludeTests {
|
||||||
static let __allTests = [
|
static let __allTests = [
|
||||||
("testInclude", testInclude),
|
("testParsing", testParsing),
|
||||||
|
("testRendering", testRendering),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
extension InheritenceTests {
|
extension InheritanceTests {
|
||||||
static let __allTests = [
|
static let __allTests = [
|
||||||
("testInheritence", testInheritence),
|
("testInheritance", testInheritance),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LexerTests {
|
extension LexerTests {
|
||||||
static let __allTests = [
|
static let __allTests = [
|
||||||
("testLexer", testLexer),
|
("testComment", testComment),
|
||||||
|
("testContentMixture", testContentMixture),
|
||||||
|
("testEmptyVariable", testEmptyVariable),
|
||||||
|
("testEscapeSequence", testEscapeSequence),
|
||||||
|
("testNewlines", testNewlines),
|
||||||
("testPerformance", testPerformance),
|
("testPerformance", testPerformance),
|
||||||
|
("testText", testText),
|
||||||
|
("testTokenizeIncorrectSyntaxWithoutCrashing", testTokenizeIncorrectSyntaxWithoutCrashing),
|
||||||
|
("testTokenWithoutSpaces", testTokenWithoutSpaces),
|
||||||
|
("testUnclosedBlock", testUnclosedBlock),
|
||||||
|
("testUnclosedTag", testUnclosedTag),
|
||||||
|
("testVariable", testVariable),
|
||||||
|
("testVariablesWithoutBeingGreedy", testVariablesWithoutBeingGreedy),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NodeTests {
|
extension NodeTests {
|
||||||
static let __allTests = [
|
static let __allTests = [
|
||||||
("testNode", testNode),
|
("testRendering", testRendering),
|
||||||
|
("testTextNode", testTextNode),
|
||||||
|
("testVariableNode", testVariableNode),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NowNodeTests {
|
extension NowNodeTests {
|
||||||
static let __allTests = [
|
static let __allTests = [
|
||||||
("testNowNode", testNowNode),
|
("testParsing", testParsing),
|
||||||
|
("testRendering", testRendering),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +161,8 @@ extension StencilTests {
|
|||||||
|
|
||||||
extension TemplateLoaderTests {
|
extension TemplateLoaderTests {
|
||||||
static let __allTests = [
|
static let __allTests = [
|
||||||
("testTemplateLoader", testTemplateLoader),
|
("testDictionaryLoader", testDictionaryLoader),
|
||||||
|
("testFileSystemLoader", testFileSystemLoader),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +186,16 @@ extension TokenTests {
|
|||||||
|
|
||||||
extension VariableTests {
|
extension VariableTests {
|
||||||
static let __allTests = [
|
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),
|
("testVariable", testVariable),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -113,6 +204,8 @@ extension VariableTests {
|
|||||||
public func __allTests() -> [XCTestCaseEntry] {
|
public func __allTests() -> [XCTestCaseEntry] {
|
||||||
return [
|
return [
|
||||||
testCase(ContextTests.__allTests),
|
testCase(ContextTests.__allTests),
|
||||||
|
testCase(EnvironmentBaseAndChildTemplateTests.__allTests),
|
||||||
|
testCase(EnvironmentIncludeTemplateTests.__allTests),
|
||||||
testCase(EnvironmentTests.__allTests),
|
testCase(EnvironmentTests.__allTests),
|
||||||
testCase(ExpressionsTests.__allTests),
|
testCase(ExpressionsTests.__allTests),
|
||||||
testCase(FilterTagTests.__allTests),
|
testCase(FilterTagTests.__allTests),
|
||||||
@@ -120,7 +213,7 @@ public func __allTests() -> [XCTestCaseEntry] {
|
|||||||
testCase(ForNodeTests.__allTests),
|
testCase(ForNodeTests.__allTests),
|
||||||
testCase(IfNodeTests.__allTests),
|
testCase(IfNodeTests.__allTests),
|
||||||
testCase(IncludeTests.__allTests),
|
testCase(IncludeTests.__allTests),
|
||||||
testCase(InheritenceTests.__allTests),
|
testCase(InheritanceTests.__allTests),
|
||||||
testCase(LexerTests.__allTests),
|
testCase(LexerTests.__allTests),
|
||||||
testCase(NodeTests.__allTests),
|
testCase(NodeTests.__allTests),
|
||||||
testCase(NowNodeTests.__allTests),
|
testCase(NowNodeTests.__allTests),
|
||||||
|
|||||||
Reference in New Issue
Block a user