Merge branch 'master' into dynamic-filter
# Conflicts: # CHANGELOG.md # Sources/ForTag.swift # Sources/IfTag.swift # Sources/Parser.swift # Sources/Variable.swift # Tests/StencilTests/ExpressionSpec.swift # Tests/StencilTests/FilterSpec.swift # Tests/StencilTests/ForNodeSpec.swift # Tests/StencilTests/VariableSpec.swift
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
.conche/
|
.conche/
|
||||||
.build/
|
.build/
|
||||||
Packages/
|
Packages/
|
||||||
Package.resolved
|
|
||||||
Package.pins
|
Package.pins
|
||||||
|
*.xcodeproj
|
||||||
|
|||||||
15
.travis.yml
15
.travis.yml
@@ -1,18 +1,15 @@
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: osx
|
- os: osx
|
||||||
osx_image: xcode8.3
|
osx_image: xcode9.4
|
||||||
env: SWIFT_VERSION=3.1.1
|
env: SWIFT_VERSION=4.1
|
||||||
- os: osx
|
- os: osx
|
||||||
osx_image: xcode9
|
osx_image: xcode10
|
||||||
env: SWIFT_VERSION=4.0
|
env: SWIFT_VERSION=4.2
|
||||||
- os: osx
|
|
||||||
osx_image: xcode9.1
|
|
||||||
env: SWIFT_VERSION=4.0
|
|
||||||
- os: linux
|
- os: linux
|
||||||
env: SWIFT_VERSION=3.1.1
|
env: SWIFT_VERSION=4.1
|
||||||
- os: linux
|
- os: linux
|
||||||
env: SWIFT_VERSION=4.0
|
env: SWIFT_VERSION=4.2
|
||||||
language: generic
|
language: generic
|
||||||
sudo: required
|
sudo: required
|
||||||
dist: trusty
|
dist: trusty
|
||||||
|
|||||||
99
CHANGELOG.md
99
CHANGELOG.md
@@ -2,6 +2,100 @@
|
|||||||
|
|
||||||
## Master
|
## Master
|
||||||
|
|
||||||
|
### Breaking
|
||||||
|
|
||||||
|
_None_
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
- Added support for dynamic filter using `filter` filter. With that you can define a variable with a name of filter
|
||||||
|
, i.e. `myfilter = "uppercase"` and then use it to invoke this filter with `{{ string|filter:myfilter }}`.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#203](https://github.com/stencilproject/Stencil/pull/203)
|
||||||
|
|
||||||
|
### Deprecations
|
||||||
|
|
||||||
|
_None_
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
_None_
|
||||||
|
|
||||||
|
### Internal Changes
|
||||||
|
|
||||||
|
_None_
|
||||||
|
|
||||||
|
|
||||||
|
## 0.13.1
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fixed a bug in Stencil 0.13 where tags without spaces were incorrectly parsed.
|
||||||
|
[David Jennes](https://github.com/djbe)
|
||||||
|
[#252](https://github.com/stencilproject/Stencil/pull/252)
|
||||||
|
|
||||||
|
|
||||||
|
## 0.13.0
|
||||||
|
|
||||||
|
### Breaking
|
||||||
|
|
||||||
|
- Now requires Swift 4.1 or newer.
|
||||||
|
[Yonas Kolb](https://github.com/yonaskolb)
|
||||||
|
[#228](https://github.com/stencilproject/Stencil/pull/228)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
- You can now use parentheses in boolean expressions to change operator precedence.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#165](https://github.com/stencilproject/Stencil/pull/165)
|
||||||
|
- Added method to add boolean filters with their negative counterparts.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#160](https://github.com/stencilproject/Stencil/pull/160)
|
||||||
|
- Now you can conditionally render variables with `{{ variable if condition }}`, which is a shorthand for `{% if condition %}{{ variable }}{% endif %}`. You can also use `else` like `{{ variable1 if condition else variable2 }}`, which is a shorthand for `{% if condition %}{{ variable1 }}{% else %}{{ variable2 }}{% endif %}`
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#243](https://github.com/stencilproject/Stencil/pull/243)
|
||||||
|
- Now you can access string characters by index or get string length the same was as if it was an array, i.e. `{{ 'string'.first }}`, `{{ 'string'.last }}`, `{{ 'string'.1 }}`, `{{ 'string'.count }}`.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#245](https://github.com/stencilproject/Stencil/pull/245)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fixed the performance issues introduced in Stencil 0.12 with the error log improvements.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#230](https://github.com/stencilproject/Stencil/pull/230)
|
||||||
|
- Now accessing undefined keys in NSObject does not cause runtime crash and instead renders empty string.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#234](https://github.com/stencilproject/Stencil/pull/234)
|
||||||
|
- `for` tag: When iterating over a dictionary the keys will now always be sorted (in an ascending order) to ensure consistent output generation.
|
||||||
|
[David Jennes](https://github.com/djbe)
|
||||||
|
[#240](https://github.com/stencilproject/Stencil/pull/240)
|
||||||
|
|
||||||
|
### Internal Changes
|
||||||
|
|
||||||
|
- Updated the codebase to use Swift 4 features.
|
||||||
|
[David Jennes](https://github.com/djbe)
|
||||||
|
[#239](https://github.com/stencilproject/Stencil/pull/239)
|
||||||
|
- Update to Spectre 0.9.0.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#247](https://github.com/stencilproject/Stencil/pull/247)
|
||||||
|
- Optimise Scanner performance.
|
||||||
|
[Eric Thorpe](https://github.com/trametheka)
|
||||||
|
[Sébastien Duperron](https://github.com/Liquidsoul)
|
||||||
|
[David Jennes](https://github.com/djbe)
|
||||||
|
[#226](https://github.com/stencilproject/Stencil/pull/226)
|
||||||
|
|
||||||
|
|
||||||
|
## 0.12.1
|
||||||
|
|
||||||
|
### Internal Changes
|
||||||
|
|
||||||
|
- Updated the PathKit dependency to 0.9.0 in CocoaPods, to be in line with SPM.
|
||||||
|
[David Jennes](https://github.com/djbe)
|
||||||
|
[#227](https://github.com/stencilproject/Stencil/pull/227)
|
||||||
|
|
||||||
|
|
||||||
|
## 0.12.0
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
- Added an optional second parameter to the `include` tag for passing a sub context to the included file.
|
- Added an optional second parameter to the `include` tag for passing a sub context to the included file.
|
||||||
@@ -14,10 +108,9 @@
|
|||||||
- Adds support for using spaces in filter expression.
|
- Adds support for using spaces in filter expression.
|
||||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
[#178](https://github.com/stencilproject/Stencil/pull/178)
|
[#178](https://github.com/stencilproject/Stencil/pull/178)
|
||||||
- Added support for dynamic filter using `filter` filter. With that you can define a variable with a name of filter
|
- Improvements in error reporting.
|
||||||
, i.e. `myfilter = "uppercase"` and then use it to invoke this filter with `{{ string|filter:myfilter }}`.
|
|
||||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
[#203](https://github.com/stencilproject/Stencil/pull/203)
|
[#167](https://github.com/stencilproject/Stencil/pull/167)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
|||||||
3
LICENSE
3
LICENSE
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2014, Kyle Fuller
|
Copyright (c) 2018, Kyle Fuller
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
@@ -21,4 +21,3 @@ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
|||||||
25
Package.resolved
Normal file
25
Package.resolved
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"object": {
|
||||||
|
"pins": [
|
||||||
|
{
|
||||||
|
"package": "PathKit",
|
||||||
|
"repositoryURL": "https://github.com/kylef/PathKit.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "e2f5be30e4c8f531c9c1e8765aa7b71c0a45d7a0",
|
||||||
|
"version": "0.9.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "Spectre",
|
||||||
|
"repositoryURL": "https://github.com/kylef/Spectre.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "f14ff47f45642aa5703900980b014c2e9394b6e5",
|
||||||
|
"version": "0.9.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
@@ -1,10 +1,22 @@
|
|||||||
// swift-tools-version:3.1
|
// swift-tools-version:4.1
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "Stencil",
|
name: "Stencil",
|
||||||
|
products: [
|
||||||
|
.library(name: "Stencil", targets: ["Stencil"])
|
||||||
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 9),
|
.package(url: "https://github.com/kylef/PathKit.git", from: "0.9.0"),
|
||||||
.Package(url: "https://github.com/kylef/Spectre.git", majorVersion: 0, minor: 8),
|
.package(url: "https://github.com/kylef/Spectre.git", from: "0.9.0")
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.target(name: "Stencil", dependencies: [
|
||||||
|
"PathKit"
|
||||||
|
], path: "Sources"),
|
||||||
|
.testTarget(name: "StencilTests", dependencies: [
|
||||||
|
"Stencil",
|
||||||
|
"Spectre"
|
||||||
|
])
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
// swift-tools-version:3.1
|
|
||||||
import PackageDescription
|
|
||||||
|
|
||||||
let package = Package(
|
|
||||||
name: "Stencil",
|
|
||||||
dependencies: [
|
|
||||||
.Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 8),
|
|
||||||
.Package(url: "https://github.com/kylef/Spectre.git", majorVersion: 0, minor: 7),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
23
Package@swift-4.2.swift
Normal file
23
Package@swift-4.2.swift
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// swift-tools-version:4.2
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "Stencil",
|
||||||
|
products: [
|
||||||
|
.library(name: "Stencil", targets: ["Stencil"])
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
.package(url: "https://github.com/kylef/PathKit.git", from: "0.9.0"),
|
||||||
|
.package(url: "https://github.com/kylef/Spectre.git", from: "0.9.0")
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.target(name: "Stencil", dependencies: [
|
||||||
|
"PathKit"
|
||||||
|
], path: "Sources"),
|
||||||
|
.testTarget(name: "StencilTests", dependencies: [
|
||||||
|
"Stencil",
|
||||||
|
"Spectre"
|
||||||
|
])
|
||||||
|
],
|
||||||
|
swiftLanguageVersions: [.v4, .v4_2]
|
||||||
|
)
|
||||||
@@ -4,7 +4,10 @@ public struct Environment {
|
|||||||
|
|
||||||
public var loader: Loader?
|
public var loader: Loader?
|
||||||
|
|
||||||
public init(loader: Loader? = nil, extensions: [Extension]? = nil, templateClass: Template.Type = Template.self) {
|
public init(loader: Loader? = nil,
|
||||||
|
extensions: [Extension]? = nil,
|
||||||
|
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()]
|
||||||
@@ -28,11 +31,18 @@ public struct Environment {
|
|||||||
|
|
||||||
public func renderTemplate(name: String, context: [String: Any]? = nil) throws -> String {
|
public func renderTemplate(name: String, context: [String: Any]? = nil) throws -> String {
|
||||||
let template = try loadTemplate(name: name)
|
let template = try loadTemplate(name: name)
|
||||||
return try template.render(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]? = nil) 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func render(template: Template, context: [String: Any]?) throws -> String {
|
||||||
|
// update template environment as it can be created from string literal with default environment
|
||||||
|
template.environment = self
|
||||||
return try template.render(context)
|
return try template.render(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,3 +17,67 @@ public class TemplateDoesNotExist: Error, CustomStringConvertible {
|
|||||||
return "Template named `\(templates)` does not exist. No loaders found"
|
return "Template named `\(templates)` does not exist. No loaders found"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
|
||||||
|
public let reason: String
|
||||||
|
public var description: String { return reason }
|
||||||
|
public internal(set) var token: Token?
|
||||||
|
public internal(set) var stackTrace: [Token]
|
||||||
|
public var templateName: String? { return token?.sourceMap.filename }
|
||||||
|
var allTokens: [Token] {
|
||||||
|
return stackTrace + (token.map({ [$0] }) ?? [])
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) {
|
||||||
|
self.reason = reason
|
||||||
|
self.stackTrace = stackTrace
|
||||||
|
self.token = token
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(_ description: String) {
|
||||||
|
self.init(reason: description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Error {
|
||||||
|
func withToken(_ token: Token?) -> Error {
|
||||||
|
if var error = self as? TemplateSyntaxError {
|
||||||
|
error.token = error.token ?? token
|
||||||
|
return error
|
||||||
|
} else {
|
||||||
|
return TemplateSyntaxError(reason: "\(self)", token: token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol ErrorReporter: class {
|
||||||
|
func renderError(_ error: Error) -> String
|
||||||
|
}
|
||||||
|
|
||||||
|
open class SimpleErrorReporter: ErrorReporter {
|
||||||
|
|
||||||
|
open func renderError(_ error: Error) -> String {
|
||||||
|
guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription }
|
||||||
|
|
||||||
|
func describe(token: Token) -> String {
|
||||||
|
let templateName = token.sourceMap.filename ?? ""
|
||||||
|
let location = token.sourceMap.location
|
||||||
|
let highlight = """
|
||||||
|
\(String(Array(repeating: " ", count: location.lineOffset)))\
|
||||||
|
^\(String(Array(repeating: "~", count: max(token.contents.count - 1, 0))))
|
||||||
|
"""
|
||||||
|
|
||||||
|
return """
|
||||||
|
\(templateName)\(location.lineNumber):\(location.lineOffset): error: \(templateError.reason)
|
||||||
|
\(location.content)
|
||||||
|
\(highlight)
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
var descriptions = templateError.stackTrace.reduce([]) { $0 + [describe(token: $1)] }
|
||||||
|
let description = templateError.token.map(describe(token:)) ?? templateError.reason
|
||||||
|
descriptions.append(description)
|
||||||
|
return descriptions.joined(separator: "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,10 +15,19 @@ 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, parser: { parser, token in
|
||||||
return SimpleNode(handler: handler)
|
return SimpleNode(token: token, handler: handler)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Registers boolean filter with it's negative counterpart
|
||||||
|
public func registerFilter(name: String, negativeFilterName: String, filter: @escaping (Any?) throws -> Bool?) {
|
||||||
|
filters[name] = .simple(filter)
|
||||||
|
filters[negativeFilterName] = .simple {
|
||||||
|
guard let result = try filter($0) else { return nil }
|
||||||
|
return !result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Registers a template filter with the given name
|
/// Registers a template filter with the given name
|
||||||
public func registerFilter(_ name: String, filter: @escaping (Any?) throws -> Any?) {
|
public func registerFilter(_ name: String, filter: @escaping (Any?) throws -> Any?) {
|
||||||
filters[name] = .simple(filter)
|
filters[name] = .simple(filter)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class FilterNode : NodeType {
|
class FilterNode : NodeType {
|
||||||
let resolvable: Resolvable
|
let resolvable: Resolvable
|
||||||
let nodes: [NodeType]
|
let nodes: [NodeType]
|
||||||
|
let token: Token?
|
||||||
|
|
||||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||||
let bits = token.components()
|
let bits = token.components()
|
||||||
@@ -15,20 +16,21 @@ class FilterNode : NodeType {
|
|||||||
throw TemplateSyntaxError("`endfilter` was not found.")
|
throw TemplateSyntaxError("`endfilter` was not found.")
|
||||||
}
|
}
|
||||||
|
|
||||||
let resolvable = try parser.compileFilter("filter_value|\(bits[1])")
|
let resolvable = try parser.compileFilter("filter_value|\(bits[1])", containedIn: token)
|
||||||
return FilterNode(nodes: blocks, resolvable: resolvable)
|
return FilterNode(nodes: blocks, resolvable: resolvable, token: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(nodes: [NodeType], resolvable: Resolvable) {
|
init(nodes: [NodeType], resolvable: Resolvable, token: Token) {
|
||||||
self.nodes = nodes
|
self.nodes = nodes
|
||||||
self.resolvable = resolvable
|
self.resolvable = resolvable
|
||||||
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
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).render(context)
|
return try VariableNode(variable: resolvable, token: token).render(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,9 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
|||||||
var indentWidth = 4
|
var indentWidth = 4
|
||||||
if arguments.count > 0 {
|
if arguments.count > 0 {
|
||||||
guard let value = arguments[0] as? Int else {
|
guard let value = arguments[0] as? Int else {
|
||||||
throw TemplateSyntaxError("'indent' filter width argument must be an Integer (\(String(describing: arguments[0])))")
|
throw TemplateSyntaxError("""
|
||||||
|
'indent' filter width argument must be an Integer (\(String(describing: arguments[0])))
|
||||||
|
""")
|
||||||
}
|
}
|
||||||
indentWidth = value
|
indentWidth = value
|
||||||
}
|
}
|
||||||
@@ -82,7 +84,9 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
|||||||
var indentationChar = " "
|
var indentationChar = " "
|
||||||
if arguments.count > 1 {
|
if arguments.count > 1 {
|
||||||
guard let value = arguments[1] as? String else {
|
guard let value = arguments[1] as? String else {
|
||||||
throw TemplateSyntaxError("'indent' filter indentation argument must be a String (\(String(describing: arguments[1]))")
|
throw TemplateSyntaxError("""
|
||||||
|
'indent' filter indentation argument must be a String (\(String(describing: arguments[1]))
|
||||||
|
""")
|
||||||
}
|
}
|
||||||
indentationChar = value
|
indentationChar = value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class ForNode : NodeType {
|
|||||||
let nodes:[NodeType]
|
let nodes:[NodeType]
|
||||||
let emptyNodes: [NodeType]
|
let emptyNodes: [NodeType]
|
||||||
let `where`: Expression?
|
let `where`: Expression?
|
||||||
|
let token: Token?
|
||||||
|
|
||||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||||
let components = token.components()
|
let components = token.components()
|
||||||
@@ -13,20 +14,25 @@ class ForNode : NodeType {
|
|||||||
func hasToken(_ token: String, at index: Int) -> Bool {
|
func hasToken(_ token: String, at index: Int) -> Bool {
|
||||||
return components.count > (index + 1) && components[index] == token
|
return components.count > (index + 1) && components[index] == token
|
||||||
}
|
}
|
||||||
|
|
||||||
func endsOrHasToken(_ token: String, at index: Int) -> Bool {
|
func endsOrHasToken(_ token: String, at index: Int) -> Bool {
|
||||||
return components.count == index || hasToken(token, at: index)
|
return components.count == index || hasToken(token, at: index)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard hasToken("in", at: 2) && endsOrHasToken("where", at: 4) else {
|
guard hasToken("in", at: 2) && endsOrHasToken("where", at: 4) else {
|
||||||
throw TemplateSyntaxError("'for' statements should use the syntax: `for <x> in <y> [where <condition>]")
|
throw TemplateSyntaxError("'for' statements should use the syntax: `for <x> in <y> [where <condition>]`.")
|
||||||
}
|
}
|
||||||
|
|
||||||
let loopVariables = components[1].characters
|
let loopVariables = components[1]
|
||||||
.split(separator: ",")
|
.split(separator: ",")
|
||||||
.map(String.init)
|
.map(String.init)
|
||||||
.map { $0.trim(character: " ") }
|
.map { $0.trim(character: " ") }
|
||||||
|
|
||||||
var emptyNodes = [NodeType]()
|
let resolvable = try parser.compileResolvable(components[3], containedIn: token)
|
||||||
|
|
||||||
|
let `where` = hasToken("where", at: 4)
|
||||||
|
? try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser, token: token)
|
||||||
|
: nil
|
||||||
|
|
||||||
let forNodes = try parser.parse(until(["endfor", "empty"]))
|
let forNodes = try parser.parse(until(["endfor", "empty"]))
|
||||||
|
|
||||||
@@ -34,26 +40,22 @@ class ForNode : NodeType {
|
|||||||
throw TemplateSyntaxError("`endfor` was not found.")
|
throw TemplateSyntaxError("`endfor` was not found.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var emptyNodes = [NodeType]()
|
||||||
if token.contents == "empty" {
|
if token.contents == "empty" {
|
||||||
emptyNodes = try parser.parse(until(["endfor"]))
|
emptyNodes = try parser.parse(until(["endfor"]))
|
||||||
_ = parser.nextToken()
|
_ = parser.nextToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
let resolvable = try parser.compileResolvable(components[3])
|
return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes: emptyNodes, where: `where`, token: token)
|
||||||
|
|
||||||
let `where` = hasToken("where", at: 4)
|
|
||||||
? try parser.compileExpression(components: Array(components.suffix(from: 5)))
|
|
||||||
: nil
|
|
||||||
|
|
||||||
return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init(resolvable: Resolvable, loopVariables: [String], nodes:[NodeType], emptyNodes:[NodeType], where: Expression? = 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
|
||||||
self.emptyNodes = emptyNodes
|
self.emptyNodes = emptyNodes
|
||||||
self.where = `where`
|
self.where = `where`
|
||||||
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result {
|
func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result {
|
||||||
@@ -91,7 +93,7 @@ class ForNode : NodeType {
|
|||||||
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.map { ($0.key, $0.value) }
|
values = dictionary.sorted { $0.key < $1.key }
|
||||||
} else if let array = resolved as? [Any] {
|
} else if let array = resolved as? [Any] {
|
||||||
values = array
|
values = array
|
||||||
} else if let range = resolved as? CountableClosedRange<Int> {
|
} else if let range = resolved as? CountableClosedRange<Int> {
|
||||||
|
|||||||
@@ -38,10 +38,11 @@ func findOperator(name: String) -> Operator? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
enum IfToken {
|
indirect enum IfToken {
|
||||||
case infix(name: String, bindingPower: Int, op: InfixOperator.Type)
|
case infix(name: String, bindingPower: Int, operatorType: InfixOperator.Type)
|
||||||
case prefix(name: String, bindingPower: Int, op: PrefixOperator.Type)
|
case prefix(name: String, bindingPower: Int, operatorType: PrefixOperator.Type)
|
||||||
case variable(Resolvable)
|
case variable(Resolvable)
|
||||||
|
case subExpression(Expression)
|
||||||
case end
|
case end
|
||||||
|
|
||||||
var bindingPower: Int {
|
var bindingPower: Int {
|
||||||
@@ -52,6 +53,8 @@ enum IfToken {
|
|||||||
return bindingPower
|
return bindingPower
|
||||||
case .variable(_):
|
case .variable(_):
|
||||||
return 0
|
return 0
|
||||||
|
case .subExpression(_):
|
||||||
|
return 0
|
||||||
case .end:
|
case .end:
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -66,6 +69,8 @@ enum IfToken {
|
|||||||
return op.init(expression: expression)
|
return op.init(expression: expression)
|
||||||
case .variable(let variable):
|
case .variable(let variable):
|
||||||
return VariableExpression(variable: variable)
|
return VariableExpression(variable: variable)
|
||||||
|
case .subExpression(let expression):
|
||||||
|
return expression
|
||||||
case .end:
|
case .end:
|
||||||
throw TemplateSyntaxError("'if' expression error: end")
|
throw TemplateSyntaxError("'if' expression error: end")
|
||||||
}
|
}
|
||||||
@@ -80,6 +85,8 @@ enum IfToken {
|
|||||||
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(_):
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
@@ -100,19 +107,69 @@ final class IfExpressionParser {
|
|||||||
let tokens: [IfToken]
|
let tokens: [IfToken]
|
||||||
var position: Int = 0
|
var position: Int = 0
|
||||||
|
|
||||||
init(components: [String], environment: Environment) throws {
|
private init(tokens: [IfToken]) {
|
||||||
self.tokens = try components.map { component in
|
self.tokens = tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
static func parser(components: [String], tokenParser: TokenParser, token: Token) throws -> IfExpressionParser {
|
||||||
|
return try IfExpressionParser(components: ArraySlice(components), tokenParser: tokenParser, token: token)
|
||||||
|
}
|
||||||
|
|
||||||
|
private init(components: ArraySlice<String>, tokenParser: TokenParser, token: Token) throws {
|
||||||
|
var parsedComponents = Set<Int>()
|
||||||
|
var bracketsBalance = 0
|
||||||
|
self.tokens = try zip(components.indices, components).compactMap { (index, component) in
|
||||||
|
guard !parsedComponents.contains(index) else { return nil }
|
||||||
|
|
||||||
|
if component == "(" {
|
||||||
|
bracketsBalance += 1
|
||||||
|
let (expression, parsedCount) = try IfExpressionParser.subExpression(
|
||||||
|
from: components.suffix(from: index + 1),
|
||||||
|
tokenParser: tokenParser,
|
||||||
|
token: token
|
||||||
|
)
|
||||||
|
parsedComponents.formUnion(Set(index...(index + parsedCount)))
|
||||||
|
return .subExpression(expression)
|
||||||
|
} else if component == ")" {
|
||||||
|
bracketsBalance -= 1
|
||||||
|
if bracketsBalance < 0 {
|
||||||
|
throw TemplateSyntaxError("'if' expression error: missing opening bracket")
|
||||||
|
}
|
||||||
|
parsedComponents.insert(index)
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
parsedComponents.insert(index)
|
||||||
if let op = findOperator(name: component) {
|
if let op = findOperator(name: component) {
|
||||||
switch op {
|
switch op {
|
||||||
case .infix(let name, let bindingPower, let cls):
|
case .infix(let name, let bindingPower, let operatorType):
|
||||||
return .infix(name: name, bindingPower: bindingPower, op: cls)
|
return .infix(name: name, bindingPower: bindingPower, operatorType: operatorType)
|
||||||
case .prefix(let name, let bindingPower, let cls):
|
case .prefix(let name, let bindingPower, let operatorType):
|
||||||
return .prefix(name: name, bindingPower: bindingPower, op: cls)
|
return .prefix(name: name, bindingPower: bindingPower, operatorType: operatorType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .variable(try tokenParser.compileResolvable(component, containedIn: token))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return .variable(try environment.compileResolvable(component))
|
private static func subExpression(from components: ArraySlice<String>, tokenParser: TokenParser, token: Token) throws -> (Expression, Int) {
|
||||||
|
var bracketsBalance = 1
|
||||||
|
let subComponents = components
|
||||||
|
.prefix(while: {
|
||||||
|
if $0 == "(" {
|
||||||
|
bracketsBalance += 1
|
||||||
|
} else if $0 == ")" {
|
||||||
|
bracketsBalance -= 1
|
||||||
}
|
}
|
||||||
|
return bracketsBalance != 0
|
||||||
|
})
|
||||||
|
if bracketsBalance > 0 {
|
||||||
|
throw TemplateSyntaxError("'if' expression error: missing closing bracket")
|
||||||
|
}
|
||||||
|
|
||||||
|
let expressionParser = try IfExpressionParser(components: subComponents, tokenParser: tokenParser, token: token)
|
||||||
|
let expression = try expressionParser.parse()
|
||||||
|
return (expression, subComponents.count)
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentToken: IfToken {
|
var currentToken: IfToken {
|
||||||
@@ -176,49 +233,51 @@ final class IfCondition {
|
|||||||
|
|
||||||
class IfNode : NodeType {
|
class IfNode : NodeType {
|
||||||
let conditions: [IfCondition]
|
let conditions: [IfCondition]
|
||||||
|
let token: Token?
|
||||||
|
|
||||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||||
var components = token.components()
|
var components = token.components()
|
||||||
components.removeFirst()
|
components.removeFirst()
|
||||||
|
|
||||||
let expression = try parser.compileExpression(components: components)
|
let expression = try parseExpression(components: components, tokenParser: parser, token: token)
|
||||||
let nodes = try parser.parse(until(["endif", "elif", "else"]))
|
let nodes = try parser.parse(until(["endif", "elif", "else"]))
|
||||||
var conditions: [IfCondition] = [
|
var conditions: [IfCondition] = [
|
||||||
IfCondition(expression: expression, nodes: nodes)
|
IfCondition(expression: expression, nodes: nodes)
|
||||||
]
|
]
|
||||||
|
|
||||||
var token = parser.nextToken()
|
var nextToken = parser.nextToken()
|
||||||
while let current = token, current.contents.hasPrefix("elif") {
|
while let current = nextToken, current.contents.hasPrefix("elif") {
|
||||||
var components = current.components()
|
var components = current.components()
|
||||||
components.removeFirst()
|
components.removeFirst()
|
||||||
let expression = try parser.compileExpression(components: components)
|
let expression = try parseExpression(components: components, tokenParser: parser, token: current)
|
||||||
|
|
||||||
let nodes = try parser.parse(until(["endif", "elif", "else"]))
|
let nodes = try parser.parse(until(["endif", "elif", "else"]))
|
||||||
token = parser.nextToken()
|
nextToken = parser.nextToken()
|
||||||
conditions.append(IfCondition(expression: expression, nodes: nodes))
|
conditions.append(IfCondition(expression: expression, nodes: nodes))
|
||||||
}
|
}
|
||||||
|
|
||||||
if let current = token, current.contents == "else" {
|
if let current = nextToken, current.contents == "else" {
|
||||||
conditions.append(IfCondition(expression: nil, nodes: try parser.parse(until(["endif"]))))
|
conditions.append(IfCondition(expression: nil, nodes: try parser.parse(until(["endif"]))))
|
||||||
token = parser.nextToken()
|
nextToken = parser.nextToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let current = token, current.contents == "endif" else {
|
guard let current = nextToken, current.contents == "endif" else {
|
||||||
throw TemplateSyntaxError("`endif` was not found.")
|
throw TemplateSyntaxError("`endif` was not found.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return IfNode(conditions: conditions)
|
return IfNode(conditions: conditions, token: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType {
|
class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||||
var components = token.components()
|
var components = token.components()
|
||||||
guard components.count == 2 else {
|
guard components.count == 2 else {
|
||||||
throw TemplateSyntaxError("'ifnot' statements should use the following 'ifnot condition' `\(token.contents)`.")
|
throw TemplateSyntaxError("'ifnot' statements should use the following syntax 'ifnot condition'.")
|
||||||
}
|
}
|
||||||
components.removeFirst()
|
components.removeFirst()
|
||||||
var trueNodes = [NodeType]()
|
var trueNodes = [NodeType]()
|
||||||
var falseNodes = [NodeType]()
|
var falseNodes = [NodeType]()
|
||||||
|
|
||||||
|
let expression = try parseExpression(components: components, tokenParser: parser, token: token)
|
||||||
falseNodes = try parser.parse(until(["endif", "else"]))
|
falseNodes = try parser.parse(until(["endif", "else"]))
|
||||||
|
|
||||||
guard let token = parser.nextToken() else {
|
guard let token = parser.nextToken() else {
|
||||||
@@ -230,15 +289,15 @@ class IfNode : NodeType {
|
|||||||
_ = parser.nextToken()
|
_ = parser.nextToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
let expression = try parser.compileExpression(components: components)
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(conditions: [IfCondition]) {
|
init(conditions: [IfCondition], token: Token? = nil) {
|
||||||
self.conditions = conditions
|
self.conditions = conditions
|
||||||
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
|
|||||||
@@ -4,20 +4,26 @@ import PathKit
|
|||||||
class IncludeNode : NodeType {
|
class IncludeNode : NodeType {
|
||||||
let templateName: Variable
|
let templateName: Variable
|
||||||
let includeContext: String?
|
let includeContext: String?
|
||||||
|
let token: Token?
|
||||||
|
|
||||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||||
let bits = token.components()
|
let bits = token.components()
|
||||||
|
|
||||||
guard bits.count == 2 || bits.count == 3 else {
|
guard bits.count == 2 || bits.count == 3 else {
|
||||||
throw TemplateSyntaxError("'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")
|
throw TemplateSyntaxError("""
|
||||||
|
'include' tag requires one argument, the template file to be included. \
|
||||||
|
A second optional argument can be used to specify the context that will \
|
||||||
|
be passed to the included file
|
||||||
|
""")
|
||||||
}
|
}
|
||||||
|
|
||||||
return IncludeNode(templateName: Variable(bits[1]), includeContext: bits.count == 3 ? bits[2] : nil)
|
return IncludeNode(templateName: Variable(bits[1]), includeContext: bits.count == 3 ? bits[2] : nil, token: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(templateName: Variable, includeContext: String? = nil) {
|
init(templateName: Variable, includeContext: String? = nil, token: Token) {
|
||||||
self.templateName = templateName
|
self.templateName = templateName
|
||||||
self.includeContext = includeContext
|
self.includeContext = includeContext
|
||||||
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
@@ -27,10 +33,18 @@ class IncludeNode : NodeType {
|
|||||||
|
|
||||||
let template = try context.environment.loadTemplate(name: templateName)
|
let template = try context.environment.loadTemplate(name: templateName)
|
||||||
|
|
||||||
|
do {
|
||||||
let subContext = includeContext.flatMap { context[$0] as? [String: Any] }
|
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)
|
return try template.render(context)
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
if let error = error as? TemplateSyntaxError {
|
||||||
|
throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens)
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
class BlockContext {
|
class BlockContext {
|
||||||
class var contextKey: String { return "block_context" }
|
class var contextKey: String { return "block_context" }
|
||||||
|
|
||||||
|
// contains mapping of block names to their nodes and templates where they are defined
|
||||||
var blocks: [String: [BlockNode]]
|
var blocks: [String: [BlockNode]]
|
||||||
|
|
||||||
init(blocks: [String: BlockNode]) {
|
init(blocks: [String: BlockNode]) {
|
||||||
self.blocks = [:]
|
self.blocks = [:]
|
||||||
blocks.forEach { (key, value) in
|
blocks.forEach { self.blocks[$0.key] = [$0.value] }
|
||||||
self.blocks[key] = [value]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func push(_ block: BlockNode, forKey blockName: String) {
|
func push(_ block: BlockNode, forKey blockName: String) {
|
||||||
@@ -51,6 +50,7 @@ extension Collection {
|
|||||||
class ExtendsNode : NodeType {
|
class ExtendsNode : NodeType {
|
||||||
let templateName: Variable
|
let templateName: Variable
|
||||||
let blocks: [String:BlockNode]
|
let blocks: [String:BlockNode]
|
||||||
|
let token: Token?
|
||||||
|
|
||||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||||
let bits = token.components()
|
let bits = token.components()
|
||||||
@@ -64,7 +64,7 @@ class ExtendsNode : NodeType {
|
|||||||
throw TemplateSyntaxError("'extends' cannot appear more than once in the same template")
|
throw TemplateSyntaxError("'extends' cannot appear more than once in the same template")
|
||||||
}
|
}
|
||||||
|
|
||||||
let blockNodes = parsedNodes.flatMap { $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
|
||||||
@@ -72,12 +72,13 @@ class ExtendsNode : NodeType {
|
|||||||
return dict
|
return dict
|
||||||
}
|
}
|
||||||
|
|
||||||
return ExtendsNode(templateName: Variable(bits[1]), blocks: nodes)
|
return ExtendsNode(templateName: Variable(bits[1]), blocks: nodes, token: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(templateName: Variable, blocks: [String: BlockNode]) {
|
init(templateName: Variable, blocks: [String: BlockNode], token: Token) {
|
||||||
self.templateName = templateName
|
self.templateName = templateName
|
||||||
self.blocks = blocks
|
self.blocks = blocks
|
||||||
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
@@ -85,21 +86,33 @@ class ExtendsNode : NodeType {
|
|||||||
throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string")
|
throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string")
|
||||||
}
|
}
|
||||||
|
|
||||||
let template = try context.environment.loadTemplate(name: templateName)
|
let baseTemplate = try context.environment.loadTemplate(name: templateName)
|
||||||
|
|
||||||
let blockContext: BlockContext
|
let blockContext: BlockContext
|
||||||
if let context = context[BlockContext.contextKey] as? BlockContext {
|
if let currentBlockContext = context[BlockContext.contextKey] as? BlockContext {
|
||||||
blockContext = context
|
blockContext = currentBlockContext
|
||||||
|
for (name, block) in blocks {
|
||||||
for (key, value) in blocks {
|
blockContext.push(block, forKey: name)
|
||||||
blockContext.push(value, forKey: key)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blockContext = BlockContext(blocks: blocks)
|
blockContext = BlockContext(blocks: blocks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
// pushes base template and renders it's content
|
||||||
|
// block_context contains all blocks from child templates
|
||||||
return try context.push(dictionary: [BlockContext.contextKey: blockContext]) {
|
return try context.push(dictionary: [BlockContext.contextKey: blockContext]) {
|
||||||
return try template.render(context)
|
return try baseTemplate.render(context)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// if error template is already set (see catch in BlockNode)
|
||||||
|
// and it happend in the same template as current template
|
||||||
|
// there is no need to wrap it in another error
|
||||||
|
if let error = error as? TemplateSyntaxError, error.templateName != token?.sourceMap.filename {
|
||||||
|
throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens)
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,6 +121,7 @@ class ExtendsNode : NodeType {
|
|||||||
class BlockNode : NodeType {
|
class BlockNode : NodeType {
|
||||||
let name: String
|
let name: String
|
||||||
let nodes: [NodeType]
|
let nodes: [NodeType]
|
||||||
|
let token: Token?
|
||||||
|
|
||||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||||
let bits = token.components()
|
let bits = token.components()
|
||||||
@@ -119,25 +133,57 @@ class BlockNode : NodeType {
|
|||||||
let blockName = bits[1]
|
let blockName = bits[1]
|
||||||
let nodes = try parser.parse(until(["endblock"]))
|
let nodes = try parser.parse(until(["endblock"]))
|
||||||
_ = parser.nextToken()
|
_ = parser.nextToken()
|
||||||
return BlockNode(name:blockName, nodes:nodes)
|
return BlockNode(name:blockName, nodes:nodes, token: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(name: String, nodes: [NodeType]) {
|
init(name: String, nodes: [NodeType], token: Token) {
|
||||||
self.name = name
|
self.name = name
|
||||||
self.nodes = nodes
|
self.nodes = nodes
|
||||||
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
if let blockContext = context[BlockContext.contextKey] as? BlockContext, let node = blockContext.pop(name) {
|
if let blockContext = context[BlockContext.contextKey] as? BlockContext, let child = blockContext.pop(name) {
|
||||||
let newContext: [String: Any] = [
|
let childContext = try self.childContext(child, blockContext: blockContext, context: context)
|
||||||
BlockContext.contextKey: blockContext,
|
// render extension node
|
||||||
"block": ["super": try self.render(context)]
|
do {
|
||||||
]
|
return try context.push(dictionary: childContext) {
|
||||||
return try context.push(dictionary: newContext) {
|
return try child.render(context)
|
||||||
return try node.render(context)
|
}
|
||||||
|
} catch {
|
||||||
|
throw error.withToken(child.token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return try renderNodes(nodes, context)
|
return try renderNodes(nodes, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// child node is a block node from child template that extends this node (has the same name)
|
||||||
|
func childContext(_ child: BlockNode, blockContext: BlockContext, context: Context) throws -> [String: Any] {
|
||||||
|
var childContext: [String: Any] = [BlockContext.contextKey: blockContext]
|
||||||
|
|
||||||
|
if let blockSuperNode = child.nodes.first(where: {
|
||||||
|
if case .variable(let variable, _)? = $0.token, variable == "block.super" { return true }
|
||||||
|
else { return false}
|
||||||
|
}) {
|
||||||
|
do {
|
||||||
|
// render base node so that its content can be used as part of child node that extends it
|
||||||
|
childContext["block"] = ["super": try self.render(context)]
|
||||||
|
} catch {
|
||||||
|
if let error = error as? TemplateSyntaxError {
|
||||||
|
throw TemplateSyntaxError(
|
||||||
|
reason: error.reason,
|
||||||
|
token: blockSuperNode.token,
|
||||||
|
stackTrace: error.allTokens)
|
||||||
|
} else {
|
||||||
|
throw TemplateSyntaxError(
|
||||||
|
reason: "\(error)",
|
||||||
|
token: blockSuperNode.token,
|
||||||
|
stackTrace: [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return childContext
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ final class KeyPath {
|
|||||||
subscriptLevel = 0
|
subscriptLevel = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
for c in variable.characters {
|
for c in variable {
|
||||||
switch c {
|
switch c {
|
||||||
case "." where subscriptLevel == 0:
|
case "." where subscriptLevel == 0:
|
||||||
try foundSeparator()
|
try foundSeparator()
|
||||||
|
|||||||
@@ -1,16 +1,47 @@
|
|||||||
struct Lexer {
|
import Foundation
|
||||||
let templateString: String
|
|
||||||
|
|
||||||
init(templateString: String) {
|
typealias Line = (content: String, number: UInt, range: Range<String.Index>)
|
||||||
|
|
||||||
|
struct Lexer {
|
||||||
|
let templateName: String?
|
||||||
|
let templateString: String
|
||||||
|
let lines: [Line]
|
||||||
|
|
||||||
|
/// The potential token start characters. In a template these appear after a
|
||||||
|
/// `{` character, for example `{{`, `{%`, `{#`, ...
|
||||||
|
private static let tokenChars: [Unicode.Scalar] = ["{", "%", "#"]
|
||||||
|
|
||||||
|
/// The token end characters, corresponding to their token start characters.
|
||||||
|
/// For example, a variable token starts with `{{` and ends with `}}`
|
||||||
|
private static let tokenCharMap: [Unicode.Scalar: Unicode.Scalar] = [
|
||||||
|
"{": "}",
|
||||||
|
"%": "%",
|
||||||
|
"#": "#"
|
||||||
|
]
|
||||||
|
|
||||||
|
init(templateName: String? = nil, templateString: String) {
|
||||||
|
self.templateName = templateName
|
||||||
self.templateString = templateString
|
self.templateString = templateString
|
||||||
|
|
||||||
|
self.lines = templateString.components(separatedBy: .newlines).enumerated().compactMap {
|
||||||
|
guard !$0.element.isEmpty else { return nil }
|
||||||
|
return (content: $0.element, number: UInt($0.offset + 1), templateString.range(of: $0.element)!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createToken(string: String) -> Token {
|
/// Create a token that will be passed on to the parser, with the given
|
||||||
|
/// content and a range. The content will be tested to see if it's a
|
||||||
|
/// `variable`, a `block` or a `comment`, otherwise it'll default to a simple
|
||||||
|
/// `text` token.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - string: The content string of the token
|
||||||
|
/// - range: The range within the template content, used for smart
|
||||||
|
/// error reporting
|
||||||
|
func createToken(string: String, at range: Range<String.Index>) -> Token {
|
||||||
func strip() -> String {
|
func strip() -> String {
|
||||||
guard string.characters.count > 4 else { return "" }
|
guard string.count > 4 else { return "" }
|
||||||
let start = string.index(string.startIndex, offsetBy: 2)
|
let trimmed = String(string.dropFirst(2).dropLast(2))
|
||||||
let end = string.index(string.endIndex, offsetBy: -2)
|
|
||||||
let trimmed = String(string[start..<end])
|
|
||||||
.components(separatedBy: "\n")
|
.components(separatedBy: "\n")
|
||||||
.filter({ !$0.isEmpty })
|
.filter({ !$0.isEmpty })
|
||||||
.map({ $0.trim(character: " ") })
|
.map({ $0.trim(character: " ") })
|
||||||
@@ -18,112 +49,150 @@ struct Lexer {
|
|||||||
return trimmed
|
return trimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if string.hasPrefix("{{") || string.hasPrefix("{%") || string.hasPrefix("{#") {
|
||||||
|
let value = strip()
|
||||||
|
let range = templateString.range(of: value, range: range) ?? range
|
||||||
|
let location = rangeLocation(range)
|
||||||
|
let sourceMap = SourceMap(filename: templateName, location: location)
|
||||||
|
|
||||||
if string.hasPrefix("{{") {
|
if string.hasPrefix("{{") {
|
||||||
return .variable(value: strip())
|
return .variable(value: value, at: sourceMap)
|
||||||
} else if string.hasPrefix("{%") {
|
} else if string.hasPrefix("{%") {
|
||||||
return .block(value: strip())
|
return .block(value: value, at: sourceMap)
|
||||||
} else if string.hasPrefix("{#") {
|
} else if string.hasPrefix("{#") {
|
||||||
return .comment(value: strip())
|
return .comment(value: value, at: sourceMap)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return .text(value: string)
|
let location = rangeLocation(range)
|
||||||
|
let sourceMap = SourceMap(filename: templateName, location: location)
|
||||||
|
return .text(value: string, at: sourceMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns an array of tokens from a given template string.
|
/// Transforms the template into a list of tokens, that will eventually be
|
||||||
|
/// passed on to the parser.
|
||||||
|
///
|
||||||
|
/// - Returns: The list of tokens (see `createToken(string: at:)`).
|
||||||
func tokenize() -> [Token] {
|
func tokenize() -> [Token] {
|
||||||
var tokens: [Token] = []
|
var tokens: [Token] = []
|
||||||
|
|
||||||
let scanner = Scanner(templateString)
|
let scanner = Scanner(templateString)
|
||||||
|
|
||||||
let map = [
|
|
||||||
"{{": "}}",
|
|
||||||
"{%": "%}",
|
|
||||||
"{#": "#}",
|
|
||||||
]
|
|
||||||
|
|
||||||
while !scanner.isEmpty {
|
while !scanner.isEmpty {
|
||||||
if let text = scanner.scan(until: ["{{", "{%", "{#"]) {
|
if let (char, text) = scanner.scanForTokenStart(Lexer.tokenChars) {
|
||||||
if !text.1.isEmpty {
|
if !text.isEmpty {
|
||||||
tokens.append(createToken(string: text.1))
|
tokens.append(createToken(string: text, at: scanner.range))
|
||||||
}
|
}
|
||||||
|
|
||||||
let end = map[text.0]!
|
guard let end = Lexer.tokenCharMap[char] else { continue }
|
||||||
let result = scanner.scan(until: end, returnUntil: true)
|
let result = scanner.scanForTokenEnd(end)
|
||||||
tokens.append(createToken(string: result))
|
tokens.append(createToken(string: result, at: scanner.range))
|
||||||
} else {
|
} else {
|
||||||
tokens.append(createToken(string: scanner.content))
|
tokens.append(createToken(string: scanner.content, at: scanner.range))
|
||||||
scanner.content = ""
|
scanner.content = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tokens
|
return tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finds the line matching the given range (for a token)
|
||||||
|
///
|
||||||
|
/// - Parameter range: The range to search for.
|
||||||
|
/// - Returns: The content for that line, the line number and offset within
|
||||||
|
/// the line.
|
||||||
|
func rangeLocation(_ range: Range<String.Index>) -> ContentLocation {
|
||||||
|
guard let line = self.lines.first(where: { $0.range.contains(range.lowerBound) }) else {
|
||||||
|
return ("", 0, 0)
|
||||||
|
}
|
||||||
|
let offset = templateString.distance(from: line.range.lowerBound, to: range.lowerBound)
|
||||||
|
return (line.content, line.number, offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
class Scanner {
|
class Scanner {
|
||||||
|
let originalContent: String
|
||||||
var content: String
|
var content: String
|
||||||
|
var range: Range<String.Index>
|
||||||
|
|
||||||
|
/// The start delimiter for a token.
|
||||||
|
private static let tokenStartDelimiter: Unicode.Scalar = "{"
|
||||||
|
/// And the corresponding end delimiter for a token.
|
||||||
|
private static let tokenEndDelimiter: Unicode.Scalar = "}"
|
||||||
|
|
||||||
init(_ content: String) {
|
init(_ content: String) {
|
||||||
|
self.originalContent = content
|
||||||
self.content = content
|
self.content = content
|
||||||
|
range = content.startIndex..<content.startIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
var isEmpty: Bool {
|
var isEmpty: Bool {
|
||||||
return content.isEmpty
|
return content.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
func scan(until: String, returnUntil: Bool = false) -> String {
|
/// Scans for the end of a token, with a specific ending character. If we're
|
||||||
if until.isEmpty {
|
/// searching for the end of a block token `%}`, this method receives a `%`.
|
||||||
return ""
|
/// The scanner will search for that `%` followed by a `}`.
|
||||||
}
|
///
|
||||||
|
/// Note: if the end of a token is found, the `content` and `range`
|
||||||
var index = content.startIndex
|
/// properties are updated to reflect this. `content` will be set to what
|
||||||
while index != content.endIndex {
|
/// remains of the template after the token. `range` will be set to the range
|
||||||
let substring = content.substring(from: index)
|
/// of the token within the template.
|
||||||
|
///
|
||||||
if substring.hasPrefix(until) {
|
/// - Parameter tokenChar: The token end character to search for.
|
||||||
let result = content.substring(to: index)
|
/// - Returns: The content of a token, or "" if no token end was found.
|
||||||
content = substring
|
func scanForTokenEnd(_ tokenChar: Unicode.Scalar) -> String {
|
||||||
|
var foundChar = false
|
||||||
if returnUntil {
|
|
||||||
content = content.substring(from: until.endIndex)
|
|
||||||
return result + until
|
|
||||||
}
|
|
||||||
|
|
||||||
|
for (index, char) in content.unicodeScalars.enumerated() {
|
||||||
|
if foundChar && char == Scanner.tokenEndDelimiter {
|
||||||
|
let result = String(content.prefix(index + 1))
|
||||||
|
content = String(content.dropFirst(index + 1))
|
||||||
|
range = range.upperBound..<originalContent.index(range.upperBound, offsetBy: index + 1)
|
||||||
return result
|
return result
|
||||||
|
} else {
|
||||||
|
foundChar = (char == tokenChar)
|
||||||
}
|
}
|
||||||
|
|
||||||
index = content.index(after: index)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
content = ""
|
content = ""
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func scan(until: [String]) -> (String, String)? {
|
/// Scans for the start of a token, with a list of potential starting
|
||||||
if until.isEmpty {
|
/// characters. To scan for the start of variables (`{{`), blocks (`{%`) and
|
||||||
return nil
|
/// comments (`{#`), this method receives the characters `{`, `%` and `#`.
|
||||||
}
|
/// The scanner will search for a `{`, followed by one of the search
|
||||||
|
/// characters. It will give the found character, and the content that came
|
||||||
|
/// before the token.
|
||||||
|
///
|
||||||
|
/// Note: if the start of a token is found, the `content` and `range`
|
||||||
|
/// properties are updated to reflect this. `content` will be set to what
|
||||||
|
/// remains of the template starting with the token. `range` will be set to
|
||||||
|
/// the start of the token within the template.
|
||||||
|
///
|
||||||
|
/// - Parameter tokenChars: List of token start characters to search for.
|
||||||
|
/// - Returns: The found token start character, together with the content
|
||||||
|
/// before the token, or nil of no token start was found.
|
||||||
|
func scanForTokenStart(_ tokenChars: [Unicode.Scalar]) -> (Unicode.Scalar, String)? {
|
||||||
|
var foundBrace = false
|
||||||
|
|
||||||
var index = content.startIndex
|
range = range.upperBound..<range.upperBound
|
||||||
while index != content.endIndex {
|
for (index, char) in content.unicodeScalars.enumerated() {
|
||||||
let substring = content.substring(from: index)
|
if foundBrace && tokenChars.contains(char) {
|
||||||
for string in until {
|
let result = String(content.prefix(index - 1))
|
||||||
if substring.hasPrefix(string) {
|
content = String(content.dropFirst(index - 1))
|
||||||
let result = content.substring(to: index)
|
range = range.upperBound..<originalContent.index(range.upperBound, offsetBy: index - 1)
|
||||||
content = substring
|
return (char, result)
|
||||||
return (string, result)
|
} else {
|
||||||
|
foundBrace = (char == Scanner.tokenStartDelimiter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
index = content.index(after: index)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
func findFirstNot(character: Character) -> String.Index? {
|
func findFirstNot(character: Character) -> String.Index? {
|
||||||
var index = startIndex
|
var index = startIndex
|
||||||
@@ -157,3 +226,5 @@ extension String {
|
|||||||
return String(self[first..<last])
|
return String(self[first..<last])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public typealias ContentLocation = (content: String, lineNumber: UInt, lineOffset: Int)
|
||||||
|
|||||||
@@ -1,35 +1,31 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
|
|
||||||
public let description:String
|
|
||||||
|
|
||||||
public init(_ description:String) {
|
|
||||||
self.description = description
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool {
|
|
||||||
return lhs.description == rhs.description
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public protocol NodeType {
|
public protocol NodeType {
|
||||||
/// Render the node in the given context
|
/// Render the node in the given context
|
||||||
func render(_ context:Context) throws -> String
|
func render(_ context:Context) throws -> String
|
||||||
|
|
||||||
|
/// Reference to this node's token
|
||||||
|
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 { try $0.render(context) }.joined(separator: "")
|
return try nodes.map {
|
||||||
|
do {
|
||||||
|
return try $0.render(context)
|
||||||
|
} catch {
|
||||||
|
throw error.withToken($0.token)
|
||||||
|
}
|
||||||
|
}.joined(separator: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SimpleNode : NodeType {
|
public class SimpleNode : NodeType {
|
||||||
public let handler:(Context) throws -> String
|
public let handler:(Context) throws -> String
|
||||||
|
public let token: Token?
|
||||||
|
|
||||||
public init(handler: @escaping (Context) throws -> String) {
|
public init(token: Token, handler: @escaping (Context) throws -> String) {
|
||||||
|
self.token = token
|
||||||
self.handler = handler
|
self.handler = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,9 +37,11 @@ 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 init(text:String) {
|
public init(text:String) {
|
||||||
self.text = text
|
self.text = text
|
||||||
|
self.token = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public func render(_ context:Context) throws -> String {
|
public func render(_ context:Context) throws -> String {
|
||||||
@@ -59,16 +57,65 @@ public protocol Resolvable {
|
|||||||
|
|
||||||
public class VariableNode : NodeType {
|
public class VariableNode : NodeType {
|
||||||
public let variable: Resolvable
|
public let variable: Resolvable
|
||||||
|
public var token: Token?
|
||||||
|
let condition: Expression?
|
||||||
|
let elseExpression: Resolvable?
|
||||||
|
|
||||||
public init(variable: Resolvable) {
|
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||||
self.variable = variable
|
var components = token.components()
|
||||||
|
|
||||||
|
func hasToken(_ token: String, at index: Int) -> Bool {
|
||||||
|
return components.count > (index + 1) && components[index] == token
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(variable: String) {
|
let condition: Expression?
|
||||||
|
let elseExpression: Resolvable?
|
||||||
|
|
||||||
|
if hasToken("if", at: 1) {
|
||||||
|
let components = components.suffix(from: 2)
|
||||||
|
if let elseIndex = components.index(of: "else") {
|
||||||
|
condition = try parseExpression(components: Array(components.prefix(upTo: elseIndex)), tokenParser: parser, token: token)
|
||||||
|
let elseToken = components.suffix(from: elseIndex.advanced(by: 1)).joined(separator: " ")
|
||||||
|
elseExpression = try parser.compileResolvable(elseToken, containedIn: token)
|
||||||
|
} else {
|
||||||
|
condition = try parseExpression(components: Array(components), tokenParser: parser, token: token)
|
||||||
|
elseExpression = nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
condition = nil
|
||||||
|
elseExpression = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let filter = try parser.compileResolvable(components[0], containedIn: token)
|
||||||
|
return VariableNode(variable: filter, token: token, condition: condition, elseExpression: elseExpression)
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(variable: Resolvable, token: Token? = nil) {
|
||||||
|
self.variable = variable
|
||||||
|
self.token = token
|
||||||
|
self.condition = nil
|
||||||
|
self.elseExpression = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
init(variable: Resolvable, token: Token? = nil, condition: Expression?, elseExpression: Resolvable?) {
|
||||||
|
self.variable = variable
|
||||||
|
self.token = token
|
||||||
|
self.condition = condition
|
||||||
|
self.elseExpression = elseExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(variable: String, token: Token? = nil) {
|
||||||
self.variable = Variable(variable)
|
self.variable = Variable(variable)
|
||||||
|
self.token = token
|
||||||
|
self.condition = nil
|
||||||
|
self.elseExpression = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public func render(_ context: Context) throws -> String {
|
public func render(_ context: Context) throws -> String {
|
||||||
|
if let condition = self.condition, try condition.evaluate(context: context) == false {
|
||||||
|
return try elseExpression?.resolve(context).map(stringify) ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
let result = try variable.resolve(context)
|
let result = try variable.resolve(context)
|
||||||
return stringify(result)
|
return stringify(result)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,23 +4,25 @@ import Foundation
|
|||||||
|
|
||||||
class NowNode : NodeType {
|
class NowNode : NodeType {
|
||||||
let format:Variable
|
let format:Variable
|
||||||
|
let token: Token?
|
||||||
|
|
||||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||||
var format:Variable?
|
var format:Variable?
|
||||||
|
|
||||||
let components = token.components()
|
let components = token.components()
|
||||||
guard components.count <= 2 else {
|
guard components.count <= 2 else {
|
||||||
throw TemplateSyntaxError("'now' tags may only have one argument: the format string `\(token.contents)`.")
|
throw TemplateSyntaxError("'now' tags may only have one argument: the format string.")
|
||||||
}
|
}
|
||||||
if components.count == 2 {
|
if components.count == 2 {
|
||||||
format = Variable(components[1])
|
format = Variable(components[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
return NowNode(format:format)
|
return NowNode(format:format, token: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(format:Variable?) {
|
init(format:Variable?, token: Token? = nil) {
|
||||||
self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"")
|
self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"")
|
||||||
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
|
|||||||
@@ -37,10 +37,10 @@ public class TokenParser {
|
|||||||
let token = nextToken()!
|
let token = nextToken()!
|
||||||
|
|
||||||
switch token {
|
switch token {
|
||||||
case .text(let text):
|
case .text(let text, _):
|
||||||
nodes.append(TextNode(text: text))
|
nodes.append(TextNode(text: text))
|
||||||
case .variable:
|
case .variable:
|
||||||
nodes.append(VariableNode(variable: try compileResolvable(token.contents)))
|
try nodes.append(VariableNode.parse(self, token: token))
|
||||||
case .block:
|
case .block:
|
||||||
if let parse_until = parse_until , parse_until(self, token) {
|
if let parse_until = parse_until , parse_until(self, token) {
|
||||||
prependToken(token)
|
prependToken(token)
|
||||||
@@ -48,8 +48,13 @@ public class TokenParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let tag = token.components().first {
|
if let tag = token.components().first {
|
||||||
let parser = try environment.findTag(name: tag)
|
do {
|
||||||
nodes.append(try parser(self, token))
|
let parser = try findTag(name: tag)
|
||||||
|
let node = try parser(self, token)
|
||||||
|
nodes.append(node)
|
||||||
|
} catch {
|
||||||
|
throw error.withToken(token)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case .comment:
|
case .comment:
|
||||||
continue
|
continue
|
||||||
@@ -108,7 +113,10 @@ extension Environment {
|
|||||||
if suggestedFilters.isEmpty {
|
if suggestedFilters.isEmpty {
|
||||||
throw TemplateSyntaxError("Unknown filter '\(name)'.")
|
throw TemplateSyntaxError("Unknown filter '\(name)'.")
|
||||||
} else {
|
} else {
|
||||||
throw TemplateSyntaxError("Unknown filter '\(name)'. Found similar filters: \(suggestedFilters.map({ "'\($0)'" }).joined(separator: ", "))")
|
throw TemplateSyntaxError("""
|
||||||
|
Unknown filter '\(name)'. \
|
||||||
|
Found similar filters: \(suggestedFilters.map({ "'\($0)'" }).joined(separator: ", ")).
|
||||||
|
""")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +126,7 @@ extension Environment {
|
|||||||
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.characters.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 []
|
||||||
}
|
}
|
||||||
@@ -126,6 +134,26 @@ extension Environment {
|
|||||||
return filtersWithDistance.filter({ $0.distance == minDistance }).map({ $0.filterName })
|
return filtersWithDistance.filter({ $0.distance == minDistance }).map({ $0.filterName })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func compileFilter(_ filterToken: String, containedIn containingToken: Token) throws -> Resolvable {
|
||||||
|
do {
|
||||||
|
return try FilterExpression(token: filterToken, parser: self)
|
||||||
|
} catch {
|
||||||
|
guard var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
// find offset of filter in the containing token so that only filter is highligted, not the whole token
|
||||||
|
if let filterTokenRange = containingToken.contents.range(of: filterToken) {
|
||||||
|
var location = containingToken.sourceMap.location
|
||||||
|
location.lineOffset += containingToken.contents.distance(from: containingToken.contents.startIndex, to: filterTokenRange.lowerBound)
|
||||||
|
syntaxError.token = .variable(value: filterToken, at: SourceMap(filename: containingToken.sourceMap.filename, location: location))
|
||||||
|
} else {
|
||||||
|
syntaxError.token = containingToken
|
||||||
|
}
|
||||||
|
throw syntaxError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, deprecated, message: "Use compileFilter(_:containedIn:)")
|
||||||
public func compileFilter(_ token: String) throws -> Resolvable {
|
public func compileFilter(_ token: String) throws -> Resolvable {
|
||||||
return try FilterExpression(token: token, environment: self)
|
return try FilterExpression(token: token, environment: self)
|
||||||
}
|
}
|
||||||
@@ -134,11 +162,17 @@ extension Environment {
|
|||||||
return try IfExpressionParser(components: components, environment: self).parse()
|
return try IfExpressionParser(components: components, environment: self).parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(*, deprecated, message: "Use compileResolvable(_:containedIn:)")
|
||||||
public func compileResolvable(_ token: String) throws -> Resolvable {
|
public func compileResolvable(_ token: String) throws -> Resolvable {
|
||||||
return try RangeVariable(token, environment: self)
|
return try RangeVariable(token, environment: self)
|
||||||
?? compileFilter(token)
|
?? compileFilter(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable {
|
||||||
|
return try RangeVariable(token, parser: self, containedIn: containingToken)
|
||||||
|
?? compileFilter(token, containedIn: containingToken)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
|
// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
|
||||||
@@ -155,10 +189,10 @@ extension String {
|
|||||||
// initialize v0 (the previous row of distances)
|
// initialize v0 (the previous row of distances)
|
||||||
// this row is A[0][i]: edit distance for an empty s
|
// this row is A[0][i]: edit distance for an empty s
|
||||||
// the distance is just the number of characters to delete from t
|
// the distance is just the number of characters to delete from t
|
||||||
last = [Int](0...target.characters.count)
|
last = [Int](0...target.count)
|
||||||
current = [Int](repeating: 0, count: target.characters.count + 1)
|
current = [Int](repeating: 0, count: target.count + 1)
|
||||||
|
|
||||||
for i in 0..<self.characters.count {
|
for i 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]
|
||||||
@@ -166,7 +200,7 @@ extension String {
|
|||||||
current[0] = i + 1
|
current[0] = i + 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.characters.count {
|
for j in 0..<target.count {
|
||||||
current[j+1] = Swift.min(
|
current[j+1] = Swift.min(
|
||||||
last[j+1] + 1,
|
last[j+1] + 1,
|
||||||
current[j] + 1,
|
current[j] + 1,
|
||||||
@@ -178,7 +212,7 @@ extension String {
|
|||||||
last = current
|
last = current
|
||||||
}
|
}
|
||||||
|
|
||||||
return current[target.characters.count]
|
return current[target.count]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ let NSFileNoSuchFileError = 4
|
|||||||
|
|
||||||
/// A class representing a template
|
/// A class representing a template
|
||||||
open class Template: ExpressibleByStringLiteral {
|
open class Template: ExpressibleByStringLiteral {
|
||||||
let environment: Environment
|
let templateString: String
|
||||||
|
internal(set) 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
|
||||||
@@ -17,8 +18,9 @@ open class Template: ExpressibleByStringLiteral {
|
|||||||
public required init(templateString: String, environment: Environment? = nil, name: String? = nil) {
|
public required init(templateString: String, environment: Environment? = nil, name: String? = nil) {
|
||||||
self.environment = environment ?? Environment()
|
self.environment = environment ?? Environment()
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.templateString = templateString
|
||||||
|
|
||||||
let lexer = Lexer(templateString: templateString)
|
let lexer = Lexer(templateName: name, templateString: templateString)
|
||||||
tokens = lexer.tokenize()
|
tokens = lexer.tokenize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ extension String {
|
|||||||
let specialCharacters = ",|:"
|
let specialCharacters = ",|:"
|
||||||
func appendWord(_ word: String) {
|
func appendWord(_ word: String) {
|
||||||
if components.count > 0 {
|
if components.count > 0 {
|
||||||
if let precedingChar = components.last?.characters.last, specialCharacters.characters.contains(precedingChar) {
|
if let precedingChar = components.last?.last, specialCharacters.contains(precedingChar) {
|
||||||
components[components.count-1] += word
|
components[components.count-1] += word
|
||||||
} else if specialCharacters.contains(word) {
|
} else if specialCharacters.contains(word) {
|
||||||
components[components.count-1] += word
|
components[components.count-1] += word
|
||||||
@@ -25,7 +25,7 @@ extension String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for character in self.characters {
|
for character in self {
|
||||||
if character == "'" { singleQuoteCount += 1 }
|
if character == "'" { singleQuoteCount += 1 }
|
||||||
else if character == "\"" { doubleQuoteCount += 1 }
|
else if character == "\"" { doubleQuoteCount += 1 }
|
||||||
|
|
||||||
@@ -55,60 +55,63 @@ extension String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct SourceMap: Equatable {
|
||||||
|
public let filename: String?
|
||||||
|
public let location: ContentLocation
|
||||||
|
|
||||||
|
init(filename: String? = nil, location: ContentLocation = ("", 0, 0)) {
|
||||||
|
self.filename = filename
|
||||||
|
self.location = location
|
||||||
|
}
|
||||||
|
|
||||||
|
static let unknown = SourceMap()
|
||||||
|
|
||||||
|
public static func ==(lhs: SourceMap, rhs: SourceMap) -> Bool {
|
||||||
|
return lhs.filename == rhs.filename && lhs.location == rhs.location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum Token : Equatable {
|
public enum Token : Equatable {
|
||||||
/// A token representing a piece of text.
|
/// A token representing a piece of text.
|
||||||
case text(value: String)
|
case text(value: String, at: SourceMap)
|
||||||
|
|
||||||
/// A token representing a variable.
|
/// A token representing a variable.
|
||||||
case variable(value: String)
|
case variable(value: String, at: SourceMap)
|
||||||
|
|
||||||
/// A token representing a comment.
|
/// A token representing a comment.
|
||||||
case comment(value: String)
|
case comment(value: String, at: SourceMap)
|
||||||
|
|
||||||
/// A token representing a template block.
|
/// A token representing a template block.
|
||||||
case block(value: String)
|
case block(value: String, at: SourceMap)
|
||||||
|
|
||||||
/// Returns the underlying value as an array seperated by spaces
|
/// Returns the underlying value as an array seperated by spaces
|
||||||
public func components() -> [String] {
|
public func components() -> [String] {
|
||||||
switch self {
|
switch self {
|
||||||
case .block(let value):
|
case .block(let value, _),
|
||||||
return value.smartSplit()
|
.variable(let value, _),
|
||||||
case .variable(let value):
|
.text(let value, _),
|
||||||
return value.smartSplit()
|
.comment(let value, _):
|
||||||
case .text(let value):
|
|
||||||
return value.smartSplit()
|
|
||||||
case .comment(let value):
|
|
||||||
return value.smartSplit()
|
return value.smartSplit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var contents: String {
|
public var contents: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .block(let value):
|
case .block(let value, _),
|
||||||
|
.variable(let value, _),
|
||||||
|
.text(let value, _),
|
||||||
|
.comment(let value, _):
|
||||||
return value
|
return value
|
||||||
case .variable(let value):
|
|
||||||
return value
|
|
||||||
case .text(let value):
|
|
||||||
return value
|
|
||||||
case .comment(let value):
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var sourceMap: SourceMap {
|
||||||
public func == (lhs: Token, rhs: Token) -> Bool {
|
switch self {
|
||||||
switch (lhs, rhs) {
|
case .block(_, let sourceMap),
|
||||||
case (.text(let lhsValue), .text(let rhsValue)):
|
.variable(_, let sourceMap),
|
||||||
return lhsValue == rhsValue
|
.text(_, let sourceMap),
|
||||||
case (.variable(let lhsValue), .variable(let rhsValue)):
|
.comment(_, let sourceMap):
|
||||||
return lhsValue == rhsValue
|
return sourceMap
|
||||||
case (.block(let lhsValue), .block(let rhsValue)):
|
}
|
||||||
return lhsValue == rhsValue
|
|
||||||
case (.comment(let lhsValue), .comment(let rhsValue)):
|
|
||||||
return lhsValue == rhsValue
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,9 @@ 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, parser: TokenParser) throws {
|
||||||
let bits = token.smartSplit(separator: "|").map({ String($0).trim(character: " ") })
|
let bits = token.split(separator: "|").map({ String($0).trim(character: " ") })
|
||||||
if bits.isEmpty {
|
if bits.isEmpty {
|
||||||
filters = []
|
|
||||||
variable = Variable("")
|
|
||||||
throw TemplateSyntaxError("Variable tags must include at least 1 argument")
|
throw TemplateSyntaxError("Variable tags must include at least 1 argument")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +50,7 @@ public struct Variable : Equatable, Resolvable {
|
|||||||
|
|
||||||
// Split the lookup string and resolve references if possible
|
// Split the lookup string and resolve references if possible
|
||||||
fileprivate func lookup(_ context: Context) throws -> [String] {
|
fileprivate func lookup(_ context: Context) throws -> [String] {
|
||||||
var keyPath = KeyPath(variable, in: context)
|
let keyPath = KeyPath(variable, in: context)
|
||||||
return try keyPath.parse()
|
return try keyPath.parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +60,7 @@ public struct Variable : Equatable, Resolvable {
|
|||||||
|
|
||||||
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.characters.index(after: variable.startIndex) ..< variable.characters.index(before: variable.endIndex)])
|
return String(variable[variable.index(after: variable.startIndex) ..< variable.index(before: variable.endIndex)])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Number literal
|
// Number literal
|
||||||
@@ -89,24 +87,16 @@ public struct Variable : Equatable, Resolvable {
|
|||||||
current = dictionary[bit]
|
current = dictionary[bit]
|
||||||
}
|
}
|
||||||
} else if let array = current as? [Any] {
|
} else if let array = current as? [Any] {
|
||||||
if let index = Int(bit) {
|
current = resolveCollection(array, bit: bit)
|
||||||
if index >= 0 && index < array.count {
|
} else if let string = current as? String {
|
||||||
current = array[index]
|
current = resolveCollection(string, bit: bit)
|
||||||
} else {
|
|
||||||
current = nil
|
|
||||||
}
|
|
||||||
} else if bit == "first" {
|
|
||||||
current = array.first
|
|
||||||
} else if bit == "last" {
|
|
||||||
current = array.last
|
|
||||||
} else if bit == "count" {
|
|
||||||
current = array.count
|
|
||||||
}
|
|
||||||
} else if let object = current as? NSObject { // NSKeyValueCoding
|
} else if let object = current as? NSObject { // NSKeyValueCoding
|
||||||
#if os(Linux)
|
#if os(Linux)
|
||||||
return nil
|
return nil
|
||||||
#else
|
#else
|
||||||
|
if object.responds(to: Selector(bit)) {
|
||||||
current = object.value(forKey: bit)
|
current = object.value(forKey: bit)
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
} else if let value = current {
|
} else if let value = current {
|
||||||
current = Mirror(reflecting: value).getValue(for: bit)
|
current = Mirror(reflecting: value).getValue(for: bit)
|
||||||
@@ -128,8 +118,22 @@ public struct Variable : Equatable, Resolvable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func ==(lhs: Variable, rhs: Variable) -> Bool {
|
private func resolveCollection<T: Collection>(_ collection: T, bit: String) -> Any? {
|
||||||
return lhs.variable == rhs.variable
|
if let index = Int(bit) {
|
||||||
|
if index >= 0 && index < collection.count {
|
||||||
|
return collection[collection.index(collection.startIndex, offsetBy: index)]
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else if bit == "first" {
|
||||||
|
return collection.first
|
||||||
|
} else if bit == "last" {
|
||||||
|
return collection[collection.index(collection.endIndex, offsetBy: -1)]
|
||||||
|
} else if bit == "count" {
|
||||||
|
return collection.count
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A structure used to represet range of two integer values expressed as `from...to`.
|
/// A structure used to represet range of two integer values expressed as `from...to`.
|
||||||
@@ -140,7 +144,8 @@ public struct RangeVariable: Resolvable {
|
|||||||
public let from: Resolvable
|
public let from: Resolvable
|
||||||
public let to: Resolvable
|
public let to: Resolvable
|
||||||
|
|
||||||
public init?(_ token: String, environment: Environment) throws {
|
@available(*, deprecated, message: "Use init?(_:parser:containedIn:)")
|
||||||
|
public init?(_ token: String, parser: TokenParser) throws {
|
||||||
let components = token.components(separatedBy: "...")
|
let components = token.components(separatedBy: "...")
|
||||||
guard components.count == 2 else {
|
guard components.count == 2 else {
|
||||||
return nil
|
return nil
|
||||||
@@ -150,6 +155,16 @@ public struct RangeVariable: Resolvable {
|
|||||||
self.to = try environment.compileFilter(components[1])
|
self.to = try environment.compileFilter(components[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public init?(_ token: String, parser: TokenParser, containedIn containingToken: Token) throws {
|
||||||
|
let components = token.components(separatedBy: "...")
|
||||||
|
guard components.count == 2 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.from = try parser.compileFilter(components[0], containedIn: containingToken)
|
||||||
|
self.to = try parser.compileFilter(components[1], containedIn: containingToken)
|
||||||
|
}
|
||||||
|
|
||||||
public func resolve(_ context: Context) throws -> Any? {
|
public func resolve(_ context: Context) throws -> Any? {
|
||||||
let fromResolved = try from.resolve(context)
|
let fromResolved = try from.resolve(context)
|
||||||
let toResolved = try to.resolve(context)
|
let toResolved = try to.resolve(context)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Stencil",
|
"name": "Stencil",
|
||||||
"version": "0.11.0",
|
"version": "0.13.1",
|
||||||
"summary": "Stencil is a simple and powerful template language for Swift.",
|
"summary": "Stencil is a simple and powerful template language for Swift.",
|
||||||
"homepage": "https://stencil.fuller.li",
|
"homepage": "https://stencil.fuller.li",
|
||||||
"license": {
|
"license": {
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"social_media_url": "https://twitter.com/kylefuller",
|
"social_media_url": "https://twitter.com/kylefuller",
|
||||||
"source": {
|
"source": {
|
||||||
"git": "https://github.com/stencilproject/Stencil.git",
|
"git": "https://github.com/stencilproject/Stencil.git",
|
||||||
"tag": "0.11.0"
|
"tag": "0.13.1"
|
||||||
},
|
},
|
||||||
"source_files": [
|
"source_files": [
|
||||||
"Sources/*.swift"
|
"Sources/*.swift"
|
||||||
@@ -23,10 +23,12 @@
|
|||||||
"osx": "10.9",
|
"osx": "10.9",
|
||||||
"tvos": "9.0"
|
"tvos": "9.0"
|
||||||
},
|
},
|
||||||
|
"cocoapods_version": ">= 1.4.0",
|
||||||
|
"swift_version": "4.2",
|
||||||
"requires_arc": true,
|
"requires_arc": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"PathKit": [
|
"PathKit": [
|
||||||
"~> 0.8.0"
|
"~> 0.9.0"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import XCTest
|
||||||
|
|
||||||
import StencilTests
|
import StencilTests
|
||||||
|
|
||||||
stencilTests()
|
var tests = [XCTestCaseEntry]()
|
||||||
|
tests += StencilTests.__allTests()
|
||||||
|
|
||||||
|
XCTMain(tests)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import XCTest
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
|
|
||||||
|
|
||||||
|
class ContextTests: XCTestCase {
|
||||||
|
|
||||||
func testContext() {
|
func testContext() {
|
||||||
describe("Context") {
|
describe("Context") {
|
||||||
var context: Context!
|
var context: Context!
|
||||||
@@ -79,3 +82,4 @@ func testContext() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
|
import XCTest
|
||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
import PathKit
|
||||||
|
@testable import Stencil
|
||||||
|
|
||||||
|
class EnvironmentTests: XCTestCase {
|
||||||
func testEnvironment() {
|
func testEnvironment() {
|
||||||
describe("Environment") {
|
describe("Environment") {
|
||||||
let environment = Environment(loader: ExampleLoader())
|
var environment: Environment!
|
||||||
|
var template: Template!
|
||||||
|
|
||||||
|
$0.before {
|
||||||
|
environment = Environment(loader: ExampleLoader())
|
||||||
|
template = nil
|
||||||
|
}
|
||||||
|
|
||||||
$0.it("can load a template from a name") {
|
$0.it("can load a template from a name") {
|
||||||
let template = try environment.loadTemplate(name: "example.html")
|
let template = try environment.loadTemplate(name: "example.html")
|
||||||
@@ -32,9 +40,321 @@ func testEnvironment() {
|
|||||||
|
|
||||||
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,
|
||||||
|
file: String = #file, line: Int = #line, function: String = #function) throws {
|
||||||
|
let expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
||||||
|
|
||||||
|
let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"]),
|
||||||
|
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
||||||
|
let reporter = SimpleErrorReporter()
|
||||||
|
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.context("given syntax error") {
|
||||||
|
|
||||||
|
$0.it("reports syntax error on invalid for tag syntax") {
|
||||||
|
template = "Hello {% for name in %}{{ name }}, {% endfor %}!"
|
||||||
|
try expectError(reason: "'for' statements should use the syntax: `for <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") {
|
||||||
|
let path = Path(#file) + ".." + "fixtures"
|
||||||
|
let loader = FileSystemLoader(paths: [path])
|
||||||
|
var environment = Environment(loader: loader)
|
||||||
|
var template: Template!
|
||||||
|
var includedTemplate: Template!
|
||||||
|
|
||||||
|
$0.before {
|
||||||
|
environment = Environment(loader: loader)
|
||||||
|
template = nil
|
||||||
|
includedTemplate = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectError(reason: String, token: String, includedToken: String,
|
||||||
|
file: String = #file, line: Int = #line, function: String = #function) throws {
|
||||||
|
var expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
||||||
|
expectedError.stackTrace = [expectedSyntaxError(token: includedToken, template: includedTemplate, description: reason).token!]
|
||||||
|
|
||||||
|
let error = try expect(environment.render(template: template, context: ["target": "World"]),
|
||||||
|
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
||||||
|
let reporter = SimpleErrorReporter()
|
||||||
|
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports syntax error in included template") {
|
||||||
|
template = Template(templateString: """
|
||||||
|
{% include "invalid-include.html" %}
|
||||||
|
""", environment: environment)
|
||||||
|
includedTemplate = try environment.loadTemplate(name: "invalid-include.html")
|
||||||
|
|
||||||
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||||
|
token: """
|
||||||
|
include "invalid-include.html"
|
||||||
|
""",
|
||||||
|
includedToken: "target|unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports runtime error in included template") {
|
||||||
|
let filterExtension = Extension()
|
||||||
|
filterExtension.registerFilter("unknown", filter: { (_: Any?) in
|
||||||
|
throw TemplateSyntaxError("filter error")
|
||||||
|
})
|
||||||
|
environment.extensions += [filterExtension]
|
||||||
|
|
||||||
|
template = Template(templateString: """
|
||||||
|
{% include "invalid-include.html" %}
|
||||||
|
""", environment: environment)
|
||||||
|
includedTemplate = try environment.loadTemplate(name: "invalid-include.html")
|
||||||
|
|
||||||
|
try expectError(reason: "filter error",
|
||||||
|
token: "include \"invalid-include.html\"",
|
||||||
|
includedToken: "target|unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.context("given base and child templates") {
|
||||||
|
let path = Path(#file) + ".." + "fixtures"
|
||||||
|
let loader = FileSystemLoader(paths: [path])
|
||||||
|
var environment: Environment!
|
||||||
|
var childTemplate: Template!
|
||||||
|
var baseTemplate: Template!
|
||||||
|
|
||||||
|
$0.before {
|
||||||
|
environment = Environment(loader: loader)
|
||||||
|
childTemplate = nil
|
||||||
|
baseTemplate = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectError(reason: String, childToken: String, baseToken: String?,
|
||||||
|
file: String = #file, line: Int = #line, function: String = #function) throws {
|
||||||
|
var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason)
|
||||||
|
if let baseToken = baseToken {
|
||||||
|
expectedError.stackTrace = [expectedSyntaxError(token: baseToken, template: baseTemplate, description: reason).token!]
|
||||||
|
}
|
||||||
|
let error = try expect(environment.render(template: childTemplate, context: ["target": "World"]),
|
||||||
|
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
||||||
|
let reporter = SimpleErrorReporter()
|
||||||
|
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports syntax error in base template") {
|
||||||
|
childTemplate = try environment.loadTemplate(name: "invalid-child-super.html")
|
||||||
|
baseTemplate = try environment.loadTemplate(name: "invalid-base.html")
|
||||||
|
|
||||||
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||||
|
childToken: "extends \"invalid-base.html\"",
|
||||||
|
baseToken: "target|unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports runtime error in base template") {
|
||||||
|
let filterExtension = Extension()
|
||||||
|
filterExtension.registerFilter("unknown", filter: { (_: Any?) in
|
||||||
|
throw TemplateSyntaxError("filter error")
|
||||||
|
})
|
||||||
|
environment.extensions += [filterExtension]
|
||||||
|
|
||||||
|
childTemplate = try environment.loadTemplate(name: "invalid-child-super.html")
|
||||||
|
baseTemplate = try environment.loadTemplate(name: "invalid-base.html")
|
||||||
|
|
||||||
|
try expectError(reason: "filter error",
|
||||||
|
childToken: "block.super",
|
||||||
|
baseToken: "target|unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports syntax error in child template") {
|
||||||
|
childTemplate = Template(templateString: """
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block body %}Child {{ target|unknown }}{% endblock %}
|
||||||
|
""", environment: environment, name: nil)
|
||||||
|
|
||||||
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||||
|
childToken: "target|unknown",
|
||||||
|
baseToken: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports runtime error in child template") {
|
||||||
|
let filterExtension = Extension()
|
||||||
|
filterExtension.registerFilter("unknown", filter: { (_: Any?) in
|
||||||
|
throw TemplateSyntaxError("filter error")
|
||||||
|
})
|
||||||
|
environment.extensions += [filterExtension]
|
||||||
|
|
||||||
|
childTemplate = Template(templateString: """
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block body %}Child {{ target|unknown }}{% endblock %}
|
||||||
|
""", environment: environment, name: nil)
|
||||||
|
|
||||||
|
try expectError(reason: "filter error",
|
||||||
|
childToken: "target|unknown",
|
||||||
|
baseToken: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Expectation {
|
||||||
|
@discardableResult
|
||||||
|
func toThrow<T: Error>() throws -> T {
|
||||||
|
var thrownError: Error? = nil
|
||||||
|
|
||||||
|
do {
|
||||||
|
_ = try expression()
|
||||||
|
} catch {
|
||||||
|
thrownError = error
|
||||||
|
}
|
||||||
|
|
||||||
|
if let thrownError = thrownError {
|
||||||
|
if let thrownError = thrownError as? T {
|
||||||
|
return thrownError
|
||||||
|
} else {
|
||||||
|
throw failure("\(thrownError) is not \(T.self)")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw failure("expression did not throw an error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fileprivate class ExampleLoader: Loader {
|
fileprivate class ExampleLoader: Loader {
|
||||||
func loadTemplate(name: String, environment: Environment) throws -> Template {
|
func loadTemplate(name: String, environment: Environment) throws -> Template {
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
|
import XCTest
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
|
|
||||||
|
class ExpressionsTests: XCTestCase {
|
||||||
func testExpressions() {
|
func testExpressions() {
|
||||||
describe("Expression") {
|
describe("Expression") {
|
||||||
let parser = TokenParser(tokens: [], environment: Environment())
|
let parser = TokenParser(tokens: [], environment: Environment())
|
||||||
|
|
||||||
|
func parseExpression(components: [String]) throws -> Expression {
|
||||||
|
let parser = try IfExpressionParser.parser(components: components, tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||||
|
return try parser.parse()
|
||||||
|
}
|
||||||
|
|
||||||
$0.describe("VariableExpression") {
|
$0.describe("VariableExpression") {
|
||||||
let expression = VariableExpression(variable: Variable("value"))
|
let expression = VariableExpression(variable: Variable("value"))
|
||||||
|
|
||||||
@@ -105,19 +111,19 @@ func testExpressions() {
|
|||||||
|
|
||||||
$0.describe("expression parsing") {
|
$0.describe("expression parsing") {
|
||||||
$0.it("can parse a variable expression") {
|
$0.it("can parse a variable expression") {
|
||||||
let expression = try parser.compileExpression(components: ["value"])
|
let expression = try parseExpression(components: ["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") {
|
$0.it("can parse a not expression") {
|
||||||
let expression = try parser.compileExpression(components: ["not", "value"])
|
let expression = try parseExpression(components: ["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") {
|
$0.describe("and expression") {
|
||||||
let expression = try! parser.compileExpression(components: ["lhs", "and", "rhs"])
|
let expression = try! parseExpression(components: ["lhs", "and", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to false with lhs false") {
|
$0.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()
|
||||||
@@ -137,7 +143,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("or expression") {
|
$0.describe("or expression") {
|
||||||
let expression = try! parser.compileExpression(components: ["lhs", "or", "rhs"])
|
let expression = try! parseExpression(components: ["lhs", "or", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with lhs true") {
|
$0.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()
|
||||||
@@ -157,7 +163,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("equality expression") {
|
$0.describe("equality expression") {
|
||||||
let expression = try! parser.compileExpression(components: ["lhs", "==", "rhs"])
|
let expression = try! parseExpression(components: ["lhs", "==", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with equal lhs/rhs") {
|
$0.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()
|
||||||
@@ -193,7 +199,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("inequality expression") {
|
$0.describe("inequality expression") {
|
||||||
let expression = try! parser.compileExpression(components: ["lhs", "!=", "rhs"])
|
let expression = try! parseExpression(components: ["lhs", "!=", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with inequal lhs/rhs") {
|
$0.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()
|
||||||
@@ -205,7 +211,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("more than expression") {
|
$0.describe("more than expression") {
|
||||||
let expression = try! parser.compileExpression(components: ["lhs", ">", "rhs"])
|
let expression = try! parseExpression(components: ["lhs", ">", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with lhs > rhs") {
|
$0.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()
|
||||||
@@ -217,7 +223,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("more than equal expression") {
|
$0.describe("more than equal expression") {
|
||||||
let expression = try! parser.compileExpression(components: ["lhs", ">=", "rhs"])
|
let expression = try! parseExpression(components: ["lhs", ">=", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with lhs == rhs") {
|
$0.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()
|
||||||
@@ -229,7 +235,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("less than expression") {
|
$0.describe("less than expression") {
|
||||||
let expression = try! parser.compileExpression(components: ["lhs", "<", "rhs"])
|
let expression = try! parseExpression(components: ["lhs", "<", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with lhs < rhs") {
|
$0.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()
|
||||||
@@ -241,7 +247,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("less than equal expression") {
|
$0.describe("less than equal expression") {
|
||||||
let expression = try! parser.compileExpression(components: ["lhs", "<=", "rhs"])
|
let expression = try! parseExpression(components: ["lhs", "<=", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true with lhs == rhs") {
|
$0.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()
|
||||||
@@ -253,7 +259,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("multiple expression") {
|
$0.describe("multiple expression") {
|
||||||
let expression = try! parser.compileExpression(components: ["one", "or", "two", "and", "not", "three"])
|
let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"])
|
||||||
|
|
||||||
$0.it("evaluates to true with one") {
|
$0.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()
|
||||||
@@ -281,7 +287,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("in expression") {
|
$0.describe("in expression") {
|
||||||
let expression = try! parser.compileExpression(components: ["lhs", "in", "rhs"])
|
let expression = try! parseExpression(components: ["lhs", "in", "rhs"])
|
||||||
|
|
||||||
$0.it("evaluates to true when rhs contains lhs") {
|
$0.it("evaluates to true when rhs contains lhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue()
|
||||||
@@ -299,6 +305,42 @@ func testExpressions() {
|
|||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 3, "rhs": 1..<3]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 3, "rhs": 1..<3]))).to.beFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$0.describe("sub expression") {
|
||||||
|
$0.it("evaluates correctly") {
|
||||||
|
let context = Context(dictionary: ["one": false, "two": false, "three": true, "four": true])
|
||||||
|
|
||||||
|
let expression = try! parseExpression(components: ["one", "and", "two", "or", "three", "and", "four"])
|
||||||
|
let expressionWithBrackets = try! parseExpression(components: ["one", "and", "(", "(", "two", ")", "or", "(", "three", "and", "four", ")", ")"])
|
||||||
|
|
||||||
|
try expect(expression.evaluate(context: context)).to.beTrue()
|
||||||
|
try expect(expressionWithBrackets.evaluate(context: context)).to.beFalse()
|
||||||
|
|
||||||
|
let notExpression = try! parseExpression(components: ["not", "one", "or", "three"])
|
||||||
|
let notExpressionWithBrackets = try! parseExpression(components: ["not", "(", "one", "or", "three", ")"])
|
||||||
|
|
||||||
|
try expect(notExpression.evaluate(context: context)).to.beTrue()
|
||||||
|
try expect(notExpressionWithBrackets.evaluate(context: context)).to.beFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("fails when brackets are not balanced") {
|
||||||
|
try expect(parseExpression(components: ["(", "lhs", "and", "rhs"]))
|
||||||
|
.toThrow(TemplateSyntaxError("'if' expression error: missing closing bracket"))
|
||||||
|
try expect(parseExpression(components: [")", "lhs", "and", "rhs"]))
|
||||||
|
.toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket"))
|
||||||
|
try expect(parseExpression(components: ["lhs", "and", "rhs", ")"]))
|
||||||
|
.toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket"))
|
||||||
|
try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", "("]))
|
||||||
|
.toThrow(TemplateSyntaxError("'if' expression error: missing closing bracket"))
|
||||||
|
try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", ")"]))
|
||||||
|
.toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket"))
|
||||||
|
try expect(parseExpression(components: ["(", "lhs", "and", ")"]))
|
||||||
|
.toThrow(TemplateSyntaxError("'if' expression error: end"))
|
||||||
|
try expect(parseExpression(components: ["(", "and", "rhs", ")"]))
|
||||||
|
.toThrow(TemplateSyntaxError("'if' expression error: infix operator 'and' doesn't have a left hand side"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
|
import XCTest
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
|
|
||||||
|
class FilterTests: XCTestCase {
|
||||||
func testFilter() {
|
func testFilter() {
|
||||||
|
|
||||||
func environmentWithFilter(_ name: String, closure: @escaping (Any?) throws -> Any?) -> Environment {
|
|
||||||
let filterExtension = Extension()
|
|
||||||
filterExtension.registerFilter(name, filter: closure)
|
|
||||||
return Environment(extensions: [filterExtension])
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("template filters") {
|
describe("template filters") {
|
||||||
let context: [String: Any] = ["name": "Kyle"]
|
let context: [String: Any] = ["name": "Kyle"]
|
||||||
|
|
||||||
$0.it("allows you to register a custom filter") {
|
$0.it("allows you to register a custom filter") {
|
||||||
let template = Template(templateString: "{{ name|repeat }}")
|
let template = Template(templateString: "{{ name|repeat }}")
|
||||||
let env = environmentWithFilter("repeat") { (value: Any?) in
|
|
||||||
|
let repeatExtension = Extension()
|
||||||
|
repeatExtension.registerFilter("repeat") { (value: Any?) in
|
||||||
if let value = value as? String {
|
if let value = value as? String {
|
||||||
return "\(value) \(value)"
|
return "\(value) \(value)"
|
||||||
}
|
}
|
||||||
@@ -23,12 +19,32 @@ func testFilter() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = try template.render(Context(dictionary: context, environment: env))
|
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") {
|
||||||
|
let repeatExtension = Extension()
|
||||||
|
repeatExtension.registerFilter(name: "isPositive", negativeFilterName: "isNotPositive") { (value: Any?) in
|
||||||
|
if let value = value as? Int {
|
||||||
|
return value > 0
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = try Template(templateString: "{{ value|isPositive }}")
|
||||||
|
.render(Context(dictionary: ["value": 1], environment: Environment(extensions: [repeatExtension])))
|
||||||
|
try expect(result) == "true"
|
||||||
|
|
||||||
|
let negativeResult = try Template(templateString: "{{ value|isNotPositive }}")
|
||||||
|
.render(Context(dictionary: ["value": -1], environment: Environment(extensions: [repeatExtension])))
|
||||||
|
try expect(negativeResult) == "true"
|
||||||
|
}
|
||||||
|
|
||||||
$0.it("allows you to register a custom filter which accepts single argument") {
|
$0.it("allows you to register a custom filter which accepts single argument") {
|
||||||
let template = Template(templateString: "{{ name|repeat:'value1, \"value2\"' }}")
|
let template = Template(templateString: """
|
||||||
|
{{ name|repeat:'value1, "value2"' }}
|
||||||
|
""")
|
||||||
|
|
||||||
let repeatExtension = Extension()
|
let repeatExtension = Extension()
|
||||||
repeatExtension.registerFilter("repeat") { value, arguments in
|
repeatExtension.registerFilter("repeat") { value, arguments in
|
||||||
@@ -40,11 +56,15 @@ func testFilter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
||||||
try expect(result) == "Kyle Kyle with args value1, \"value2\""
|
try expect(result) == """
|
||||||
|
Kyle Kyle with args value1, "value2"
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("allows you to register a custom filter which accepts several arguments") {
|
$0.it("allows you to register a custom filter which accepts several arguments") {
|
||||||
let template = Template(templateString: "{{ name|repeat:'value\"1\"',\"value'2'\",'(key, value)' }}")
|
let template = Template(templateString: """
|
||||||
|
{{ 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
|
||||||
@@ -56,7 +76,9 @@ func testFilter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
||||||
try expect(result) == "Kyle Kyle with args 0: value\"1\", 1: value'2', 2: (key, value)"
|
try expect(result) == """
|
||||||
|
Kyle Kyle with args 0: value"1", 1: value'2', 2: (key, value)
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("allows you to register a custom which throws") {
|
$0.it("allows you to register a custom which throws") {
|
||||||
@@ -67,21 +89,25 @@ func testFilter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let context = Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))
|
let context = Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))
|
||||||
try expect(try template.render(context)).toThrow(TemplateSyntaxError("No Repeat"))
|
try expect(try template.render(context)).toThrow(TemplateSyntaxError(reason: "No Repeat", token: template.tokens.first))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("allows you to override a default filter") {
|
$0.it("allows you to override a default filter") {
|
||||||
let template = Template(templateString: "{{ name|join }}")
|
let template = Template(templateString: "{{ name|join }}")
|
||||||
let env = environmentWithFilter("join") { (value: Any?) in
|
|
||||||
|
let repeatExtension = Extension()
|
||||||
|
repeatExtension.registerFilter("join") { (value: Any?) in
|
||||||
return "joined"
|
return "joined"
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = try template.render(Context(dictionary: context, environment: env))
|
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
||||||
try expect(result) == "joined"
|
try expect(result) == "joined"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("allows whitespace in expression") {
|
$0.it("allows whitespace in expression") {
|
||||||
let template = Template(templateString: "{{ value | join : \", \" }}")
|
let template = Template(templateString: """
|
||||||
|
{{ 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"
|
||||||
}
|
}
|
||||||
@@ -117,25 +143,33 @@ func testFilter() {
|
|||||||
$0.it("transforms a string to be capitalized") {
|
$0.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) == "[\"Kyle\", \"Kyle\"]"
|
try expect(result) == """
|
||||||
|
["Kyle", "Kyle"]
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("transforms a string to be uppercase") {
|
$0.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) == "[\"KYLE\", \"KYLE\"]"
|
try expect(result) == """
|
||||||
|
["KYLE", "KYLE"]
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("transforms a string to be lowercase") {
|
$0.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) == "[\"kyle\", \"kyle\"]"
|
try expect(result) == """
|
||||||
|
["kyle", "kyle"]
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("default filter") {
|
describe("default filter") {
|
||||||
let template = Template(templateString: "Hello {{ name|default:\"World\" }}")
|
let template = Template(templateString: """
|
||||||
|
Hello {{ name|default:"World" }}
|
||||||
|
""")
|
||||||
|
|
||||||
$0.it("shows the variable value") {
|
$0.it("shows the variable value") {
|
||||||
let result = try template.render(Context(dictionary: ["name": "Kyle"]))
|
let result = try template.render(Context(dictionary: ["name": "Kyle"]))
|
||||||
@@ -148,7 +182,9 @@ func testFilter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("supports multiple defaults") {
|
$0.it("supports multiple defaults") {
|
||||||
let template = Template(templateString: "Hello {{ name|default:a,b,c,\"World\" }}")
|
let template = Template(templateString: """
|
||||||
|
Hello {{ name|default:a,b,c,"World" }}
|
||||||
|
""")
|
||||||
let result = try template.render(Context(dictionary: [:]))
|
let result = try template.render(Context(dictionary: [:]))
|
||||||
try expect(result) == "Hello World"
|
try expect(result) == "Hello World"
|
||||||
}
|
}
|
||||||
@@ -166,7 +202,9 @@ func testFilter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("checks for underlying nil value correctly") {
|
$0.it("checks for underlying nil value correctly") {
|
||||||
let template = Template(templateString: "Hello {{ user.name|default:\"anonymous\" }}")
|
let template = Template(templateString: """
|
||||||
|
Hello {{ user.name|default:"anonymous" }}
|
||||||
|
""")
|
||||||
let nilName: String? = nil
|
let nilName: String? = nil
|
||||||
let user: [String: Any?] = ["name": nilName]
|
let user: [String: Any?] = ["name": nilName]
|
||||||
let result = try template.render(Context(dictionary: ["user": user]))
|
let result = try template.render(Context(dictionary: ["user": user]))
|
||||||
@@ -175,7 +213,9 @@ func testFilter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("join filter") {
|
describe("join filter") {
|
||||||
let template = Template(templateString: "{{ value|join:\", \" }}")
|
let template = Template(templateString: """
|
||||||
|
{{ value|join:", " }}
|
||||||
|
""")
|
||||||
|
|
||||||
$0.it("joins a collection of strings") {
|
$0.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"]]))
|
||||||
@@ -188,87 +228,163 @@ func testFilter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can join by non string") {
|
$0.it("can join by non string") {
|
||||||
let template = Template(templateString: "{{ value|join:separator }}")
|
let template = Template(templateString: """
|
||||||
|
{{ value|join:separator }}
|
||||||
|
""")
|
||||||
let result = try template.render(Context(dictionary: ["value": ["One", "Two"], "separator": true]))
|
let result = try template.render(Context(dictionary: ["value": ["One", "Two"], "separator": true]))
|
||||||
try expect(result) == "OnetrueTwo"
|
try expect(result) == "OnetrueTwo"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can join without arguments") {
|
$0.it("can join without arguments") {
|
||||||
let template = Template(templateString: "{{ value|join }}")
|
let template = Template(templateString: """
|
||||||
|
{{ 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) == "OneTwo"
|
try expect(result) == "OneTwo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("split filter") {
|
describe("split filter") {
|
||||||
let template = Template(templateString: "{{ value|split:\", \" }}")
|
let template = Template(templateString: """
|
||||||
|
{{ value|split:", " }}
|
||||||
|
""")
|
||||||
|
|
||||||
$0.it("split a string into array") {
|
$0.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) == "[\"One\", \"Two\"]"
|
try expect(result) == """
|
||||||
|
["One", "Two"]
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can split without arguments") {
|
$0.it("can split without arguments") {
|
||||||
let template = Template(templateString: "{{ value|split }}")
|
let template = Template(templateString: """
|
||||||
|
{{ value|split }}
|
||||||
|
""")
|
||||||
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"]
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
describe("filter suggestion") {
|
describe("filter suggestion") {
|
||||||
|
var template: Template!
|
||||||
|
var filterExtension: Extension!
|
||||||
|
|
||||||
|
func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError {
|
||||||
|
guard let range = template.templateString.range(of: token) else {
|
||||||
|
fatalError("Can't find '\(token)' in '\(template)'")
|
||||||
|
}
|
||||||
|
let lexer = Lexer(templateString: template.templateString)
|
||||||
|
let location = lexer.rangeLocation(range)
|
||||||
|
let sourceMap = SourceMap(filename: template.name, location: location)
|
||||||
|
let token = Token.block(value: token, at: sourceMap)
|
||||||
|
return TemplateSyntaxError(reason: description, token: token, stackTrace: [])
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectError(reason: String, token: String,
|
||||||
|
file: String = #file, line: Int = #line, function: String = #function) throws {
|
||||||
|
let expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
||||||
|
let environment = Environment(extensions: [filterExtension])
|
||||||
|
|
||||||
|
let error = try expect(environment.render(template: template, context: [:]),
|
||||||
|
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
||||||
|
let reporter = SimpleErrorReporter()
|
||||||
|
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
||||||
|
}
|
||||||
|
|
||||||
$0.it("made for unknown filter") {
|
$0.it("made for unknown filter") {
|
||||||
let template = Template(templateString: "{{ value|unknownFilter }}")
|
template = Template(templateString: "{{ value|unknownFilter }}")
|
||||||
let expectedError = TemplateSyntaxError("Unknown filter 'unknownFilter'. Found similar filters: 'knownFilter'")
|
|
||||||
|
|
||||||
let filterExtension = Extension()
|
filterExtension = Extension()
|
||||||
filterExtension.registerFilter("knownFilter") { value, _ in value }
|
filterExtension.registerFilter("knownFilter") { value, _ in value }
|
||||||
|
|
||||||
try expect(template.render(Context(dictionary: [:], environment: Environment(extensions: [filterExtension])))).toThrow(expectedError)
|
try expectError(reason: "Unknown filter 'unknownFilter'. Found similar filters: 'knownFilter'.", token: "value|unknownFilter")
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("made for multiple similar filters") {
|
$0.it("made for multiple similar filters") {
|
||||||
let template = Template(templateString: "{{ value|lowerFirst }}")
|
template = Template(templateString: "{{ value|lowerFirst }}")
|
||||||
let expectedError = TemplateSyntaxError("Unknown filter 'lowerFirst'. Found similar filters: 'lowerFirstWord', 'lowercase'")
|
|
||||||
|
|
||||||
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 expect(template.render(Context(dictionary: [:], environment: Environment(extensions: [filterExtension])))).toThrow(expectedError)
|
try expectError(reason: "Unknown filter 'lowerFirst'. Found similar filters: 'lowerFirstWord', 'lowercase'.", token: "value|lowerFirst")
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("not made when can't find similar filter") {
|
$0.it("not made when can't find similar filter") {
|
||||||
let template = Template(templateString: "{{ value|unknownFilter }}")
|
template = Template(templateString: "{{ value|unknownFilter }}")
|
||||||
let expectedError = TemplateSyntaxError("Unknown filter 'unknownFilter'.")
|
try expectError(reason: "Unknown filter 'unknownFilter'. Found similar filters: 'lowerFirstWord'.", token: "value|unknownFilter")
|
||||||
try expect(template.render(Context(dictionary: [:]))).toThrow(expectedError)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
describe("indent filter") {
|
describe("indent filter") {
|
||||||
$0.it("indents content") {
|
$0.it("indents content") {
|
||||||
let template = Template(templateString: "{{ value|indent:2 }}")
|
let template = Template(templateString: """
|
||||||
let result = try template.render(Context(dictionary: ["value": "One\nTwo"]))
|
{{ value|indent:2 }}
|
||||||
try expect(result) == "One\n Two"
|
""")
|
||||||
|
let result = try template.render(Context(dictionary: ["value": """
|
||||||
|
One
|
||||||
|
Two
|
||||||
|
"""]))
|
||||||
|
try expect(result) == """
|
||||||
|
One
|
||||||
|
Two
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can indent with arbitrary character") {
|
$0.it("can indent with arbitrary character") {
|
||||||
let template = Template(templateString: "{{ value|indent:2,\"\t\" }}")
|
let template = Template(templateString: """
|
||||||
let result = try template.render(Context(dictionary: ["value": "One\nTwo"]))
|
{{ value|indent:2,"\t" }}
|
||||||
try expect(result) == "One\n\t\tTwo"
|
""")
|
||||||
|
let result = try template.render(Context(dictionary: ["value": """
|
||||||
|
One
|
||||||
|
Two
|
||||||
|
"""]))
|
||||||
|
try expect(result) == """
|
||||||
|
One
|
||||||
|
\t\tTwo
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can indent first line") {
|
$0.it("can indent first line") {
|
||||||
let template = Template(templateString: "{{ value|indent:2,\" \",true }}")
|
let template = Template(templateString: """
|
||||||
let result = try template.render(Context(dictionary: ["value": "One\nTwo"]))
|
{{ value|indent:2," ",true }}
|
||||||
try expect(result) == " One\n Two"
|
""")
|
||||||
|
let result = try template.render(Context(dictionary: ["value": """
|
||||||
|
One
|
||||||
|
Two
|
||||||
|
"""]))
|
||||||
|
try expect(result) == """
|
||||||
|
One
|
||||||
|
Two
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("does not indent empty lines") {
|
$0.it("does not indent empty lines") {
|
||||||
let template = Template(templateString: "{{ value|indent }}")
|
let template = Template(templateString: """
|
||||||
let result = try template.render(Context(dictionary: ["value": "One\n\n\nTwo\n\n"]))
|
{{ value|indent }}
|
||||||
try expect(result) == "One\n\n\n Two\n\n"
|
""")
|
||||||
|
let result = try template.render(Context(dictionary: ["value": """
|
||||||
|
One
|
||||||
|
|
||||||
|
|
||||||
|
Two
|
||||||
|
|
||||||
|
|
||||||
|
"""]))
|
||||||
|
try expect(result) == """
|
||||||
|
One
|
||||||
|
|
||||||
|
|
||||||
|
Two
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import XCTest
|
||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
import Stencil
|
||||||
|
|
||||||
|
class FilterTagTests: XCTestCase {
|
||||||
func testFilterTag() {
|
func testFilterTag() {
|
||||||
describe("Filter Tag") {
|
describe("Filter Tag") {
|
||||||
$0.it("allows you to use a filter") {
|
$0.it("allows you to use a filter") {
|
||||||
@@ -17,7 +18,7 @@ func testFilterTag() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("errors without a filter") {
|
$0.it("errors without a filter") {
|
||||||
let template = Template(templateString: "{% filter %}Test{% endfilter %}")
|
let template = Template(templateString: "Some {% filter %}Test{% endfilter %}")
|
||||||
try expect(try template.render()).toThrow()
|
try expect(try template.render()).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +28,9 @@ func testFilterTag() {
|
|||||||
return ($0 as! String).components(separatedBy: $1[0] as! String)
|
return ($0 as! String).components(separatedBy: $1[0] as! String)
|
||||||
})
|
})
|
||||||
let env = Environment(extensions: [ext])
|
let env = Environment(extensions: [ext])
|
||||||
let result = try env.renderTemplate(string: "{% filter split:\",\"|join:\";\" %}{{ items|join:\",\" }}{% endfilter %}", context: ["items": [1, 2]])
|
let result = try env.renderTemplate(string: """
|
||||||
|
{% filter split:","|join:";" %}{{ items|join:"," }}{% endfilter %}
|
||||||
|
""", context: ["items": [1, 2]])
|
||||||
try expect(result) == "1;2"
|
try expect(result) == "1;2"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,8 +41,11 @@ func testFilterTag() {
|
|||||||
return ($0 as! String).replacingOccurrences(of: $1[0] as! String, with: $1[1] as! String)
|
return ($0 as! String).replacingOccurrences(of: $1[0] as! String, with: $1[1] as! String)
|
||||||
})
|
})
|
||||||
let env = Environment(extensions: [ext])
|
let env = Environment(extensions: [ext])
|
||||||
let result = try env.renderTemplate(string: "{% filter replace:'\"',\"\" %}{{ items|join:\",\" }}{% endfilter %}", context: ["items": ["\"1\"", "\"2\""]])
|
let result = try env.renderTemplate(string: """
|
||||||
|
{% filter replace:'"',"" %}{{ items|join:"," }}{% endfilter %}
|
||||||
|
""", context: ["items": ["\"1\"", "\"2\""]])
|
||||||
try expect(result) == "1,2"
|
try expect(result) == "1,2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import XCTest
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
class ForNodeTests: XCTestCase {
|
||||||
func testForNode() {
|
func testForNode() {
|
||||||
describe("ForNode") {
|
describe("ForNode") {
|
||||||
let context = Context(dictionary: [
|
let context = Context(dictionary: [
|
||||||
@@ -98,8 +99,7 @@ func testForNode() {
|
|||||||
|
|
||||||
$0.it("renders the given nodes while filtering items using where expression") {
|
$0.it("renders the given nodes while filtering items using where expression") {
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
|
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
|
||||||
let parser = TokenParser(tokens: [], environment: Environment())
|
let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown))
|
||||||
let `where` = try parser.compileExpression(components: ["item", ">", "1"])
|
|
||||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`)
|
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`)
|
||||||
try expect(try node.render(context)) == "2132"
|
try expect(try node.render(context)) == "2132"
|
||||||
}
|
}
|
||||||
@@ -107,16 +107,17 @@ func testForNode() {
|
|||||||
$0.it("renders the given empty nodes when all items filtered out with where expression") {
|
$0.it("renders the given empty nodes when all items filtered out with where expression") {
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||||
let parser = TokenParser(tokens: [], environment: Environment())
|
let `where` = try parseExpression(components: ["item", "==", "0"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown))
|
||||||
let `where` = try parser.compileExpression(components: ["item", "==", "0"])
|
|
||||||
let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`)
|
let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`)
|
||||||
try expect(try node.render(context)) == "empty"
|
try expect(try node.render(context)) == "empty"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can render a filter with spaces") {
|
$0.it("can render a filter with spaces") {
|
||||||
let templateString = "{% for article in ars | default: a, b , articles %}" +
|
let templateString = """
|
||||||
"- {{ article.title }} by {{ article.author }}.\n" +
|
{% for article in ars | default: a, b , articles %}\
|
||||||
"{% endfor %}\n"
|
- {{ article.title }} by {{ article.author }}.
|
||||||
|
{% endfor %}
|
||||||
|
"""
|
||||||
|
|
||||||
let context = Context(dictionary: [
|
let context = Context(dictionary: [
|
||||||
"articles": [
|
"articles": [
|
||||||
@@ -128,54 +129,70 @@ func testForNode() {
|
|||||||
let template = Template(templateString: templateString)
|
let template = Template(templateString: templateString)
|
||||||
let result = try template.render(context)
|
let result = try template.render(context)
|
||||||
|
|
||||||
let fixture = "" +
|
try expect(result) == """
|
||||||
"- Migrating from OCUnit to XCTest by Kyle Fuller.\n" +
|
- Migrating from OCUnit to XCTest by Kyle Fuller.
|
||||||
"- Memory Management with ARC by Kyle Fuller.\n" +
|
- Memory Management with ARC by Kyle Fuller.
|
||||||
"\n"
|
|
||||||
|
|
||||||
try expect(result) == fixture
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.context("given array of tuples") {
|
$0.context("given array of tuples") {
|
||||||
$0.it("can iterate over all tuple values") {
|
$0.it("can iterate over all tuple values") {
|
||||||
let templateString = "{% for first,second,third in tuples %}" +
|
let templateString = """
|
||||||
"{{ first }}, {{ second }}, {{ third }}\n" +
|
{% for first,second,third in tuples %}\
|
||||||
"{% endfor %}\n"
|
{{ first }}, {{ second }}, {{ third }}
|
||||||
|
{% endfor %}
|
||||||
|
"""
|
||||||
|
|
||||||
let template = Template(templateString: templateString)
|
let template = Template(templateString: templateString)
|
||||||
let result = try template.render(context)
|
let result = try template.render(context)
|
||||||
|
|
||||||
let fixture = "1, 2, 3\n4, 5, 6\n\n"
|
try expect(result) == """
|
||||||
try expect(result) == fixture
|
1, 2, 3
|
||||||
|
4, 5, 6
|
||||||
|
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can iterate with less number of variables") {
|
$0.it("can iterate with less number of variables") {
|
||||||
let templateString = "{% for first,second in tuples %}" +
|
let templateString = """
|
||||||
"{{ first }}, {{ second }}\n" +
|
{% for first,second in tuples %}\
|
||||||
"{% endfor %}\n"
|
{{ first }}, {{ second }}
|
||||||
|
{% endfor %}
|
||||||
|
"""
|
||||||
|
|
||||||
let template = Template(templateString: templateString)
|
let template = Template(templateString: templateString)
|
||||||
let result = try template.render(context)
|
let result = try template.render(context)
|
||||||
|
|
||||||
let fixture = "1, 2\n4, 5\n\n"
|
try expect(result) == """
|
||||||
try expect(result) == fixture
|
1, 2
|
||||||
|
4, 5
|
||||||
|
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can use _ to skip variables") {
|
$0.it("can use _ to skip variables") {
|
||||||
let templateString = "{% for first,_,third in tuples %}" +
|
let templateString = """
|
||||||
"{{ first }}, {{ third }}\n" +
|
{% for first,_,third in tuples %}\
|
||||||
"{% endfor %}\n"
|
{{ first }}, {{ third }}
|
||||||
|
{% endfor %}
|
||||||
|
"""
|
||||||
|
|
||||||
let template = Template(templateString: templateString)
|
let template = Template(templateString: templateString)
|
||||||
let result = try template.render(context)
|
let result = try template.render(context)
|
||||||
|
|
||||||
let fixture = "1, 3\n4, 6\n\n"
|
try expect(result) == """
|
||||||
try expect(result) == fixture
|
1, 3
|
||||||
|
4, 6
|
||||||
|
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws when number of variables is more than number of tuple values") {
|
$0.it("throws when number of variables is more than number of tuple values") {
|
||||||
let templateString = "{% for key,value,smth in dict %}" +
|
let templateString = """
|
||||||
"{% endfor %}\n"
|
{% for key,value,smth in dict %}
|
||||||
|
{% endfor %}
|
||||||
|
"""
|
||||||
|
|
||||||
let template = Template(templateString: templateString)
|
let template = Template(templateString: templateString)
|
||||||
try expect(template.render(context)).toThrow()
|
try expect(template.render(context)).toThrow()
|
||||||
@@ -184,15 +201,18 @@ func testForNode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can iterate over dictionary") {
|
$0.it("can iterate over dictionary") {
|
||||||
let templateString = "{% for key, value in dict %}" +
|
let templateString = """
|
||||||
"{{ key }}: {{ value }}," +
|
{% for key, value in dict %}\
|
||||||
"{% endfor %}"
|
{{ key }}: {{ value }},\
|
||||||
|
{% endfor %}
|
||||||
|
"""
|
||||||
|
|
||||||
let template = Template(templateString: templateString)
|
let template = Template(templateString: templateString)
|
||||||
let result = try template.render(context)
|
let result = try template.render(context)
|
||||||
|
|
||||||
let sortedResult = result.characters.split(separator: ",").map(String.init).sorted(by: <)
|
try expect(result) == """
|
||||||
try expect(sortedResult) == ["one: I", "two: II"]
|
one: I,two: II,
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("renders supports iterating over dictionary") {
|
$0.it("renders supports iterating over dictionary") {
|
||||||
@@ -204,8 +224,9 @@ func testForNode() {
|
|||||||
let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key"], nodes: nodes, emptyNodes: emptyNodes, where: nil)
|
let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key"], nodes: nodes, emptyNodes: emptyNodes, where: nil)
|
||||||
let result = try node.render(context)
|
let result = try node.render(context)
|
||||||
|
|
||||||
let sortedResult = result.characters.split(separator: ",").map(String.init).sorted(by: <)
|
try expect(result) == """
|
||||||
try expect(sortedResult) == ["one", "two"]
|
one,two,
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("renders supports iterating over dictionary") {
|
$0.it("renders supports iterating over dictionary") {
|
||||||
@@ -217,19 +238,17 @@ func testForNode() {
|
|||||||
]
|
]
|
||||||
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(resolvable: Variable("dict"), loopVariables: ["key", "value"], nodes: nodes, emptyNodes: emptyNodes, where: nil)
|
||||||
|
|
||||||
let result = try node.render(context)
|
let result = try node.render(context)
|
||||||
|
|
||||||
let sortedResult = result.characters.split(separator: ",").map(String.init).sorted(by: <)
|
try expect(result) == """
|
||||||
try expect(sortedResult) == ["one=I", "two=II"]
|
one=I,two=II,
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("handles invalid input") {
|
$0.it("handles invalid input") {
|
||||||
let tokens: [Token] = [
|
let token = Token.block(value: "for i", at: .unknown)
|
||||||
.block(value: "for i"),
|
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)
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
|
||||||
let error = TemplateSyntaxError("'for' statements should use the syntax: `for <x> in <y> [where <condition>]")
|
|
||||||
try expect(try parser.parse()).toThrow(error)
|
try expect(try parser.parse()).toThrow(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +271,11 @@ func testForNode() {
|
|||||||
let node = ForNode(resolvable: Variable("struct"), loopVariables: ["property", "value"], nodes: nodes, emptyNodes: [])
|
let node = ForNode(resolvable: Variable("struct"), loopVariables: ["property", "value"], nodes: nodes, emptyNodes: [])
|
||||||
let result = try node.render(context)
|
let result = try node.render(context)
|
||||||
|
|
||||||
try expect(result) == "string=abc\nnumber=123\n"
|
try expect(result) == """
|
||||||
|
string=abc
|
||||||
|
number=123
|
||||||
|
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can iterate tuple items") {
|
$0.it("can iterate tuple items") {
|
||||||
@@ -270,7 +293,11 @@ func testForNode() {
|
|||||||
let node = ForNode(resolvable: Variable("tuple"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: [])
|
let node = ForNode(resolvable: Variable("tuple"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: [])
|
||||||
let result = try node.render(context)
|
let result = try node.render(context)
|
||||||
|
|
||||||
try expect(result) == "one=1\ntwo=dva\n"
|
try expect(result) == """
|
||||||
|
one=1
|
||||||
|
two=dva
|
||||||
|
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can iterate over class properties") {
|
$0.it("can iterate over class properties") {
|
||||||
@@ -305,7 +332,12 @@ func testForNode() {
|
|||||||
let node = ForNode(resolvable: Variable("class"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: [])
|
let node = ForNode(resolvable: Variable("class"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: [])
|
||||||
let result = try node.render(context)
|
let result = try node.render(context)
|
||||||
|
|
||||||
try expect(result) == "childString=child\nbaseString=base\nbaseInt=1\n"
|
try expect(result) == """
|
||||||
|
childString=child
|
||||||
|
baseString=base
|
||||||
|
baseInt=1
|
||||||
|
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can iterate in range of variables") {
|
$0.it("can iterate in range of variables") {
|
||||||
@@ -316,7 +348,7 @@ func testForNode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fileprivate struct Article {
|
fileprivate struct Article {
|
||||||
let title: String
|
let title: String
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
|
import XCTest
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
|
|
||||||
|
class IfNodeTests: XCTestCase {
|
||||||
func testIfNode() {
|
func testIfNode() {
|
||||||
describe("IfNode") {
|
describe("IfNode") {
|
||||||
$0.describe("parsing") {
|
$0.describe("parsing") {
|
||||||
$0.it("can parse an if block") {
|
$0.it("can parse an if block") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value"),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -25,11 +26,11 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("can parse an if with else block") {
|
$0.it("can parse an if with else block") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value"),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "else"),
|
.block(value: "else", at: .unknown),
|
||||||
.text(value: "false"),
|
.text(value: "false", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -50,13 +51,13 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("can parse an if with elif block") {
|
$0.it("can parse an if with elif block") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value"),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "elif something"),
|
.block(value: "elif something", at: .unknown),
|
||||||
.text(value: "some"),
|
.text(value: "some", at: .unknown),
|
||||||
.block(value: "else"),
|
.block(value: "else", at: .unknown),
|
||||||
.text(value: "false"),
|
.text(value: "false", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -81,11 +82,11 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("can parse an if with elif block without else") {
|
$0.it("can parse an if with elif block without else") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value"),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "elif something"),
|
.block(value: "elif something", at: .unknown),
|
||||||
.text(value: "some"),
|
.text(value: "some", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -106,15 +107,15 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("can parse an if with multiple elif block") {
|
$0.it("can parse an if with multiple elif block") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value"),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "elif something1"),
|
.block(value: "elif something1", at: .unknown),
|
||||||
.text(value: "some1"),
|
.text(value: "some1", at: .unknown),
|
||||||
.block(value: "elif something2"),
|
.block(value: "elif something2", at: .unknown),
|
||||||
.text(value: "some2"),
|
.text(value: "some2", at: .unknown),
|
||||||
.block(value: "else"),
|
.block(value: "else", at: .unknown),
|
||||||
.text(value: "false"),
|
.text(value: "false", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -144,9 +145,9 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("can parse an if with complex expression") {
|
$0.it("can parse an if with complex expression") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value == \"test\" and not name"),
|
.block(value: "if value == \"test\" and not name", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -156,11 +157,11 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("can parse an ifnot block") {
|
$0.it("can parse an ifnot block") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "ifnot value"),
|
.block(value: "ifnot value", at: .unknown),
|
||||||
.text(value: "false"),
|
.text(value: "false", at: .unknown),
|
||||||
.block(value: "else"),
|
.block(value: "else", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -179,22 +180,18 @@ func testIfNode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws an error when parsing an if block without an endif") {
|
$0.it("throws an error when parsing an if block without an endif") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [.block(value: "if value", at: .unknown)]
|
||||||
.block(value: "if value"),
|
|
||||||
]
|
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
let error = TemplateSyntaxError("`endif` was not found.")
|
let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first)
|
||||||
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") {
|
$0.it("throws an error when parsing an ifnot without an endif") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [.block(value: "ifnot value", at: .unknown)]
|
||||||
.block(value: "ifnot value"),
|
|
||||||
]
|
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
let error = TemplateSyntaxError("`endif` was not found.")
|
let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first)
|
||||||
try expect(try parser.parse()).toThrow(error)
|
try expect(try parser.parse()).toThrow(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,9 +239,9 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("supports variable filters in the if expression") {
|
$0.it("supports variable filters in the if expression") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value|uppercase == \"TEST\""),
|
.block(value: "if value|uppercase == \"TEST\"", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -256,9 +253,9 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("evaluates nil properties as false") {
|
$0.it("evaluates nil properties as false") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if instance.value"),
|
.block(value: "if instance.value", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -273,11 +270,11 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("supports closed range variables") {
|
$0.it("supports closed range variables") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value in 1...3"),
|
.block(value: "if value in 1...3", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "else"),
|
.block(value: "else", at: .unknown),
|
||||||
.text(value: "false"),
|
.text(value: "false", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -289,3 +286,4 @@ func testIfNode() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import XCTest
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
import PathKit
|
import PathKit
|
||||||
|
|
||||||
|
class IncludeTests: XCTestCase {
|
||||||
func testInclude() {
|
func testInclude() {
|
||||||
describe("Include") {
|
describe("Include") {
|
||||||
let path = Path(#file) + ".." + "fixtures"
|
let path = Path(#file) + ".." + "fixtures"
|
||||||
@@ -11,15 +12,15 @@ func testInclude() {
|
|||||||
|
|
||||||
$0.describe("parsing") {
|
$0.describe("parsing") {
|
||||||
$0.it("throws an error when no template is given") {
|
$0.it("throws an error when no template is given") {
|
||||||
let tokens: [Token] = [ .block(value: "include") ]
|
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("'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")
|
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") {
|
$0.it("can parse a valid include block") {
|
||||||
let tokens: [Token] = [ .block(value: "include \"test.html\"") ]
|
let tokens: [Token] = [ .block(value: "include \"test.html\"", 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()
|
||||||
@@ -31,7 +32,7 @@ func testInclude() {
|
|||||||
|
|
||||||
$0.describe("rendering") {
|
$0.describe("rendering") {
|
||||||
$0.it("throws an error when rendering without a loader") {
|
$0.it("throws an error when rendering without a loader") {
|
||||||
let node = IncludeNode(templateName: Variable("\"test.html\""))
|
let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown))
|
||||||
|
|
||||||
do {
|
do {
|
||||||
_ = try node.render(Context())
|
_ = try node.render(Context())
|
||||||
@@ -41,7 +42,7 @@ func testInclude() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws an error when it cannot find the included template") {
|
$0.it("throws an error when it cannot find the included template") {
|
||||||
let node = IncludeNode(templateName: Variable("\"unknown.html\""))
|
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: environment))
|
||||||
@@ -51,14 +52,16 @@ func testInclude() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("successfully renders a found included template") {
|
$0.it("successfully renders a found included template") {
|
||||||
let node = IncludeNode(templateName: Variable("\"test.html\""))
|
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: 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") {
|
$0.it("successfully passes context") {
|
||||||
let template = Template(templateString: "{% include \"test.html\" child %}")
|
let template = Template(templateString: """
|
||||||
|
{% include "test.html" child %}
|
||||||
|
""")
|
||||||
let context = Context(dictionary: ["child": ["target": "World"]], environment: environment)
|
let context = Context(dictionary: ["child": ["target": "World"]], environment: environment)
|
||||||
let value = try template.render(context)
|
let value = try template.render(context)
|
||||||
try expect(value) == "Hello World!"
|
try expect(value) == "Hello World!"
|
||||||
@@ -66,3 +69,4 @@ func testInclude() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import XCTest
|
||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
import Stencil
|
||||||
import PathKit
|
import PathKit
|
||||||
|
|
||||||
|
class InheritenceTests: XCTestCase {
|
||||||
func testInheritence() {
|
func testInheritence() {
|
||||||
describe("Inheritence") {
|
describe("Inheritence") {
|
||||||
let path = Path(#file) + ".." + "fixtures"
|
let path = Path(#file) + ".." + "fixtures"
|
||||||
@@ -11,17 +12,27 @@ func testInheritence() {
|
|||||||
|
|
||||||
$0.it("can inherit from another template") {
|
$0.it("can inherit from another template") {
|
||||||
let template = try environment.loadTemplate(name: "child.html")
|
let template = try environment.loadTemplate(name: "child.html")
|
||||||
try expect(try template.render()) == "Super_Header Child_Header\nChild_Body"
|
try expect(try template.render()) == """
|
||||||
|
Super_Header Child_Header
|
||||||
|
Child_Body
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can inherit from another template inheriting from another template") {
|
$0.it("can inherit from another template inheriting from another template") {
|
||||||
let template = try environment.loadTemplate(name: "child-child.html")
|
let template = try environment.loadTemplate(name: "child-child.html")
|
||||||
try expect(try template.render()) == "Super_Header Child_Header Child_Child_Header\nChild_Body"
|
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") {
|
$0.it("can inherit from a template that calls a super block") {
|
||||||
let template = try environment.loadTemplate(name: "child-super.html")
|
let template = try environment.loadTemplate(name: "child-super.html")
|
||||||
try expect(try template.render()) == "Header\nChild_Body"
|
try expect(try template.render()) == """
|
||||||
|
Header
|
||||||
|
Child_Body
|
||||||
|
"""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
|
import PathKit
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
class LexerTests: XCTestCase {
|
||||||
func testLexer() {
|
func testLexer() {
|
||||||
describe("Lexer") {
|
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") {
|
$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()
|
||||||
|
|
||||||
try expect(tokens.count) == 1
|
try expect(tokens.count) == 1
|
||||||
try expect(tokens.first) == .text(value: "Hello World")
|
try expect(tokens.first) == .text(value: "Hello World", at: makeSourceMap("Hello World", for: lexer))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize a comment") {
|
$0.it("can tokenize a comment") {
|
||||||
@@ -17,7 +24,7 @@ func testLexer() {
|
|||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
try expect(tokens.count) == 1
|
try expect(tokens.count) == 1
|
||||||
try expect(tokens.first) == .comment(value: "Comment")
|
try expect(tokens.first) == .comment(value: "Comment", at: makeSourceMap("Comment", for: lexer))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize a variable") {
|
$0.it("can tokenize a variable") {
|
||||||
@@ -25,66 +32,106 @@ func testLexer() {
|
|||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
try expect(tokens.count) == 1
|
try expect(tokens.count) == 1
|
||||||
try expect(tokens.first) == .variable(value: "Variable")
|
try expect(tokens.first) == .variable(value: "Variable", at: makeSourceMap("Variable", for: lexer))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize unclosed tag by ignoring it") {
|
$0.it("can tokenize a token without spaces") {
|
||||||
let lexer = Lexer(templateString: "{{ thing")
|
let lexer = Lexer(templateString: "{{Variable}}")
|
||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
try expect(tokens.count) == 1
|
try expect(tokens.count) == 1
|
||||||
try expect(tokens.first) == .text(value: "")
|
try expect(tokens.first) == .variable(value: "Variable", at: makeSourceMap("Variable", for: lexer))
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can tokenize unclosed tag by ignoring it") {
|
||||||
|
let templateString = "{{ thing"
|
||||||
|
let lexer = Lexer(templateString: templateString)
|
||||||
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
|
try expect(tokens.count) == 1
|
||||||
|
try expect(tokens.first) == .text(value: "", at: makeSourceMap("{{ thing", for: lexer))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize a mixture of content") {
|
$0.it("can tokenize a mixture of content") {
|
||||||
let lexer = Lexer(templateString: "My name is {{ name }}.")
|
let templateString = "My name is {{ myname }}."
|
||||||
|
let lexer = Lexer(templateString: templateString)
|
||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
try expect(tokens.count) == 3
|
try expect(tokens.count) == 3
|
||||||
try expect(tokens[0]) == Token.text(value: "My name is ")
|
try expect(tokens[0]) == Token.text(value: "My name is ", at: makeSourceMap("My name is ", for: lexer))
|
||||||
try expect(tokens[1]) == Token.variable(value: "name")
|
try expect(tokens[1]) == Token.variable(value: "myname", at: makeSourceMap("myname", for: lexer))
|
||||||
try expect(tokens[2]) == Token.text(value: ".")
|
try expect(tokens[2]) == Token.text(value: ".", at: makeSourceMap(".", for: lexer))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize two variables without being greedy") {
|
$0.it("can tokenize two variables without being greedy") {
|
||||||
let lexer = Lexer(templateString: "{{ thing }}{{ name }}")
|
let templateString = "{{ thing }}{{ name }}"
|
||||||
|
let lexer = Lexer(templateString: templateString)
|
||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
try expect(tokens.count) == 2
|
try expect(tokens.count) == 2
|
||||||
try expect(tokens[0]) == Token.variable(value: "thing")
|
try expect(tokens[0]) == Token.variable(value: "thing", at: makeSourceMap("thing", for: lexer))
|
||||||
try expect(tokens[1]) == Token.variable(value: "name")
|
try expect(tokens[1]) == Token.variable(value: "name", at: makeSourceMap("name", for: lexer))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize an unclosed block") {
|
$0.it("can tokenize an unclosed block") {
|
||||||
let lexer = Lexer(templateString: "{%}")
|
let lexer = Lexer(templateString: "{%}")
|
||||||
let _ = lexer.tokenize()
|
_ = lexer.tokenize()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can tokenize incorrect syntax without crashing") {
|
||||||
|
let lexer = Lexer(templateString: "func some() {{% if %}")
|
||||||
|
_ = lexer.tokenize()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize an empty variable") {
|
$0.it("can tokenize an empty variable") {
|
||||||
let lexer = Lexer(templateString: "{{}}")
|
let lexer = Lexer(templateString: "{{}}")
|
||||||
let _ = lexer.tokenize()
|
_ = lexer.tokenize()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize with new lines") {
|
$0.it("can tokenize with new lines") {
|
||||||
let lexer = Lexer(templateString:
|
let templateString = """
|
||||||
"My name is {%\n" +
|
My name is {%
|
||||||
" if name\n" +
|
if name
|
||||||
" and\n" +
|
and
|
||||||
" name\n" +
|
name
|
||||||
"%}{{\n" +
|
%}{{
|
||||||
"name\n" +
|
name
|
||||||
"}}{%\n" +
|
}}{%
|
||||||
"endif %}.")
|
endif %}.
|
||||||
|
"""
|
||||||
|
let lexer = Lexer(templateString: templateString)
|
||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
try expect(tokens.count) == 5
|
try expect(tokens.count) == 5
|
||||||
try expect(tokens[0]) == Token.text(value: "My name is ")
|
try expect(tokens[0]) == Token.text(value: "My name is ", at: makeSourceMap("My name is", for: lexer))
|
||||||
try expect(tokens[1]) == Token.block(value: "if name and name")
|
try expect(tokens[1]) == Token.block(value: "if name and name", at: makeSourceMap("{%", for: lexer))
|
||||||
try expect(tokens[2]) == Token.variable(value: "name")
|
try expect(tokens[2]) == Token.variable(value: "name", at: makeSourceMap("name", for: lexer, options: .backwards))
|
||||||
try expect(tokens[3]) == Token.block(value: "endif")
|
try expect(tokens[3]) == Token.block(value: "endif", at: makeSourceMap("endif", for: lexer))
|
||||||
try expect(tokens[4]) == Token.text(value: ".")
|
try expect(tokens[4]) == Token.text(value: ".", at: makeSourceMap(".", for: lexer))
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can tokenize escape sequences") {
|
||||||
|
let templateString = "class Some {{ '{' }}{% if true %}{{ stuff }}{% endif %}"
|
||||||
|
let lexer = Lexer(templateString: templateString)
|
||||||
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
|
try expect(tokens.count) == 5
|
||||||
|
try expect(tokens[0]) == Token.text(value: "class Some ", at: makeSourceMap("class Some ", for: lexer))
|
||||||
|
try expect(tokens[1]) == Token.variable(value: "'{'", at: makeSourceMap("'{'", for: lexer))
|
||||||
|
try expect(tokens[2]) == Token.block(value: "if true", at: makeSourceMap("if true", for: lexer))
|
||||||
|
try expect(tokens[3]) == Token.variable(value: "stuff", at: makeSourceMap("stuff", for: lexer))
|
||||||
|
try expect(tokens[4]) == Token.block(value: "endif", at: makeSourceMap("endif", for: lexer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPerformance() throws {
|
||||||
|
let path = Path(#file) + ".." + "fixtures" + "huge.html"
|
||||||
|
let content: String = try path.read()
|
||||||
|
|
||||||
|
measure {
|
||||||
|
let lexer = Lexer(templateString: content)
|
||||||
|
_ = lexer.tokenize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import XCTest
|
||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
import Stencil
|
||||||
import PathKit
|
import PathKit
|
||||||
|
|
||||||
|
class TemplateLoaderTests: XCTestCase {
|
||||||
func testTemplateLoader() {
|
func testTemplateLoader() {
|
||||||
describe("FileSystemLoader") {
|
describe("FileSystemLoader") {
|
||||||
let path = Path(#file) + ".." + "fixtures"
|
let path = Path(#file) + ".." + "fixtures"
|
||||||
@@ -53,3 +54,4 @@ func testTemplateLoader() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
|
import XCTest
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
|
|
||||||
|
|
||||||
class ErrorNode : NodeType {
|
class ErrorNode : NodeType {
|
||||||
|
let token: Token?
|
||||||
|
init(token: Token? = nil) {
|
||||||
|
self.token = token
|
||||||
|
}
|
||||||
|
|
||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
throw TemplateSyntaxError("Custom Error")
|
throw TemplateSyntaxError("Custom Error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NodeTests: XCTestCase {
|
||||||
func testNode() {
|
func testNode() {
|
||||||
describe("Node") {
|
describe("Node") {
|
||||||
let context = Context(dictionary: [
|
let context = Context(dictionary: [
|
||||||
@@ -58,3 +63,4 @@ func testNode() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
|
import XCTest
|
||||||
import Foundation
|
import Foundation
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
|
|
||||||
|
|
||||||
|
class NowNodeTests: XCTestCase {
|
||||||
func testNowNode() {
|
func testNowNode() {
|
||||||
#if !os(Linux)
|
#if !os(Linux)
|
||||||
describe("NowNode") {
|
describe("NowNode") {
|
||||||
$0.describe("parsing") {
|
$0.describe("parsing") {
|
||||||
$0.it("parses default format without any now arguments") {
|
$0.it("parses default format without any now arguments") {
|
||||||
let tokens: [Token] = [ .block(value: "now") ]
|
let tokens: [Token] = [ .block(value: "now", 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()
|
||||||
@@ -18,7 +20,7 @@ func testNowNode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("parses now with a format") {
|
$0.it("parses now with a format") {
|
||||||
let tokens: [Token] = [ .block(value: "now \"HH:mm\"") ]
|
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
|
||||||
@@ -41,3 +43,4 @@ func testNowNode() {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
|
import XCTest
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
|
|
||||||
|
class TokenParserTests: XCTestCase {
|
||||||
func testTokenParser() {
|
func testTokenParser() {
|
||||||
describe("TokenParser") {
|
describe("TokenParser") {
|
||||||
$0.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")
|
.text(value: "Hello World", at: .unknown)
|
||||||
], environment: Environment())
|
], environment: Environment())
|
||||||
|
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
@@ -18,7 +19,7 @@ func testTokenParser() {
|
|||||||
|
|
||||||
$0.it("can parse a variable token") {
|
$0.it("can parse a variable token") {
|
||||||
let parser = TokenParser(tokens: [
|
let parser = TokenParser(tokens: [
|
||||||
.variable(value: "'name'")
|
.variable(value: "'name'", at: .unknown)
|
||||||
], environment: Environment())
|
], environment: Environment())
|
||||||
|
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
@@ -30,7 +31,7 @@ func testTokenParser() {
|
|||||||
|
|
||||||
$0.it("can parse a comment token") {
|
$0.it("can parse a comment token") {
|
||||||
let parser = TokenParser(tokens: [
|
let parser = TokenParser(tokens: [
|
||||||
.comment(value: "Secret stuff!")
|
.comment(value: "Secret stuff!", at: .unknown)
|
||||||
], environment: Environment())
|
], environment: Environment())
|
||||||
|
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
@@ -44,7 +45,7 @@ func testTokenParser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let parser = TokenParser(tokens: [
|
let parser = TokenParser(tokens: [
|
||||||
.block(value: "known"),
|
.block(value: "known", at: .unknown),
|
||||||
], environment: Environment(extensions: [simpleExtension]))
|
], environment: Environment(extensions: [simpleExtension]))
|
||||||
|
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
@@ -52,11 +53,11 @@ func testTokenParser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("errors when parsing an unknown tag") {
|
$0.it("errors when parsing an unknown tag") {
|
||||||
let parser = TokenParser(tokens: [
|
let tokens: [Token] = [.block(value: "unknown", at: .unknown)]
|
||||||
.block(value: "unknown"),
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
], environment: Environment())
|
|
||||||
|
|
||||||
try expect(try parser.parse()).toThrow(TemplateSyntaxError("Unknown template tag 'unknown'"))
|
try expect(try parser.parse()).toThrow(TemplateSyntaxError(reason: "Unknown template tag 'unknown'", token: tokens.first))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
|
import XCTest
|
||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
import Stencil
|
||||||
|
|
||||||
|
fileprivate struct CustomNode : NodeType {
|
||||||
fileprivate class CustomNode : NodeType {
|
let token: Token?
|
||||||
func render(_ context:Context) throws -> String {
|
func render(_ context:Context) throws -> String {
|
||||||
return "Hello World"
|
return "Hello World"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fileprivate struct Article {
|
fileprivate struct Article {
|
||||||
let title: String
|
let title: String
|
||||||
let author: String
|
let author: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StencilTests: XCTestCase {
|
||||||
func testStencil() {
|
func testStencil() {
|
||||||
describe("Stencil") {
|
describe("Stencil") {
|
||||||
let exampleExtension = Extension()
|
let exampleExtension = Extension()
|
||||||
@@ -24,18 +24,20 @@ func testStencil() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exampleExtension.registerTag("customtag") { parser, token in
|
exampleExtension.registerTag("customtag") { parser, token in
|
||||||
return CustomNode()
|
return CustomNode(token: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
let environment = Environment(extensions: [exampleExtension])
|
let environment = Environment(extensions: [exampleExtension])
|
||||||
|
|
||||||
$0.it("can render the README example") {
|
$0.it("can render the README example") {
|
||||||
|
|
||||||
let templateString = "There are {{ articles.count }} articles.\n" +
|
let templateString = """
|
||||||
"\n" +
|
There are {{ articles.count }} articles.
|
||||||
"{% for article in articles %}" +
|
|
||||||
" - {{ article.title }} by {{ article.author }}.\n" +
|
{% for article in articles %}\
|
||||||
"{% endfor %}\n"
|
- {{ article.title }} by {{ article.author }}.
|
||||||
|
{% endfor %}
|
||||||
|
"""
|
||||||
|
|
||||||
let context = [
|
let context = [
|
||||||
"articles": [
|
"articles": [
|
||||||
@@ -47,13 +49,13 @@ func testStencil() {
|
|||||||
let template = Template(templateString: templateString)
|
let template = Template(templateString: templateString)
|
||||||
let result = try template.render(context)
|
let result = try template.render(context)
|
||||||
|
|
||||||
let fixture = "There are 2 articles.\n" +
|
try expect(result) == """
|
||||||
"\n" +
|
There are 2 articles.
|
||||||
" - Migrating from OCUnit to XCTest by Kyle Fuller.\n" +
|
|
||||||
" - Memory Management with ARC by Kyle Fuller.\n" +
|
|
||||||
"\n"
|
|
||||||
|
|
||||||
try expect(result) == fixture
|
- Migrating from OCUnit to XCTest by Kyle Fuller.
|
||||||
|
- Memory Management with ARC by Kyle Fuller.
|
||||||
|
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can render a custom template tag") {
|
$0.it("can render a custom template tag") {
|
||||||
@@ -67,3 +69,4 @@ func testStencil() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import XCTest
|
||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
@testable import Stencil
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateTests: XCTestCase {
|
||||||
func testTemplate() {
|
func testTemplate() {
|
||||||
describe("Template") {
|
describe("Template") {
|
||||||
$0.it("can render a template from a string") {
|
$0.it("can render a template from a string") {
|
||||||
@@ -15,5 +16,7 @@ func testTemplate() {
|
|||||||
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,12 @@
|
|||||||
|
import XCTest
|
||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
@testable import Stencil
|
||||||
|
|
||||||
|
|
||||||
|
class TokenTests: XCTestCase {
|
||||||
func testToken() {
|
func testToken() {
|
||||||
describe("Token") {
|
describe("Token") {
|
||||||
$0.it("can split the contents into components") {
|
$0.it("can split the contents into components") {
|
||||||
let token = Token.text(value: "hello world")
|
let token = Token.text(value: "hello world", at: .unknown)
|
||||||
let components = token.components()
|
let components = token.components()
|
||||||
|
|
||||||
try expect(components.count) == 2
|
try expect(components.count) == 2
|
||||||
@@ -14,7 +15,7 @@ func testToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can split the contents into components with single quoted strings") {
|
$0.it("can split the contents into components with single quoted strings") {
|
||||||
let token = Token.text(value: "hello 'kyle fuller'")
|
let token = Token.text(value: "hello 'kyle fuller'", at: .unknown)
|
||||||
let components = token.components()
|
let components = token.components()
|
||||||
|
|
||||||
try expect(components.count) == 2
|
try expect(components.count) == 2
|
||||||
@@ -23,7 +24,7 @@ func testToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can split the contents into components with double quoted strings") {
|
$0.it("can split the contents into components with double quoted strings") {
|
||||||
let token = Token.text(value: "hello \"kyle fuller\"")
|
let token = Token.text(value: "hello \"kyle fuller\"", at: .unknown)
|
||||||
let components = token.components()
|
let components = token.components()
|
||||||
|
|
||||||
try expect(components.count) == 2
|
try expect(components.count) == 2
|
||||||
@@ -32,3 +33,4 @@ func testToken() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import XCTest
|
||||||
import Foundation
|
import Foundation
|
||||||
import Spectre
|
import Spectre
|
||||||
@testable import Stencil
|
@testable import Stencil
|
||||||
@@ -29,6 +30,7 @@ fileprivate class Blog: WebSite {
|
|||||||
let featuring: Article? = Article(author: Person(name: "Jhon"))
|
let featuring: Article? = Article(author: Person(name: "Jhon"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class VariableTests: XCTestCase {
|
||||||
func testVariable() {
|
func testVariable() {
|
||||||
describe("Variable") {
|
describe("Variable") {
|
||||||
let context = Context(dictionary: [
|
let context = Context(dictionary: [
|
||||||
@@ -86,13 +88,62 @@ func testVariable() {
|
|||||||
try expect(result) == "Kyle"
|
try expect(result) == "Kyle"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve an item from a dictionary") {
|
$0.context("given string") {
|
||||||
|
$0.it("can resolve an item via it's index") {
|
||||||
|
let variable = Variable("name.0")
|
||||||
|
let result = try variable.resolve(context) as? Character
|
||||||
|
try expect(result) == "K"
|
||||||
|
|
||||||
|
let variable1 = Variable("name.1")
|
||||||
|
let result1 = try variable1.resolve(context) as? Character
|
||||||
|
try expect(result1) == "y"
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can resolve an item via unknown index") {
|
||||||
|
let variable = Variable("name.5")
|
||||||
|
let result = try variable.resolve(context) as? Character
|
||||||
|
try expect(result).to.beNil()
|
||||||
|
|
||||||
|
let variable1 = Variable("name.-5")
|
||||||
|
let result1 = try variable1.resolve(context) as? Character
|
||||||
|
try expect(result1).to.beNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can resolve the first item") {
|
||||||
|
let variable = Variable("name.first")
|
||||||
|
let result = try variable.resolve(context) as? Character
|
||||||
|
try expect(result) == "K"
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can resolve the last item") {
|
||||||
|
let variable = Variable("name.last")
|
||||||
|
let result = try variable.resolve(context) as? Character
|
||||||
|
try expect(result) == "e"
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can get the characters count") {
|
||||||
|
let variable = Variable("name.count")
|
||||||
|
let result = try variable.resolve(context) as? Int
|
||||||
|
try expect(result) == 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$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(context) as? String
|
||||||
try expect(result) == "kylef"
|
try expect(result) == "kylef"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve an item from an array via it's index") {
|
$0.it("can get the count") {
|
||||||
|
let variable = Variable("profiles.count")
|
||||||
|
let result = try variable.resolve(context) as? Int
|
||||||
|
try expect(result) == 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.context("given array") {
|
||||||
|
$0.it("can resolve an item via it's index") {
|
||||||
let variable = Variable("contacts.0")
|
let variable = Variable("contacts.0")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(context) as? String
|
||||||
try expect(result) == "Katie"
|
try expect(result) == "Katie"
|
||||||
@@ -102,7 +153,7 @@ func testVariable() {
|
|||||||
try expect(result1) == "Carlton"
|
try expect(result1) == "Carlton"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve an item from an array via unknown index") {
|
$0.it("can resolve an item 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(context) as? String
|
||||||
try expect(result).to.beNil()
|
try expect(result).to.beNil()
|
||||||
@@ -112,30 +163,31 @@ func testVariable() {
|
|||||||
try expect(result1).to.beNil()
|
try expect(result1).to.beNil()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve the first item from an array") {
|
$0.it("can resolve the first item") {
|
||||||
let variable = Variable("contacts.first")
|
let variable = Variable("contacts.first")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(context) as? String
|
||||||
try expect(result) == "Katie"
|
try expect(result) == "Katie"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve the last item from an array") {
|
$0.it("can resolve the last item") {
|
||||||
let variable = Variable("contacts.last")
|
let variable = Variable("contacts.last")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(context) as? String
|
||||||
try expect(result) == "Carlton"
|
try expect(result) == "Carlton"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$0.it("can get the count") {
|
||||||
|
let variable = Variable("contacts.count")
|
||||||
|
let result = try variable.resolve(context) as? Int
|
||||||
|
try expect(result) == 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$0.it("can resolve a property with reflection") {
|
$0.it("can resolve a property with reflection") {
|
||||||
let variable = Variable("article.author.name")
|
let variable = Variable("article.author.name")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(context) as? String
|
||||||
try expect(result) == "Kyle"
|
try expect(result) == "Kyle"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can get the count of a dictionary") {
|
|
||||||
let variable = Variable("profiles.count")
|
|
||||||
let result = try variable.resolve(context) as? Int
|
|
||||||
try expect(result) == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
#if os(OSX)
|
#if os(OSX)
|
||||||
$0.it("can resolve a value via KVO") {
|
$0.it("can resolve a value via KVO") {
|
||||||
let variable = Variable("object.title")
|
let variable = Variable("object.title")
|
||||||
@@ -148,6 +200,12 @@ func testVariable() {
|
|||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(context) as? String
|
||||||
try expect(result) == "Foo"
|
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
|
#endif
|
||||||
|
|
||||||
$0.it("can resolve a value via reflection") {
|
$0.it("can resolve a value via reflection") {
|
||||||
@@ -189,7 +247,7 @@ func testVariable() {
|
|||||||
try expect(result) == 2
|
try expect(result) == 2
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("Subrscripting") {
|
$0.describe("Subscripting") {
|
||||||
$0.it("can resolve a property subscript via reflection") {
|
$0.it("can resolve a property subscript via reflection") {
|
||||||
try context.push(dictionary: ["property": "name"]) {
|
try context.push(dictionary: ["property": "name"]) {
|
||||||
let variable = Variable("article.author[property]")
|
let variable = Variable("article.author[property]")
|
||||||
@@ -292,7 +350,9 @@ func testVariable() {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
func makeVariable(_ token: String) throws -> RangeVariable? {
|
func makeVariable(_ token: String) throws -> RangeVariable? {
|
||||||
return try RangeVariable(token, environment: context.environment)
|
let token = Token.variable(value: token, at: .unknown)
|
||||||
|
let parser = TokenParser(tokens: [token], environment: context.environment)
|
||||||
|
return try RangeVariable(token.contents, parser: parser, containedIn: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve closed range as array") {
|
$0.it("can resolve closed range as array") {
|
||||||
@@ -329,4 +389,24 @@ func testVariable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import XCTest
|
|
||||||
|
|
||||||
|
|
||||||
public func stencilTests() {
|
|
||||||
testContext()
|
|
||||||
testFilter()
|
|
||||||
testLexer()
|
|
||||||
testToken()
|
|
||||||
testTokenParser()
|
|
||||||
testTemplateLoader()
|
|
||||||
testTemplate()
|
|
||||||
testVariable()
|
|
||||||
testNode()
|
|
||||||
testForNode()
|
|
||||||
testExpressions()
|
|
||||||
testIfNode()
|
|
||||||
testNowNode()
|
|
||||||
testInclude()
|
|
||||||
testInheritence()
|
|
||||||
testFilterTag()
|
|
||||||
testEnvironment()
|
|
||||||
testStencil()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class StencilTests: XCTestCase {
|
|
||||||
func testRunStencilTests() {
|
|
||||||
stencilTests()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
135
Tests/StencilTests/XCTestManifests.swift
Normal file
135
Tests/StencilTests/XCTestManifests.swift
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import XCTest
|
||||||
|
|
||||||
|
extension ContextTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testContext", testContext),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EnvironmentTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testEnvironment", testEnvironment),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ExpressionsTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testExpressions", testExpressions),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FilterTagTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testFilterTag", testFilterTag),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FilterTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testFilter", testFilter),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ForNodeTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testForNode", testForNode),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension IfNodeTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testIfNode", testIfNode),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension IncludeTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testInclude", testInclude),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InheritenceTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testInheritence", testInheritence),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LexerTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testLexer", testLexer),
|
||||||
|
("testPerformance", testPerformance),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NodeTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testNode", testNode),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NowNodeTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testNowNode", testNowNode),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StencilTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testStencil", testStencil),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TemplateLoaderTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testTemplateLoader", testTemplateLoader),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TemplateTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testTemplate", testTemplate),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TokenParserTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testTokenParser", testTokenParser),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TokenTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testToken", testToken),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VariableTests {
|
||||||
|
static let __allTests = [
|
||||||
|
("testVariable", testVariable),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !os(macOS)
|
||||||
|
public func __allTests() -> [XCTestCaseEntry] {
|
||||||
|
return [
|
||||||
|
testCase(ContextTests.__allTests),
|
||||||
|
testCase(EnvironmentTests.__allTests),
|
||||||
|
testCase(ExpressionsTests.__allTests),
|
||||||
|
testCase(FilterTagTests.__allTests),
|
||||||
|
testCase(FilterTests.__allTests),
|
||||||
|
testCase(ForNodeTests.__allTests),
|
||||||
|
testCase(IfNodeTests.__allTests),
|
||||||
|
testCase(IncludeTests.__allTests),
|
||||||
|
testCase(InheritenceTests.__allTests),
|
||||||
|
testCase(LexerTests.__allTests),
|
||||||
|
testCase(NodeTests.__allTests),
|
||||||
|
testCase(NowNodeTests.__allTests),
|
||||||
|
testCase(StencilTests.__allTests),
|
||||||
|
testCase(TemplateLoaderTests.__allTests),
|
||||||
|
testCase(TemplateTests.__allTests),
|
||||||
|
testCase(TokenParserTests.__allTests),
|
||||||
|
testCase(TokenTests.__allTests),
|
||||||
|
testCase(VariableTests.__allTests),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
#endif
|
||||||
1131
Tests/StencilTests/fixtures/huge.html
Normal file
1131
Tests/StencilTests/fixtures/huge.html
Normal file
File diff suppressed because it is too large
Load Diff
2
Tests/StencilTests/fixtures/invalid-base.html
Normal file
2
Tests/StencilTests/fixtures/invalid-base.html
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
{% block header %}Header{% endblock %}
|
||||||
|
{% block body %}Body {{ target|unknown }} {% endblock %}
|
||||||
3
Tests/StencilTests/fixtures/invalid-child-super.html
Normal file
3
Tests/StencilTests/fixtures/invalid-child-super.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{% extends "invalid-base.html" %}
|
||||||
|
{% block body %}Child {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
1
Tests/StencilTests/fixtures/invalid-include.html
Normal file
1
Tests/StencilTests/fixtures/invalid-include.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Hello {{ target|unknown }}!
|
||||||
2
docs/_templates/sidebar_intro.html
vendored
2
docs/_templates/sidebar_intro.html
vendored
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<iframe
|
<iframe
|
||||||
src="https://ghbtns.com/github-btn.html?user=kylef&repo=Stencil&type=watch&count=true&size=large"
|
src="https://ghbtns.com/github-btn.html?user=stencilproject&repo=Stencil&type=watch&count=true&size=large"
|
||||||
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px">
|
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px">
|
||||||
</iframe>
|
</iframe>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -149,6 +149,19 @@ Will be treated as:
|
|||||||
|
|
||||||
one or (two and three)
|
one or (two and three)
|
||||||
|
|
||||||
|
You can use parentheses to change operator precedence. For example:
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
{% if (one or two) and three %}
|
||||||
|
|
||||||
|
Will be treated as:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
(one or two) and three
|
||||||
|
|
||||||
|
|
||||||
``==`` operator
|
``==`` operator
|
||||||
"""""""""""""""
|
"""""""""""""""
|
||||||
|
|
||||||
|
|||||||
@@ -58,9 +58,9 @@ author = 'Kyle Fuller'
|
|||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = '0.7.0'
|
version = '0.13.1'
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = '0.7.0'
|
release = '0.13.1'
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
|
|||||||
@@ -48,6 +48,17 @@ Registering custom filters with arguments:
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Registering custom boolean filters:
|
||||||
|
|
||||||
|
.. code-block:: swift
|
||||||
|
|
||||||
|
ext.registerFilter("ordinary", negativeFilterName: "odd") { (value: Any?) in
|
||||||
|
if let value = value as? Int {
|
||||||
|
return myInt % 2 == 0
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
Custom Tags
|
Custom Tags
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ dependencies inside ``Package.swift``.
|
|||||||
let package = Package(
|
let package = Package(
|
||||||
name: "MyApplication",
|
name: "MyApplication",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.Package(url: "https://github.com/kylef/Stencil.git", majorVersion: 0, minor: 8),
|
.Package(url: "https://github.com/stencilproject/Stencil.git", majorVersion: 0, minor: 13),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ If you're using CocoaPods, you can add Stencil to your ``Podfile`` and then run
|
|||||||
|
|
||||||
.. code-block:: ruby
|
.. code-block:: ruby
|
||||||
|
|
||||||
pod 'Stencil', '~> 0.8.0'
|
pod 'Stencil', '~> 0.13.1'
|
||||||
|
|
||||||
Carthage
|
Carthage
|
||||||
--------
|
--------
|
||||||
@@ -37,7 +37,7 @@ Carthage
|
|||||||
|
|
||||||
.. code-block:: text
|
.. code-block:: text
|
||||||
|
|
||||||
github "kylef/Stencil" ~> 0.8.0
|
github "stencilproject/Stencil" ~> 0.13.1
|
||||||
|
|
||||||
2) Checkout your dependencies, generate the Stencil Xcode project, and then use Carthage to build Stencil:
|
2) Checkout your dependencies, generate the Stencil Xcode project, and then use Carthage to build Stencil:
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ following lookup:
|
|||||||
|
|
||||||
- Context lookup
|
- Context lookup
|
||||||
- Dictionary lookup
|
- Dictionary lookup
|
||||||
- Array lookup (first, last, count, index)
|
- Array and string lookup (first, last, count, by index)
|
||||||
- Key value coding lookup
|
- Key value coding lookup
|
||||||
- Type introspection
|
- Type introspection (via ``Mirror``)
|
||||||
|
|
||||||
For example, if `people` was an array:
|
For example, if `people` was an array:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user