Merge branch 'master' into errors-logs-improvements
This commit is contained in:
92
CHANGELOG.md
92
CHANGELOG.md
@@ -4,17 +4,89 @@
|
|||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
- Added support for resolving superclass properties for not-NSObject subclasses
|
- Added an optional second parameter to the `include` tag for passing a sub context to the included file.
|
||||||
- The `{% for %}` tag can now iterate over tuples, structures and classes via
|
[Yonas Kolb](https://github.com/yonaskolb)
|
||||||
their stored properties.
|
[#214](https://github.com/stencilproject/Stencil/pull/214)
|
||||||
- Drastic improvements in error reporting
|
- Variables now support the subscript notation. For example, if you have a variable `key = "name"`, and an
|
||||||
|
object `item = ["name": "John"]`, then `{{ item[key] }}` will evaluate to "John".
|
||||||
|
[David Jennes](https://github.com/djbe)
|
||||||
|
[#215](https://github.com/stencilproject/Stencil/pull/215)
|
||||||
|
- Adds support for using spaces in filter expression.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#178](https://github.com/stencilproject/Stencil/pull/178)
|
||||||
|
- Improvements in error reporting.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#167](https://github.com/stencilproject/Stencil/pull/167)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
- Fixed rendering `{{ block.super }}` with several levels of inheritance
|
- Fixed using quote as a filter parameter.
|
||||||
- Fixed checking dictionary values for nil in `default` filter
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
- Fixed comparing string variables with string literals, in Swift 4 string literals became `Substring` and thus couldn't be directly compared to strings.
|
[#210](https://github.com/stencilproject/Stencil/pull/210)
|
||||||
- Integer literals now resolve into Int values, not Float
|
|
||||||
|
|
||||||
|
## 0.11.0 (2018-04-04)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
- Added support for resolving superclass properties for not-NSObject subclasses.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#152](https://github.com/stencilproject/Stencil/pull/152)
|
||||||
|
- The `{% for %}` tag can now iterate over tuples, structures and classes via
|
||||||
|
their stored properties.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#172](https://github.com/stencilproject/Stencil/pull/173)
|
||||||
|
- Added `split` filter.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#187](https://github.com/stencilproject/Stencil/pull/187)
|
||||||
|
- Allow default string filters to be applied to arrays.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#190](https://github.com/stencilproject/Stencil/pull/190)
|
||||||
|
- Similar filters are suggested when unknown filter is used.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#186](https://github.com/stencilproject/Stencil/pull/186)
|
||||||
|
- Added `indent` filter.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#188](https://github.com/stencilproject/Stencil/pull/188)
|
||||||
|
- Allow using new lines inside tags.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#202](https://github.com/stencilproject/Stencil/pull/202)
|
||||||
|
- Added support for iterating arrays of tuples.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#177](https://github.com/stencilproject/Stencil/pull/177)
|
||||||
|
- Added support for ranges in if-in expression.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#193](https://github.com/stencilproject/Stencil/pull/193)
|
||||||
|
- Added property `forloop.length` to get number of items in the loop.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#171](https://github.com/stencilproject/Stencil/pull/171)
|
||||||
|
- Now you can construct ranges for loops using `a...b` syntax, i.e. `for i in 1...array.count`.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#192](https://github.com/stencilproject/Stencil/pull/192)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fixed rendering `{{ block.super }}` with several levels of inheritance.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#154](https://github.com/stencilproject/Stencil/pull/154)
|
||||||
|
- Fixed checking dictionary values for nil in `default` filter.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#162](https://github.com/stencilproject/Stencil/pull/162)
|
||||||
|
- Fixed comparing string variables with string literals, in Swift 4 string literals became `Substring` and thus couldn't be directly compared to strings.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#168](https://github.com/stencilproject/Stencil/pull/168)
|
||||||
|
- Integer literals now resolve into Int values, not Float.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#181](https://github.com/stencilproject/Stencil/pull/181)
|
||||||
|
- Fixed accessing properties of optional properties via reflection.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#204](https://github.com/stencilproject/Stencil/pull/204)
|
||||||
|
- No longer render optional values in arrays as `Optional(..)`.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#205](https://github.com/stencilproject/Stencil/pull/205)
|
||||||
|
- Fixed subscription tuples by value index, i.e. `{{ tuple.0 }}`.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#172](https://github.com/stencilproject/Stencil/pull/172)
|
||||||
|
|
||||||
|
|
||||||
## 0.10.1
|
## 0.10.1
|
||||||
@@ -215,10 +287,10 @@
|
|||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
- Variables (`{{ variable.5 }}`) that reference an array index at an unknown
|
- Variables (`{{ variable.5 }}`) that reference an array index at an unknown
|
||||||
index will now resolve to `nil` instead of causing a crash.
|
index will now resolve to `nil` instead of causing a crash.
|
||||||
[#72](https://github.com/kylef/Stencil/issues/72)
|
[#72](https://github.com/kylef/Stencil/issues/72)
|
||||||
|
|
||||||
- Templates can now extend templates that extend other templates.
|
- Templates can now extend templates that extend other templates.
|
||||||
[#60](https://github.com/kylef/Stencil/issues/60)
|
[#60](https://github.com/kylef/Stencil/issues/60)
|
||||||
|
|
||||||
- If comparisons will now treat 0 and below numbers as negative.
|
- If comparisons will now treat 0 and below numbers as negative.
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
|
// swift-tools-version:3.1
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "Stencil",
|
name: "Stencil",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 8),
|
.Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 9),
|
||||||
|
.Package(url: "https://github.com/kylef/Spectre.git", majorVersion: 0, minor: 8),
|
||||||
// https://github.com/apple/swift-package-manager/pull/597
|
|
||||||
.Package(url: "https://github.com/kylef/Spectre.git", majorVersion: 0, minor: 7),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
10
Package@swift-3.swift
Normal file
10
Package@swift-3.swift
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// 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),
|
||||||
|
]
|
||||||
|
)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Stencil
|
# Stencil
|
||||||
|
|
||||||
[](https://travis-ci.org/kylef/Stencil)
|
[](https://travis-ci.org/stencilproject/Stencil)
|
||||||
|
|
||||||
Stencil is a simple and powerful template language for Swift. It provides a
|
Stencil is a simple and powerful template language for Swift. It provides a
|
||||||
syntax similar to Django and Mustache. If you're familiar with these, you will
|
syntax similar to Django and Mustache. If you're familiar with these, you will
|
||||||
@@ -63,6 +63,13 @@ Resources to help you integrate Stencil into a Swift project:
|
|||||||
- [API Reference](http://stencil.fuller.li/en/latest/api.html)
|
- [API Reference](http://stencil.fuller.li/en/latest/api.html)
|
||||||
- [Custom Template Tags and Filters](http://stencil.fuller.li/en/latest/custom-template-tags-and-filters.html)
|
- [Custom Template Tags and Filters](http://stencil.fuller.li/en/latest/custom-template-tags-and-filters.html)
|
||||||
|
|
||||||
|
## Projects that use Stencil
|
||||||
|
|
||||||
|
[Sourcery](https://github.com/krzysztofzablocki/Sourcery),
|
||||||
|
[SwiftGen](https://github.com/SwiftGen/SwiftGen),
|
||||||
|
[Kitura](https://github.com/IBM-Swift/Kitura),
|
||||||
|
[Weaver](https://github.com/scribd/Weaver)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Stencil is licensed under the BSD license. See [LICENSE](LICENSE) for more
|
Stencil is licensed under the BSD license. See [LICENSE](LICENSE) for more
|
||||||
|
|||||||
@@ -105,6 +105,10 @@ final class InExpression: Expression, InfixOperator, CustomStringConvertible {
|
|||||||
|
|
||||||
if let lhs = lhsValue as? AnyHashable, let rhs = rhsValue as? [AnyHashable] {
|
if let lhs = lhsValue as? AnyHashable, let rhs = rhsValue as? [AnyHashable] {
|
||||||
return rhs.contains(lhs)
|
return rhs.contains(lhs)
|
||||||
|
} else if let lhs = lhsValue as? Int, let rhs = rhsValue as? CountableClosedRange<Int> {
|
||||||
|
return rhs.contains(lhs)
|
||||||
|
} else if let lhs = lhsValue as? Int, let rhs = rhsValue as? CountableRange<Int> {
|
||||||
|
return rhs.contains(lhs)
|
||||||
} else if let lhs = lhsValue as? String, let rhs = rhsValue as? String {
|
} else if let lhs = lhsValue as? String, let rhs = rhsValue as? String {
|
||||||
return rhs.contains(lhs)
|
return rhs.contains(lhs)
|
||||||
} else if lhsValue == nil && rhsValue == nil {
|
} else if lhsValue == nil && rhsValue == nil {
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ class DefaultExtension: Extension {
|
|||||||
registerFilter("uppercase", filter: uppercase)
|
registerFilter("uppercase", filter: uppercase)
|
||||||
registerFilter("lowercase", filter: lowercase)
|
registerFilter("lowercase", filter: lowercase)
|
||||||
registerFilter("join", filter: joinFilter)
|
registerFilter("join", filter: joinFilter)
|
||||||
|
registerFilter("split", filter: splitFilter)
|
||||||
|
registerFilter("indent", filter: indentFilter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,25 @@
|
|||||||
func capitalise(_ value: Any?) -> Any? {
|
func capitalise(_ value: Any?) -> Any? {
|
||||||
return stringify(value).capitalized
|
if let array = value as? [Any?] {
|
||||||
|
return array.map { stringify($0).capitalized }
|
||||||
|
} else {
|
||||||
|
return stringify(value).capitalized
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func uppercase(_ value: Any?) -> Any? {
|
func uppercase(_ value: Any?) -> Any? {
|
||||||
return stringify(value).uppercased()
|
if let array = value as? [Any?] {
|
||||||
|
return array.map { stringify($0).uppercased() }
|
||||||
|
} else {
|
||||||
|
return stringify(value).uppercased()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func lowercase(_ value: Any?) -> Any? {
|
func lowercase(_ value: Any?) -> Any? {
|
||||||
return stringify(value).lowercased()
|
if let array = value as? [Any?] {
|
||||||
|
return array.map { stringify($0).lowercased() }
|
||||||
|
} else {
|
||||||
|
return stringify(value).lowercased()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultFilter(value: Any?, arguments: [Any?]) -> Any? {
|
func defaultFilter(value: Any?, arguments: [Any?]) -> Any? {
|
||||||
@@ -40,3 +52,62 @@ func joinFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
|||||||
|
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func splitFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||||
|
guard arguments.count < 2 else {
|
||||||
|
throw TemplateSyntaxError("'split' filter takes a single argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
let separator = stringify(arguments.first ?? " ")
|
||||||
|
if let value = value as? String {
|
||||||
|
return value.components(separatedBy: separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||||
|
guard arguments.count <= 3 else {
|
||||||
|
throw TemplateSyntaxError("'indent' filter can take at most 3 arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
var indentWidth = 4
|
||||||
|
if arguments.count > 0 {
|
||||||
|
guard let value = arguments[0] as? Int else {
|
||||||
|
throw TemplateSyntaxError("'indent' filter width argument must be an Integer (\(String(describing: arguments[0])))")
|
||||||
|
}
|
||||||
|
indentWidth = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var indentationChar = " "
|
||||||
|
if arguments.count > 1 {
|
||||||
|
guard let value = arguments[1] as? String else {
|
||||||
|
throw TemplateSyntaxError("'indent' filter indentation argument must be a String (\(String(describing: arguments[1]))")
|
||||||
|
}
|
||||||
|
indentationChar = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var indentFirst = false
|
||||||
|
if arguments.count > 2 {
|
||||||
|
guard let value = arguments[2] as? Bool else {
|
||||||
|
throw TemplateSyntaxError("'indent' filter indentFirst argument must be a Bool")
|
||||||
|
}
|
||||||
|
indentFirst = value
|
||||||
|
}
|
||||||
|
|
||||||
|
let indentation = [String](repeating: indentationChar, count: indentWidth).joined(separator: "")
|
||||||
|
return indent(stringify(value), indentation: indentation, indentFirst: indentFirst)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func indent(_ content: String, indentation: String, indentFirst: Bool) -> String {
|
||||||
|
guard !indentation.isEmpty else { return content }
|
||||||
|
|
||||||
|
var lines = content.components(separatedBy: .newlines)
|
||||||
|
let firstLine = (indentFirst ? indentation : "") + lines.removeFirst()
|
||||||
|
let result = lines.reduce([firstLine]) { (result, line) in
|
||||||
|
return result + [(line.isEmpty ? "" : "\(indentation)\(line)")]
|
||||||
|
}
|
||||||
|
return result.joined(separator: "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,25 +11,28 @@ class ForNode : NodeType {
|
|||||||
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()
|
||||||
|
|
||||||
guard components.count >= 3 && components[2] == "in" &&
|
func hasToken(_ token: String, at index: Int) -> Bool {
|
||||||
(components.count == 4 || (components.count >= 6 && components[4] == "where")) else {
|
return components.count > (index + 1) && components[index] == token
|
||||||
throw TemplateSyntaxError("'for' statements should use the following syntax 'for x in y where condition'.")
|
}
|
||||||
|
|
||||||
|
func endsOrHasToken(_ token: String, at index: Int) -> Bool {
|
||||||
|
return components.count == index || hasToken(token, at: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard hasToken("in", at: 2) && endsOrHasToken("where", at: 4) else {
|
||||||
|
throw TemplateSyntaxError("'for' statements should use the syntax: `for <x> in <y> [where <condition>]`.")
|
||||||
}
|
}
|
||||||
|
|
||||||
let loopVariables = components[1].characters
|
let loopVariables = components[1].characters
|
||||||
.split(separator: ",")
|
.split(separator: ",")
|
||||||
.map(String.init)
|
.map(String.init)
|
||||||
.map { $0.trimmingCharacters(in: CharacterSet.whitespaces) }
|
.map { $0.trim(character: " ") }
|
||||||
|
|
||||||
let variable = components[3]
|
let resolvable = try parser.compileResolvable(components[3], containedIn: token)
|
||||||
let filter = try parser.compileFilter(variable, containedIn: token)
|
|
||||||
|
|
||||||
let `where`: Expression?
|
let `where` = hasToken("where", at: 4)
|
||||||
if components.count >= 6 {
|
? try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser, token: token)
|
||||||
`where` = try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser, token: token)
|
: nil
|
||||||
} else {
|
|
||||||
`where` = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let forNodes = try parser.parse(until(["endfor", "empty"]))
|
let forNodes = try parser.parse(until(["endfor", "empty"]))
|
||||||
|
|
||||||
@@ -43,7 +46,7 @@ class ForNode : NodeType {
|
|||||||
_ = parser.nextToken()
|
_ = parser.nextToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
return ForNode(resolvable: filter, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`, token: token)
|
return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`, token: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(resolvable: Resolvable, loopVariables: [String], nodes:[NodeType], emptyNodes:[NodeType], where: Expression? = nil, token: Token? = nil) {
|
init(resolvable: Resolvable, loopVariables: [String], nodes:[NodeType], emptyNodes:[NodeType], where: Expression? = nil, token: Token? = nil) {
|
||||||
@@ -55,25 +58,26 @@ class ForNode : NodeType {
|
|||||||
self.token = token
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) rethrows -> Result {
|
func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result {
|
||||||
if loopVariables.isEmpty {
|
if loopVariables.isEmpty {
|
||||||
return try context.push() {
|
return try context.push() {
|
||||||
return try closure()
|
return try closure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let value = value as? (Any, Any) {
|
let valueMirror = Mirror(reflecting: value)
|
||||||
let first = loopVariables[0]
|
if case .tuple? = valueMirror.displayStyle {
|
||||||
|
if loopVariables.count > Int(valueMirror.children.count) {
|
||||||
if loopVariables.count == 2 {
|
throw TemplateSyntaxError("Tuple '\(value)' has less values than loop variables")
|
||||||
let second = loopVariables[1]
|
|
||||||
|
|
||||||
return try context.push(dictionary: [first: value.0, second: value.1]) {
|
|
||||||
return try closure()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
var variablesContext = [String: Any]()
|
||||||
|
valueMirror.children.prefix(loopVariables.count).enumerated().forEach({ (offset, element) in
|
||||||
|
if loopVariables[offset] != "_" {
|
||||||
|
variablesContext[loopVariables[offset]] = element.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return try context.push(dictionary: [first: value.0]) {
|
return try context.push(dictionary: variablesContext) {
|
||||||
return try closure()
|
return try closure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,14 +137,15 @@ class ForNode : NodeType {
|
|||||||
"last": index == (count - 1),
|
"last": index == (count - 1),
|
||||||
"counter": index + 1,
|
"counter": index + 1,
|
||||||
"counter0": index,
|
"counter0": index,
|
||||||
]
|
"length": count
|
||||||
|
]
|
||||||
|
|
||||||
return try context.push(dictionary: ["forloop": forContext]) {
|
return try context.push(dictionary: ["forloop": forContext]) {
|
||||||
return try push(value: item, context: context) {
|
return try push(value: item, context: context) {
|
||||||
try renderNodes(nodes, context)
|
try renderNodes(nodes, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.joined(separator: "")
|
}.joined(separator: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
return try context.push {
|
return try context.push {
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ final class IfExpressionParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return .variable(try tokenParser.compileFilter(component, containedIn: token))
|
return .variable(try tokenParser.compileResolvable(component, containedIn: token))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,20 +3,22 @@ import PathKit
|
|||||||
|
|
||||||
class IncludeNode : NodeType {
|
class IncludeNode : NodeType {
|
||||||
let templateName: Variable
|
let templateName: Variable
|
||||||
|
let includeContext: String?
|
||||||
let token: Token?
|
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 else {
|
guard bits.count == 2 || bits.count == 3 else {
|
||||||
throw TemplateSyntaxError("'include' tag takes one argument, the template file to be included")
|
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]), token: token)
|
return IncludeNode(templateName: Variable(bits[1]), includeContext: bits.count == 3 ? bits[2] : nil, token: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(templateName: Variable, token: Token) {
|
init(templateName: Variable, includeContext: String? = nil, token: Token) {
|
||||||
self.templateName = templateName
|
self.templateName = templateName
|
||||||
|
self.includeContext = includeContext
|
||||||
self.token = token
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +30,8 @@ class IncludeNode : NodeType {
|
|||||||
let template = try context.environment.loadTemplate(name: templateName)
|
let template = try context.environment.loadTemplate(name: templateName)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
return try context.push {
|
let subContext = includeContext.flatMap { context[$0] as? [String: Any] }
|
||||||
|
return try context.push(dictionary: subContext) {
|
||||||
return try template.render(context)
|
return try template.render(context)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
112
Sources/KeyPath.swift
Normal file
112
Sources/KeyPath.swift
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// A structure used to represent a template variable, and to resolve it in a given context.
|
||||||
|
final class KeyPath {
|
||||||
|
private var components = [String]()
|
||||||
|
private var current = ""
|
||||||
|
private var partialComponents = [String]()
|
||||||
|
private var subscriptLevel = 0
|
||||||
|
|
||||||
|
let variable: String
|
||||||
|
let context: Context
|
||||||
|
|
||||||
|
// Split the keypath string and resolve references if possible
|
||||||
|
init(_ variable: String, in context: Context) {
|
||||||
|
self.variable = variable
|
||||||
|
self.context = context
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse() throws -> [String] {
|
||||||
|
defer {
|
||||||
|
components = []
|
||||||
|
current = ""
|
||||||
|
partialComponents = []
|
||||||
|
subscriptLevel = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for c in variable.characters {
|
||||||
|
switch c {
|
||||||
|
case "." where subscriptLevel == 0:
|
||||||
|
try foundSeparator()
|
||||||
|
case "[":
|
||||||
|
try openBracket()
|
||||||
|
case "]":
|
||||||
|
try closeBracket()
|
||||||
|
default:
|
||||||
|
try addCharacter(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try finish()
|
||||||
|
|
||||||
|
return components
|
||||||
|
}
|
||||||
|
|
||||||
|
private func foundSeparator() throws {
|
||||||
|
if !current.isEmpty {
|
||||||
|
partialComponents.append(current)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !partialComponents.isEmpty else {
|
||||||
|
throw TemplateSyntaxError("Unexpected '.' in variable '\(variable)'")
|
||||||
|
}
|
||||||
|
|
||||||
|
components += partialComponents
|
||||||
|
current = ""
|
||||||
|
partialComponents = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// when opening the first bracket, we must have a partial component
|
||||||
|
private func openBracket() throws {
|
||||||
|
guard !partialComponents.isEmpty || !current.isEmpty else {
|
||||||
|
throw TemplateSyntaxError("Unexpected '[' in variable '\(variable)'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if subscriptLevel > 0 {
|
||||||
|
current.append("[")
|
||||||
|
} else if !current.isEmpty {
|
||||||
|
partialComponents.append(current)
|
||||||
|
current = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptLevel += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// for a closing bracket at root level, try to resolve the reference
|
||||||
|
private func closeBracket() throws {
|
||||||
|
guard subscriptLevel > 0 else {
|
||||||
|
throw TemplateSyntaxError("Unbalanced ']' in variable '\(variable)'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if subscriptLevel > 1 {
|
||||||
|
current.append("]")
|
||||||
|
} else if !current.isEmpty,
|
||||||
|
let value = try Variable(current).resolve(context) {
|
||||||
|
partialComponents.append("\(value)")
|
||||||
|
current = ""
|
||||||
|
} else {
|
||||||
|
throw TemplateSyntaxError("Unable to resolve subscript '\(current)' in variable '\(variable)'")
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptLevel -= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addCharacter(_ c: Character) throws {
|
||||||
|
guard partialComponents.isEmpty || subscriptLevel > 0 else {
|
||||||
|
throw TemplateSyntaxError("Unexpected character '\(c)' in variable '\(variable)'")
|
||||||
|
}
|
||||||
|
|
||||||
|
current.append(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func finish() throws {
|
||||||
|
// check if we have a last piece
|
||||||
|
if !current.isEmpty {
|
||||||
|
partialComponents.append(current)
|
||||||
|
}
|
||||||
|
components += partialComponents
|
||||||
|
|
||||||
|
guard subscriptLevel == 0 else {
|
||||||
|
throw TemplateSyntaxError("Unbalanced subscript brackets in variable '\(variable)'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,12 @@ struct Lexer {
|
|||||||
guard string.characters.count > 4 else { return "" }
|
guard string.characters.count > 4 else { return "" }
|
||||||
let start = string.index(string.startIndex, offsetBy: 2)
|
let start = string.index(string.startIndex, offsetBy: 2)
|
||||||
let end = string.index(string.endIndex, offsetBy: -2)
|
let end = string.index(string.endIndex, offsetBy: -2)
|
||||||
return String(string[start..<end]).trim(character: " ")
|
let trimmed = String(string[start..<end])
|
||||||
|
.components(separatedBy: "\n")
|
||||||
|
.filter({ !$0.isEmpty })
|
||||||
|
.map({ $0.trim(character: " ") })
|
||||||
|
.joined(separator: " ")
|
||||||
|
return trimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
if string.hasPrefix("{{") || string.hasPrefix("{%") || string.hasPrefix("{#") {
|
if string.hasPrefix("{{") || string.hasPrefix("{%") || string.hasPrefix("{#") {
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ public class VariableNode : NodeType {
|
|||||||
func stringify(_ result: Any?) -> String {
|
func stringify(_ result: Any?) -> String {
|
||||||
if let result = result as? String {
|
if let result = result as? String {
|
||||||
return result
|
return result
|
||||||
|
} else if let array = result as? [Any?] {
|
||||||
|
return unwrap(array).description
|
||||||
} else if let result = result as? CustomStringConvertible {
|
} else if let result = result as? CustomStringConvertible {
|
||||||
return result.description
|
return result.description
|
||||||
} else if let result = result as? NSObject {
|
} else if let result = result as? NSObject {
|
||||||
@@ -87,3 +89,16 @@ func stringify(_ result: Any?) -> String {
|
|||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func unwrap(_ array: [Any?]) -> [Any] {
|
||||||
|
return array.map { (item: Any?) -> Any in
|
||||||
|
if let item = item {
|
||||||
|
if let items = item as? [Any?] {
|
||||||
|
return unwrap(items)
|
||||||
|
} else {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else { return item as Any }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class TokenParser {
|
|||||||
case .text(let text, _):
|
case .text(let text, _):
|
||||||
nodes.append(TextNode(text: text))
|
nodes.append(TextNode(text: text))
|
||||||
case .variable:
|
case .variable:
|
||||||
let filter = try compileFilter(token.contents, containedIn: token)
|
let filter = try compileResolvable(token.contents, containedIn: token)
|
||||||
nodes.append(VariableNode(variable: filter, token: token))
|
nodes.append(VariableNode(variable: filter, 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) {
|
||||||
@@ -94,7 +94,26 @@ public class TokenParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw TemplateSyntaxError("Unknown filter '\(name)'")
|
let suggestedFilters = self.suggestedFilters(for: name)
|
||||||
|
if suggestedFilters.isEmpty {
|
||||||
|
throw TemplateSyntaxError("Unknown filter '\(name)'.")
|
||||||
|
} else {
|
||||||
|
throw TemplateSyntaxError("Unknown filter '\(name)'. Found similar filters: \(suggestedFilters.map({ "'\($0)'" }).joined(separator: ", ")).")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func suggestedFilters(for name: String) -> [String] {
|
||||||
|
let allFilters = environment.extensions.flatMap({ $0.filters.keys })
|
||||||
|
|
||||||
|
let filtersWithDistance = allFilters
|
||||||
|
.map({ (filterName: $0, distance: $0.levenshteinDistance(name)) })
|
||||||
|
// do not suggest filters which names are shorter than the distance
|
||||||
|
.filter({ $0.filterName.characters.count > $0.distance })
|
||||||
|
guard let minDistance = filtersWithDistance.min(by: { $0.distance < $1.distance })?.distance else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
// suggest all filters with the same distance
|
||||||
|
return filtersWithDistance.filter({ $0.distance == minDistance }).map({ $0.filterName })
|
||||||
}
|
}
|
||||||
|
|
||||||
public func compileFilter(_ filterToken: String, containedIn containingToken: Token) throws -> Resolvable {
|
public func compileFilter(_ filterToken: String, containedIn containingToken: Token) throws -> Resolvable {
|
||||||
@@ -122,4 +141,57 @@ public class TokenParser {
|
|||||||
return try FilterExpression(token: token, parser: self)
|
return try FilterExpression(token: token, parser: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(*, deprecated, message: "Use compileResolvable(_:containedIn:)")
|
||||||
|
public func compileResolvable(_ token: String) throws -> Resolvable {
|
||||||
|
return try RangeVariable(token, parser: self)
|
||||||
|
?? compileFilter(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable {
|
||||||
|
return try RangeVariable(token, parser: self)
|
||||||
|
?? compileFilter(token, containedIn: containingToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
|
||||||
|
extension String {
|
||||||
|
|
||||||
|
subscript(_ i: Int) -> Character {
|
||||||
|
return self[self.index(self.startIndex, offsetBy: i)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func levenshteinDistance(_ target: String) -> Int {
|
||||||
|
// create two work vectors of integer distances
|
||||||
|
var last, current: [Int]
|
||||||
|
|
||||||
|
// initialize v0 (the previous row of distances)
|
||||||
|
// this row is A[0][i]: edit distance for an empty s
|
||||||
|
// the distance is just the number of characters to delete from t
|
||||||
|
last = [Int](0...target.characters.count)
|
||||||
|
current = [Int](repeating: 0, count: target.characters.count + 1)
|
||||||
|
|
||||||
|
for i in 0..<self.characters.count {
|
||||||
|
// calculate v1 (current row distances) from the previous row v0
|
||||||
|
|
||||||
|
// first element of v1 is A[i+1][0]
|
||||||
|
// edit distance is delete (i+1) chars from s to match empty t
|
||||||
|
current[0] = i + 1
|
||||||
|
|
||||||
|
// use formula to fill in the rest of the row
|
||||||
|
for j in 0..<target.characters.count {
|
||||||
|
current[j+1] = Swift.min(
|
||||||
|
last[j+1] + 1,
|
||||||
|
current[j] + 1,
|
||||||
|
last[j] + (self[i] == target[j] ? 0 : 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy v1 (current row) to v0 (previous row) for next iteration
|
||||||
|
last = current
|
||||||
|
}
|
||||||
|
|
||||||
|
return current[target.characters.count]
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,21 @@ extension String {
|
|||||||
var singleQuoteCount = 0
|
var singleQuoteCount = 0
|
||||||
var doubleQuoteCount = 0
|
var doubleQuoteCount = 0
|
||||||
|
|
||||||
|
let specialCharacters = ",|:"
|
||||||
|
func appendWord(_ word: String) {
|
||||||
|
if components.count > 0 {
|
||||||
|
if let precedingChar = components.last?.characters.last, specialCharacters.characters.contains(precedingChar) {
|
||||||
|
components[components.count-1] += word
|
||||||
|
} else if specialCharacters.contains(word) {
|
||||||
|
components[components.count-1] += word
|
||||||
|
} else {
|
||||||
|
components.append(word)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
components.append(word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for character in self.characters {
|
for character in self.characters {
|
||||||
if character == "'" { singleQuoteCount += 1 }
|
if character == "'" { singleQuoteCount += 1 }
|
||||||
else if character == "\"" { doubleQuoteCount += 1 }
|
else if character == "\"" { doubleQuoteCount += 1 }
|
||||||
@@ -18,8 +33,8 @@ extension String {
|
|||||||
|
|
||||||
if separate != separator {
|
if separate != separator {
|
||||||
word.append(separate)
|
word.append(separate)
|
||||||
} else if singleQuoteCount % 2 == 0 && doubleQuoteCount % 2 == 0 && !word.isEmpty {
|
} else if (singleQuoteCount % 2 == 0 || doubleQuoteCount % 2 == 0) && !word.isEmpty {
|
||||||
components.append(word)
|
appendWord(word)
|
||||||
word = ""
|
word = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +48,7 @@ extension String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !word.isEmpty {
|
if !word.isEmpty {
|
||||||
components.append(word)
|
appendWord(word)
|
||||||
}
|
}
|
||||||
|
|
||||||
return components
|
return components
|
||||||
|
|||||||
@@ -48,8 +48,10 @@ public struct Variable : Equatable, Resolvable {
|
|||||||
self.variable = variable
|
self.variable = variable
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func lookup() -> [String] {
|
// Split the lookup string and resolve references if possible
|
||||||
return variable.characters.split(separator: ".").map(String.init)
|
fileprivate func lookup(_ context: Context) throws -> [String] {
|
||||||
|
var keyPath = KeyPath(variable, in: context)
|
||||||
|
return try keyPath.parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the variable in the given context
|
/// Resolve the variable in the given context
|
||||||
@@ -68,8 +70,12 @@ public struct Variable : Equatable, Resolvable {
|
|||||||
if let number = Number(variable) {
|
if let number = Number(variable) {
|
||||||
return number
|
return number
|
||||||
}
|
}
|
||||||
|
// Boolean literal
|
||||||
|
if let bool = Bool(variable) {
|
||||||
|
return bool
|
||||||
|
}
|
||||||
|
|
||||||
for bit in lookup() {
|
for bit in try lookup(context) {
|
||||||
current = normalize(current)
|
current = normalize(current)
|
||||||
|
|
||||||
if let context = current as? Context {
|
if let context = current as? Context {
|
||||||
@@ -124,6 +130,42 @@ public func ==(lhs: Variable, rhs: Variable) -> Bool {
|
|||||||
return lhs.variable == rhs.variable
|
return lhs.variable == rhs.variable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A structure used to represet range of two integer values expressed as `from...to`.
|
||||||
|
/// Values should be numbers (they will be converted to integers).
|
||||||
|
/// Rendering this variable produces array from range `from...to`.
|
||||||
|
/// If `from` is more than `to` array will contain values of reversed range.
|
||||||
|
public struct RangeVariable: Resolvable {
|
||||||
|
public let from: Resolvable
|
||||||
|
public let to: Resolvable
|
||||||
|
|
||||||
|
public init?(_ token: String, parser: TokenParser) throws {
|
||||||
|
let components = token.components(separatedBy: "...")
|
||||||
|
guard components.count == 2 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.from = try parser.compileFilter(components[0])
|
||||||
|
self.to = try parser.compileFilter(components[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
public func resolve(_ context: Context) throws -> Any? {
|
||||||
|
let fromResolved = try from.resolve(context)
|
||||||
|
let toResolved = try to.resolve(context)
|
||||||
|
|
||||||
|
guard let from = fromResolved.flatMap(toNumber(value:)).flatMap(Int.init) else {
|
||||||
|
throw TemplateSyntaxError("'from' value is not an Integer (\(fromResolved ?? "nil"))")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let to = toResolved.flatMap(toNumber(value:)).flatMap(Int.init) else {
|
||||||
|
throw TemplateSyntaxError("'to' value is not an Integer (\(toResolved ?? "nil") )")
|
||||||
|
}
|
||||||
|
|
||||||
|
let range = min(from, to)...max(from, to)
|
||||||
|
return from > to ? Array(range.reversed()) : Array(range)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func normalize(_ current: Any?) -> Any? {
|
func normalize(_ current: Any?) -> Any? {
|
||||||
if let current = current as? Normalizable {
|
if let current = current as? Normalizable {
|
||||||
@@ -167,25 +209,47 @@ extension Dictionary : Normalizable {
|
|||||||
|
|
||||||
func parseFilterComponents(token: String) -> (String, [Variable]) {
|
func parseFilterComponents(token: String) -> (String, [Variable]) {
|
||||||
var components = token.smartSplit(separator: ":")
|
var components = token.smartSplit(separator: ":")
|
||||||
let name = components.removeFirst()
|
let name = components.removeFirst().trim(character: " ")
|
||||||
let variables = components
|
let variables = components
|
||||||
.joined(separator: ":")
|
.joined(separator: ":")
|
||||||
.smartSplit(separator: ",")
|
.smartSplit(separator: ",")
|
||||||
.map { Variable($0) }
|
.map { Variable($0.trim(character: " ")) }
|
||||||
return (name, variables)
|
return (name, variables)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Mirror {
|
extension Mirror {
|
||||||
func getValue(for key: String) -> Any? {
|
func getValue(for key: String) -> Any? {
|
||||||
let result = descendant(key)
|
let result = descendant(key) ?? Int(key).flatMap({ descendant($0) })
|
||||||
if result == nil {
|
if result == nil {
|
||||||
// go through inheritance chain to reach superclass properties
|
// go through inheritance chain to reach superclass properties
|
||||||
return superclassMirror?.getValue(for: key)
|
return superclassMirror?.getValue(for: key)
|
||||||
} else if let result = result, String(describing: result) == "nil" {
|
} else if let result = result {
|
||||||
// mirror returns non-nil value even for nil-containing properties
|
guard String(describing: result) != "nil" else {
|
||||||
// so we have to check if its value is actually nil or not
|
// mirror returns non-nil value even for nil-containing properties
|
||||||
return nil
|
// so we have to check if its value is actually nil or not
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if let result = (result as? AnyOptional)?.wrapped {
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protocol AnyOptional {
|
||||||
|
var wrapped: Any? { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Optional: AnyOptional {
|
||||||
|
var wrapped: Any? {
|
||||||
|
switch self {
|
||||||
|
case let .some(value): return value
|
||||||
|
case .none: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Stencil",
|
"name": "Stencil",
|
||||||
"version": "0.10.1",
|
"version": "0.11.0",
|
||||||
"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": {
|
||||||
@@ -12,8 +12,8 @@
|
|||||||
},
|
},
|
||||||
"social_media_url": "https://twitter.com/kylefuller",
|
"social_media_url": "https://twitter.com/kylefuller",
|
||||||
"source": {
|
"source": {
|
||||||
"git": "https://github.com/kylef/Stencil.git",
|
"git": "https://github.com/stencilproject/Stencil.git",
|
||||||
"tag": "0.10.1"
|
"tag": "0.11.0"
|
||||||
},
|
},
|
||||||
"source_files": [
|
"source_files": [
|
||||||
"Sources/*.swift"
|
"Sources/*.swift"
|
||||||
@@ -25,6 +25,8 @@
|
|||||||
},
|
},
|
||||||
"requires_arc": true,
|
"requires_arc": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"PathKit": [ "~> 0.8.0" ]
|
"PathKit": [
|
||||||
|
"~> 0.8.0"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,18 +48,20 @@ func testEnvironment() {
|
|||||||
return TemplateSyntaxError(reason: description, token: token, stackTrace: [])
|
return TemplateSyntaxError(reason: description, token: token, stackTrace: [])
|
||||||
}
|
}
|
||||||
|
|
||||||
func expectError(reason: String, token: String) throws {
|
func expectError(reason: String, token: String,
|
||||||
|
file: String = #file, line: Int = #line, function: String = #function) throws {
|
||||||
let expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
let expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
||||||
|
|
||||||
let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"])).toThrow() as TemplateSyntaxError
|
let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"]),
|
||||||
try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError)
|
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
||||||
|
try expect(environment.errorReporter.renderError(error), file: file, line: line, function: function) == environment.errorReporter.renderError(expectedError)
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.context("given syntax error") {
|
$0.context("given syntax error") {
|
||||||
|
|
||||||
$0.it("reports syntax error on invalid for tag syntax") {
|
$0.it("reports syntax error on invalid for tag syntax") {
|
||||||
template = "Hello {% for name in %}{{ name }}, {% endfor %}!"
|
template = "Hello {% for name in %}{{ name }}, {% endfor %}!"
|
||||||
try expectError(reason: "'for' statements should use the following syntax 'for x in y where condition'.", token: "for name in")
|
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") {
|
$0.it("reports syntax error on missing endfor") {
|
||||||
@@ -78,37 +80,37 @@ func testEnvironment() {
|
|||||||
|
|
||||||
$0.it("reports syntax error in for tag") {
|
$0.it("reports syntax error in for tag") {
|
||||||
template = "{% for name in names|unknown %}{{ name }}{% endfor %}"
|
template = "{% for name in names|unknown %}{{ name }}{% endfor %}"
|
||||||
try expectError(reason: "Unknown filter 'unknown'", token: "names|unknown")
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "names|unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("reports syntax error in for-where tag") {
|
$0.it("reports syntax error in for-where tag") {
|
||||||
template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}"
|
template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}"
|
||||||
try expectError(reason: "Unknown filter 'unknown'", token: "name|unknown")
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("reports syntax error in if tag") {
|
$0.it("reports syntax error in if tag") {
|
||||||
template = "{% if name|unknown %}{{ name }}{% endif %}"
|
template = "{% if name|unknown %}{{ name }}{% endif %}"
|
||||||
try expectError(reason: "Unknown filter 'unknown'", token: "name|unknown")
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("reports syntax error in elif tag") {
|
$0.it("reports syntax error in elif tag") {
|
||||||
template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}"
|
template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}"
|
||||||
try expectError(reason: "Unknown filter 'unknown'", token: "name|unknown")
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("reports syntax error in ifnot tag") {
|
$0.it("reports syntax error in ifnot tag") {
|
||||||
template = "{% ifnot name|unknown %}{{ name }}{% endif %}"
|
template = "{% ifnot name|unknown %}{{ name }}{% endif %}"
|
||||||
try expectError(reason: "Unknown filter 'unknown'", token: "name|unknown")
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("reports syntax error in filter tag") {
|
$0.it("reports syntax error in filter tag") {
|
||||||
template = "{% filter unknown %}Text{% endfilter %}"
|
template = "{% filter unknown %}Text{% endfilter %}"
|
||||||
try expectError(reason: "Unknown filter 'unknown'", token: "filter unknown")
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "filter unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("reports syntax error in variable tag") {
|
$0.it("reports syntax error in variable tag") {
|
||||||
template = "{{ name|unknown }}"
|
template = "{{ name|unknown }}"
|
||||||
try expectError(reason: "Unknown filter 'unknown'", token: "name|unknown")
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -200,20 +202,21 @@ func testEnvironment() {
|
|||||||
includedTemplate = nil
|
includedTemplate = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func expectError(reason: String, token: String, includedToken: String) throws {
|
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)
|
var expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
||||||
expectedError.stackTrace = [expectedSyntaxError(token: includedToken, template: includedTemplate, description: reason).token!]
|
expectedError.stackTrace = [expectedSyntaxError(token: includedToken, template: includedTemplate, description: reason).token!]
|
||||||
|
|
||||||
let error = try expect(environment.render(template: template, context: ["target": "World"]))
|
let error = try expect(environment.render(template: template, context: ["target": "World"]),
|
||||||
.toThrow() as TemplateSyntaxError
|
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
||||||
try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError)
|
try expect(environment.errorReporter.renderError(error), file: file, line: line, function: function) == environment.errorReporter.renderError(expectedError)
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("reports syntax error in included template") {
|
$0.it("reports syntax error in included template") {
|
||||||
template = Template(templateString: "{% include \"invalid-include.html\" %}", environment: environment)
|
template = Template(templateString: "{% include \"invalid-include.html\" %}", environment: environment)
|
||||||
includedTemplate = try environment.loadTemplate(name: "invalid-include.html")
|
includedTemplate = try environment.loadTemplate(name: "invalid-include.html")
|
||||||
|
|
||||||
try expectError(reason: "Unknown filter 'unknown'",
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||||
token: "include \"invalid-include.html\"",
|
token: "include \"invalid-include.html\"",
|
||||||
includedToken: "target|unknown")
|
includedToken: "target|unknown")
|
||||||
}
|
}
|
||||||
@@ -248,21 +251,22 @@ func testEnvironment() {
|
|||||||
baseTemplate = nil
|
baseTemplate = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func expectError(reason: String, childToken: String, baseToken: String?) throws {
|
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)
|
var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason)
|
||||||
if let baseToken = baseToken {
|
if let baseToken = baseToken {
|
||||||
expectedError.stackTrace = [expectedSyntaxError(token: baseToken, template: baseTemplate, description: reason).token!]
|
expectedError.stackTrace = [expectedSyntaxError(token: baseToken, template: baseTemplate, description: reason).token!]
|
||||||
}
|
}
|
||||||
let error = try expect(environment.render(template: childTemplate, context: ["target": "World"]))
|
let error = try expect(environment.render(template: childTemplate, context: ["target": "World"]),
|
||||||
.toThrow() as TemplateSyntaxError
|
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
||||||
try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError)
|
try expect(environment.errorReporter.renderError(error), file: file, line: line, function: function) == environment.errorReporter.renderError(expectedError)
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("reports syntax error in base template") {
|
$0.it("reports syntax error in base template") {
|
||||||
childTemplate = try environment.loadTemplate(name: "invalid-child-super.html")
|
childTemplate = try environment.loadTemplate(name: "invalid-child-super.html")
|
||||||
baseTemplate = try environment.loadTemplate(name: "invalid-base.html")
|
baseTemplate = try environment.loadTemplate(name: "invalid-base.html")
|
||||||
|
|
||||||
try expectError(reason: "Unknown filter 'unknown'",
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||||
childToken: "extends \"invalid-base.html\"",
|
childToken: "extends \"invalid-base.html\"",
|
||||||
baseToken: "target|unknown")
|
baseToken: "target|unknown")
|
||||||
}
|
}
|
||||||
@@ -286,7 +290,7 @@ func testEnvironment() {
|
|||||||
childTemplate = Template(templateString: "{% extends \"base.html\" %}\n" +
|
childTemplate = Template(templateString: "{% extends \"base.html\" %}\n" +
|
||||||
"{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil)
|
"{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil)
|
||||||
|
|
||||||
try expectError(reason: "Unknown filter 'unknown'",
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||||
childToken: "target|unknown",
|
childToken: "target|unknown",
|
||||||
baseToken: nil)
|
baseToken: nil)
|
||||||
}
|
}
|
||||||
@@ -311,7 +315,7 @@ func testEnvironment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension Expectation {
|
extension Expectation {
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func toThrow<T: Error>() throws -> T {
|
func toThrow<T: Error>() throws -> T {
|
||||||
var thrownError: Error? = nil
|
var thrownError: Error? = nil
|
||||||
|
|||||||
@@ -287,12 +287,16 @@ func testExpressions() {
|
|||||||
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()
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["a", "b", "c"]]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["a", "b", "c"]]))).to.beTrue()
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "abc"]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "abc"]))).to.beTrue()
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1...3]))).to.beTrue()
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1..<3]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("evaluates to false when rhs does not contain lhs") {
|
$0.it("evaluates to false when rhs does not contain lhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [2, 3, 4]]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [2, 3, 4]]))).to.beFalse()
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["b", "c", "d"]]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["b", "c", "d"]]))).to.beFalse()
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "bcd"]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "bcd"]))).to.beFalse()
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 1...3]))).to.beFalse()
|
||||||
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 3, "rhs": 1..<3]))).to.beFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,9 +78,9 @@ func testFilter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("allows whitespace in expression") {
|
$0.it("allows whitespace in expression") {
|
||||||
let template = Template(templateString: "{{ name | uppercase }}")
|
let template = Template(templateString: "{{ value | join : \", \" }}")
|
||||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
let result = try template.render(Context(dictionary: ["value": ["One", "Two"]]))
|
||||||
try expect(result) == "KYLE"
|
try expect(result) == "One, Two"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws when you pass arguments to simple filter") {
|
$0.it("throws when you pass arguments to simple filter") {
|
||||||
@@ -89,32 +89,45 @@ func testFilter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe("string filters") {
|
||||||
|
$0.context("given string") {
|
||||||
|
$0.it("transforms a string to be capitalized") {
|
||||||
|
let template = Template(templateString: "{{ name|capitalize }}")
|
||||||
|
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
||||||
|
try expect(result) == "Kyle"
|
||||||
|
}
|
||||||
|
|
||||||
describe("capitalize filter") {
|
$0.it("transforms a string to be uppercase") {
|
||||||
let template = Template(templateString: "{{ name|capitalize }}")
|
let template = Template(templateString: "{{ name|uppercase }}")
|
||||||
|
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
||||||
|
try expect(result) == "KYLE"
|
||||||
|
}
|
||||||
|
|
||||||
$0.it("capitalizes a string") {
|
$0.it("transforms a string to be lowercase") {
|
||||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
let template = Template(templateString: "{{ name|lowercase }}")
|
||||||
try expect(result) == "Kyle"
|
let result = try template.render(Context(dictionary: ["name": "Kyle"]))
|
||||||
|
try expect(result) == "kyle"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
$0.context("given array of strings") {
|
||||||
|
$0.it("transforms a string to be capitalized") {
|
||||||
|
let template = Template(templateString: "{{ names|capitalize }}")
|
||||||
|
let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]]))
|
||||||
|
try expect(result) == "[\"Kyle\", \"Kyle\"]"
|
||||||
|
}
|
||||||
|
|
||||||
describe("uppercase filter") {
|
$0.it("transforms a string to be uppercase") {
|
||||||
let template = Template(templateString: "{{ name|uppercase }}")
|
let template = Template(templateString: "{{ names|uppercase }}")
|
||||||
|
let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]]))
|
||||||
|
try expect(result) == "[\"KYLE\", \"KYLE\"]"
|
||||||
|
}
|
||||||
|
|
||||||
$0.it("transforms a string to be uppercase") {
|
$0.it("transforms a string to be lowercase") {
|
||||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
let template = Template(templateString: "{{ names|lowercase }}")
|
||||||
try expect(result) == "KYLE"
|
let result = try template.render(Context(dictionary: ["names": ["Kyle", "Kyle"]]))
|
||||||
}
|
try expect(result) == "[\"kyle\", \"kyle\"]"
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("lowercase filter") {
|
|
||||||
let template = Template(templateString: "{{ name|lowercase }}")
|
|
||||||
|
|
||||||
$0.it("transforms a string to be lowercase") {
|
|
||||||
let result = try template.render(Context(dictionary: ["name": "Kyle"]))
|
|
||||||
try expect(result) == "kyle"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,4 +196,96 @@ func testFilter() {
|
|||||||
try expect(result) == "OneTwo"
|
try expect(result) == "OneTwo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe("split filter") {
|
||||||
|
let template = Template(templateString: "{{ value|split:\", \" }}")
|
||||||
|
|
||||||
|
$0.it("split a string into array") {
|
||||||
|
let result = try template.render(Context(dictionary: ["value": "One, Two"]))
|
||||||
|
try expect(result) == "[\"One\", \"Two\"]"
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can split without arguments") {
|
||||||
|
let template = Template(templateString: "{{ value|split }}")
|
||||||
|
let result = try template.render(Context(dictionary: ["value": "One, Two"]))
|
||||||
|
try expect(result) == "[\"One,\", \"Two\"]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
describe("filter suggestion") {
|
||||||
|
var template: Template!
|
||||||
|
var filterExtension: Extension!
|
||||||
|
|
||||||
|
func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError {
|
||||||
|
let range = template.templateString.range(of: token)!
|
||||||
|
let rangeLine = template.templateString.rangeLine(range)
|
||||||
|
let sourceMap = SourceMap(filename: template.name, line: rangeLine)
|
||||||
|
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
|
||||||
|
|
||||||
|
try expect(environment.errorReporter.renderError(error), file: file, line: line, function: function) == environment.errorReporter.renderError(expectedError)
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("made for unknown filter") {
|
||||||
|
template = Template(templateString: "{{ value|unknownFilter }}")
|
||||||
|
|
||||||
|
filterExtension = Extension()
|
||||||
|
filterExtension.registerFilter("knownFilter") { value, _ in value }
|
||||||
|
|
||||||
|
try expectError(reason: "Unknown filter 'unknownFilter'. Found similar filters: 'knownFilter'.", token: "value|unknownFilter")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("made for multiple similar filters") {
|
||||||
|
template = Template(templateString: "{{ value|lowerFirst }}")
|
||||||
|
|
||||||
|
filterExtension = Extension()
|
||||||
|
filterExtension.registerFilter("lowerFirstWord") { value, _ in value }
|
||||||
|
filterExtension.registerFilter("lowerFirstLetter") { value, _ in value }
|
||||||
|
|
||||||
|
try expectError(reason: "Unknown filter 'lowerFirst'. Found similar filters: 'lowerFirstWord', 'lowercase'.", token: "value|lowerFirst")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("not made when can't find similar filter") {
|
||||||
|
template = Template(templateString: "{{ value|unknownFilter }}")
|
||||||
|
try expectError(reason: "Unknown filter 'unknownFilter'. Found similar filters: 'lowerFirstWord'.", token: "value|unknownFilter")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
describe("indent filter") {
|
||||||
|
$0.it("indents content") {
|
||||||
|
let template = Template(templateString: "{{ value|indent:2 }}")
|
||||||
|
let result = try template.render(Context(dictionary: ["value": "One\nTwo"]))
|
||||||
|
try expect(result) == "One\n Two"
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can indent with arbitrary character") {
|
||||||
|
let template = Template(templateString: "{{ value|indent:2,\"\t\" }}")
|
||||||
|
let result = try template.render(Context(dictionary: ["value": "One\nTwo"]))
|
||||||
|
try expect(result) == "One\n\t\tTwo"
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can indent first line") {
|
||||||
|
let template = Template(templateString: "{{ value|indent:2,\" \",true }}")
|
||||||
|
let result = try template.render(Context(dictionary: ["value": "One\nTwo"]))
|
||||||
|
try expect(result) == " One\n Two"
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("does not indent empty lines") {
|
||||||
|
let template = Template(templateString: "{{ value|indent }}")
|
||||||
|
let result = try template.render(Context(dictionary: ["value": "One\n\n\nTwo\n\n"]))
|
||||||
|
try expect(result) == "One\n\n\n Two\n\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,5 +21,25 @@ func testFilterTag() {
|
|||||||
try expect(try template.render()).toThrow()
|
try expect(try template.render()).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$0.it("can render filters with arguments") {
|
||||||
|
let ext = Extension()
|
||||||
|
ext.registerFilter("split", filter: {
|
||||||
|
return ($0 as! String).components(separatedBy: $1[0] as! String)
|
||||||
|
})
|
||||||
|
let env = Environment(extensions: [ext])
|
||||||
|
let result = try env.renderTemplate(string: "{% filter split:\",\"|join:\";\" %}{{ items|join:\",\" }}{% endfilter %}", context: ["items": [1, 2]])
|
||||||
|
try expect(result) == "1;2"
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can render filters with quote as an argument") {
|
||||||
|
let ext = Extension()
|
||||||
|
ext.registerFilter("replace", filter: {
|
||||||
|
print($1[0] as! String)
|
||||||
|
return ($0 as! String).replacingOccurrences(of: $1[0] as! String, with: $1[1] as! String)
|
||||||
|
})
|
||||||
|
let env = Environment(extensions: [ext])
|
||||||
|
let result = try env.renderTemplate(string: "{% filter replace:'\"',\"\" %}{{ items|join:\",\" }}{% endfilter %}", context: ["items": ["\"1\"", "\"2\""]])
|
||||||
|
try expect(result) == "1,2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ func testForNode() {
|
|||||||
"dict": [
|
"dict": [
|
||||||
"one": "I",
|
"one": "I",
|
||||||
"two": "II",
|
"two": "II",
|
||||||
]
|
],
|
||||||
|
"tuples": [(1, 2, 3), (4, 5, 6)]
|
||||||
])
|
])
|
||||||
|
|
||||||
$0.it("renders the given nodes for each item") {
|
$0.it("renders the given nodes for each item") {
|
||||||
@@ -89,6 +90,12 @@ func testForNode() {
|
|||||||
try expect(try node.render(context)) == "102132"
|
try expect(try node.render(context)) == "102132"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$0.it("renders the given nodes while providing loop length") {
|
||||||
|
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.length")]
|
||||||
|
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||||
|
try expect(try node.render(context)) == "132333"
|
||||||
|
}
|
||||||
|
|
||||||
$0.it("renders the given nodes while filtering items using where expression") {
|
$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 `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown))
|
let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown))
|
||||||
@@ -104,8 +111,8 @@ func testForNode() {
|
|||||||
try expect(try node.render(context)) == "empty"
|
try expect(try node.render(context)) == "empty"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can render a filter") {
|
$0.it("can render a filter with spaces") {
|
||||||
let templateString = "{% for article in ars|default:articles %}" +
|
let templateString = "{% for article in ars | default: a, b , articles %}" +
|
||||||
"- {{ article.title }} by {{ article.author }}.\n" +
|
"- {{ article.title }} by {{ article.author }}.\n" +
|
||||||
"{% endfor %}\n"
|
"{% endfor %}\n"
|
||||||
|
|
||||||
@@ -127,8 +134,55 @@ func testForNode() {
|
|||||||
try expect(result) == fixture
|
try expect(result) == fixture
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$0.context("given array of tuples") {
|
||||||
|
$0.it("can iterate over all tuple values") {
|
||||||
|
let templateString = "{% for first,second,third in tuples %}" +
|
||||||
|
"{{ first }}, {{ second }}, {{ third }}\n" +
|
||||||
|
"{% endfor %}\n"
|
||||||
|
|
||||||
|
let template = Template(templateString: templateString)
|
||||||
|
let result = try template.render(context)
|
||||||
|
|
||||||
|
let fixture = "1, 2, 3\n4, 5, 6\n\n"
|
||||||
|
try expect(result) == fixture
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can iterate with less number of variables") {
|
||||||
|
let templateString = "{% for first,second in tuples %}" +
|
||||||
|
"{{ first }}, {{ second }}\n" +
|
||||||
|
"{% endfor %}\n"
|
||||||
|
|
||||||
|
let template = Template(templateString: templateString)
|
||||||
|
let result = try template.render(context)
|
||||||
|
|
||||||
|
let fixture = "1, 2\n4, 5\n\n"
|
||||||
|
try expect(result) == fixture
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can use _ to skip variables") {
|
||||||
|
let templateString = "{% for first,_,third in tuples %}" +
|
||||||
|
"{{ first }}, {{ third }}\n" +
|
||||||
|
"{% endfor %}\n"
|
||||||
|
|
||||||
|
let template = Template(templateString: templateString)
|
||||||
|
let result = try template.render(context)
|
||||||
|
|
||||||
|
let fixture = "1, 3\n4, 6\n\n"
|
||||||
|
try expect(result) == fixture
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("throws when number of variables is more than number of tuple values") {
|
||||||
|
let templateString = "{% for key,value,smth in dict %}" +
|
||||||
|
"{% endfor %}\n"
|
||||||
|
|
||||||
|
let template = Template(templateString: templateString)
|
||||||
|
try expect(template.render(context)).toThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
$0.it("can iterate over dictionary") {
|
$0.it("can iterate over dictionary") {
|
||||||
let templateString = "{% for key,value in dict %}" +
|
let templateString = "{% for key, value in dict %}" +
|
||||||
"{{ key }}: {{ value }}," +
|
"{{ key }}: {{ value }}," +
|
||||||
"{% endfor %}"
|
"{% endfor %}"
|
||||||
|
|
||||||
@@ -169,12 +223,92 @@ func testForNode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("handles invalid input") {
|
$0.it("handles invalid input") {
|
||||||
let tokens: [Token] = [.block(value: "for i", at: .unknown)]
|
let token = Token.block(value: "for i", at: .unknown)
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: [token], environment: Environment())
|
||||||
let error = TemplateSyntaxError(reason: "'for' statements should use the following syntax 'for x in y where condition'.", token: tokens.first)
|
let error = TemplateSyntaxError(reason: "'for' statements should use the syntax: `for <x> in <y> [where <condition>]`.", token: token)
|
||||||
try expect(try parser.parse()).toThrow(error)
|
try expect(try parser.parse()).toThrow(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$0.it("can iterate over struct properties") {
|
||||||
|
struct MyStruct {
|
||||||
|
let string: String
|
||||||
|
let number: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = Context(dictionary: [
|
||||||
|
"struct": MyStruct(string: "abc", number: 123)
|
||||||
|
])
|
||||||
|
|
||||||
|
let nodes: [NodeType] = [
|
||||||
|
VariableNode(variable: "property"),
|
||||||
|
TextNode(text: "="),
|
||||||
|
VariableNode(variable: "value"),
|
||||||
|
TextNode(text: "\n"),
|
||||||
|
]
|
||||||
|
let node = ForNode(resolvable: Variable("struct"), loopVariables: ["property", "value"], nodes: nodes, emptyNodes: [])
|
||||||
|
let result = try node.render(context)
|
||||||
|
|
||||||
|
try expect(result) == "string=abc\nnumber=123\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can iterate tuple items") {
|
||||||
|
let context = Context(dictionary: [
|
||||||
|
"tuple": (one: 1, two: "dva"),
|
||||||
|
])
|
||||||
|
|
||||||
|
let nodes: [NodeType] = [
|
||||||
|
VariableNode(variable: "label"),
|
||||||
|
TextNode(text: "="),
|
||||||
|
VariableNode(variable: "value"),
|
||||||
|
TextNode(text: "\n"),
|
||||||
|
]
|
||||||
|
|
||||||
|
let node = ForNode(resolvable: Variable("tuple"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: [])
|
||||||
|
let result = try node.render(context)
|
||||||
|
|
||||||
|
try expect(result) == "one=1\ntwo=dva\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can iterate over class properties") {
|
||||||
|
class MyClass {
|
||||||
|
var baseString: String
|
||||||
|
var baseInt: Int
|
||||||
|
init(_ string: String, _ int: Int) {
|
||||||
|
baseString = string
|
||||||
|
baseInt = int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MySubclass: MyClass {
|
||||||
|
var childString: String
|
||||||
|
init(_ childString: String, _ string: String, _ int: Int) {
|
||||||
|
self.childString = childString
|
||||||
|
super.init(string, int)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = Context(dictionary: [
|
||||||
|
"class": MySubclass("child", "base", 1)
|
||||||
|
])
|
||||||
|
|
||||||
|
let nodes: [NodeType] = [
|
||||||
|
VariableNode(variable: "label"),
|
||||||
|
TextNode(text: "="),
|
||||||
|
VariableNode(variable: "value"),
|
||||||
|
TextNode(text: "\n"),
|
||||||
|
]
|
||||||
|
|
||||||
|
let node = ForNode(resolvable: Variable("class"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: [])
|
||||||
|
let result = try node.render(context)
|
||||||
|
|
||||||
|
try expect(result) == "childString=child\nbaseString=base\nbaseInt=1\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can iterate in range of variables") {
|
||||||
|
let template: Template = "{% for i in 1...j %}{{ i }}{% endfor %}"
|
||||||
|
try expect(try template.render(Context(dictionary: ["j": 3]))) == "123"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,5 +266,22 @@ func testIfNode() {
|
|||||||
let result = try renderNodes(nodes, Context(dictionary: ["instance": SomeType()]))
|
let result = try renderNodes(nodes, Context(dictionary: ["instance": SomeType()]))
|
||||||
try expect(result) == ""
|
try expect(result) == ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$0.it("supports closed range variables") {
|
||||||
|
let tokens: [Token] = [
|
||||||
|
.block(value: "if value in 1...3", at: .unknown),
|
||||||
|
.text(value: "true", at: .unknown),
|
||||||
|
.block(value: "else", at: .unknown),
|
||||||
|
.text(value: "false", at: .unknown),
|
||||||
|
.block(value: "endif", at: .unknown)
|
||||||
|
]
|
||||||
|
|
||||||
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
|
let nodes = try parser.parse()
|
||||||
|
|
||||||
|
try expect(renderNodes(nodes, Context(dictionary: ["value": 3]))) == "true"
|
||||||
|
try expect(renderNodes(nodes, Context(dictionary: ["value": 4]))) == "false"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func testInclude() {
|
|||||||
let tokens: [Token] = [ .block(value: "include", at: .unknown) ]
|
let tokens: [Token] = [ .block(value: "include", at: .unknown) ]
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
|
|
||||||
let error = TemplateSyntaxError(reason: "'include' tag takes one argument, the template file to be included", token: tokens.first)
|
let error = TemplateSyntaxError(reason: "'include' tag requires one argument, the template file to be included. A second optional argument can be used to specify the context that will be passed to the included file", token: tokens.first)
|
||||||
try expect(try parser.parse()).toThrow(error)
|
try expect(try parser.parse()).toThrow(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +56,13 @@ func testInclude() {
|
|||||||
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") {
|
||||||
|
let template = Template(templateString: "{% include \"test.html\" child %}")
|
||||||
|
let context = Context(dictionary: ["child": ["target": "World"]], environment: environment)
|
||||||
|
let value = try template.render(context)
|
||||||
|
try expect(value) == "Hello World!"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,5 +67,28 @@ func testLexer() {
|
|||||||
let lexer = Lexer(templateString: "{{}}")
|
let lexer = Lexer(templateString: "{{}}")
|
||||||
let _ = lexer.tokenize()
|
let _ = lexer.tokenize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$0.it("can tokenize with new lines") {
|
||||||
|
let templateString =
|
||||||
|
"My name is {%\n" +
|
||||||
|
" if name\n" +
|
||||||
|
" and\n" +
|
||||||
|
" name\n" +
|
||||||
|
"%}{{\n" +
|
||||||
|
"name\n" +
|
||||||
|
"}}{%\n" +
|
||||||
|
"endif %}."
|
||||||
|
|
||||||
|
let lexer = Lexer(templateString: templateString)
|
||||||
|
|
||||||
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
|
try expect(tokens.count) == 5
|
||||||
|
try expect(tokens[0]) == Token.text(value: "My name is ", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "My name is")!)))
|
||||||
|
try expect(tokens[1]) == Token.block(value: "if name and name", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "{%")!)))
|
||||||
|
try expect(tokens[2]) == Token.variable(value: "name", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "name", options: [.backwards])!)))
|
||||||
|
try expect(tokens[3]) == Token.block(value: "endif", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "endif")!)))
|
||||||
|
try expect(tokens[4]) == Token.text(value: ".", at: SourceMap(line: templateString.rangeLine(templateString.range(of: ".")!)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import Spectre
|
|||||||
|
|
||||||
#if os(OSX)
|
#if os(OSX)
|
||||||
@objc class Superclass: NSObject {
|
@objc class Superclass: NSObject {
|
||||||
let name = "Foo"
|
@objc let name = "Foo"
|
||||||
}
|
}
|
||||||
@objc class Object : Superclass {
|
@objc class Object : Superclass {
|
||||||
let title = "Hello World"
|
@objc let title = "Hello World"
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ fileprivate class WebSite {
|
|||||||
|
|
||||||
fileprivate class Blog: WebSite {
|
fileprivate class Blog: WebSite {
|
||||||
let articles: [Article] = [Article(author: Person(name: "Kyle"))]
|
let articles: [Article] = [Article(author: Person(name: "Kyle"))]
|
||||||
|
let featuring: Article? = Article(author: Person(name: "Jhon"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testVariable() {
|
func testVariable() {
|
||||||
@@ -39,7 +40,8 @@ func testVariable() {
|
|||||||
"counter": [
|
"counter": [
|
||||||
"count": "kylef",
|
"count": "kylef",
|
||||||
],
|
],
|
||||||
"article": Article(author: Person(name: "Kyle"))
|
"article": Article(author: Person(name: "Kyle")),
|
||||||
|
"tuple": (one: 1, two: 2)
|
||||||
])
|
])
|
||||||
|
|
||||||
#if os(OSX)
|
#if os(OSX)
|
||||||
@@ -71,6 +73,13 @@ func testVariable() {
|
|||||||
try expect(result) == 3.14
|
try expect(result) == 3.14
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$0.it("can resolve boolean literal") {
|
||||||
|
try expect(Variable("true").resolve(context) as? Bool) == true
|
||||||
|
try expect(Variable("false").resolve(context) as? Bool) == false
|
||||||
|
try expect(Variable("0").resolve(context) as? Int) == 0
|
||||||
|
try expect(Variable("1").resolve(context) as? Int) == 1
|
||||||
|
}
|
||||||
|
|
||||||
$0.it("can resolve a string variable") {
|
$0.it("can resolve a string variable") {
|
||||||
let variable = Variable("name")
|
let variable = Variable("name")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(context) as? String
|
||||||
@@ -140,7 +149,7 @@ func testVariable() {
|
|||||||
try expect(result) == "Foo"
|
try expect(result) == "Foo"
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
$0.it("can resolve a value via reflection") {
|
$0.it("can resolve a value via reflection") {
|
||||||
let variable = Variable("blog.articles.0.author.name")
|
let variable = Variable("blog.articles.0.author.name")
|
||||||
let result = try variable.resolve(context) as? String
|
let result = try variable.resolve(context) as? String
|
||||||
@@ -153,5 +162,171 @@ func testVariable() {
|
|||||||
try expect(result) == "blog.com"
|
try expect(result) == "blog.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$0.it("can resolve optional variable property using reflection") {
|
||||||
|
let variable = Variable("blog.featuring.author.name")
|
||||||
|
let result = try variable.resolve(context) as? String
|
||||||
|
try expect(result) == "Jhon"
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("does not render Optional") {
|
||||||
|
var array: [Any?] = [1, nil]
|
||||||
|
array.append(array)
|
||||||
|
let context = Context(dictionary: ["values": array])
|
||||||
|
|
||||||
|
try expect(VariableNode(variable: "values").render(context)) == "[1, nil, [1, nil]]"
|
||||||
|
try expect(VariableNode(variable: "values.1").render(context)) == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can subscript tuple by index") {
|
||||||
|
let variable = Variable("tuple.0")
|
||||||
|
let result = try variable.resolve(context) as? Int
|
||||||
|
try expect(result) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can subscript tuple by label") {
|
||||||
|
let variable = Variable("tuple.two")
|
||||||
|
let result = try variable.resolve(context) as? Int
|
||||||
|
try expect(result) == 2
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.describe("Subrscripting") {
|
||||||
|
$0.it("can resolve a property subscript via reflection") {
|
||||||
|
try context.push(dictionary: ["property": "name"]) {
|
||||||
|
let variable = Variable("article.author[property]")
|
||||||
|
let result = try variable.resolve(context) as? String
|
||||||
|
try expect(result) == "Kyle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can subscript an array with a valid index") {
|
||||||
|
try context.push(dictionary: ["property": 0]) {
|
||||||
|
let variable = Variable("contacts[property]")
|
||||||
|
let result = try variable.resolve(context) as? String
|
||||||
|
try expect(result) == "Katie"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can subscript an array with an unknown index") {
|
||||||
|
try context.push(dictionary: ["property": 5]) {
|
||||||
|
let variable = Variable("contacts[property]")
|
||||||
|
let result = try variable.resolve(context) as? String
|
||||||
|
try expect(result).to.beNil()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(OSX)
|
||||||
|
$0.it("can resolve a subscript via KVO") {
|
||||||
|
try context.push(dictionary: ["property": "name"]) {
|
||||||
|
let variable = Variable("object[property]")
|
||||||
|
let result = try variable.resolve(context) as? String
|
||||||
|
try expect(result) == "Foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
$0.it("can resolve an optional subscript via reflection") {
|
||||||
|
try context.push(dictionary: ["property": "featuring"]) {
|
||||||
|
let variable = Variable("blog[property].author.name")
|
||||||
|
let result = try variable.resolve(context) as? String
|
||||||
|
try expect(result) == "Jhon"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can resolve multiple subscripts") {
|
||||||
|
try context.push(dictionary: [
|
||||||
|
"prop1": "articles",
|
||||||
|
"prop2": 0,
|
||||||
|
"prop3": "name"
|
||||||
|
]) {
|
||||||
|
let variable = Variable("blog[prop1][prop2].author[prop3]")
|
||||||
|
let result = try variable.resolve(context) as? String
|
||||||
|
try expect(result) == "Kyle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can resolve nested subscripts") {
|
||||||
|
try context.push(dictionary: [
|
||||||
|
"prop1": "prop2",
|
||||||
|
"ref": ["prop2": "name"]
|
||||||
|
]) {
|
||||||
|
let variable = Variable("article.author[ref[prop1]]")
|
||||||
|
let result = try variable.resolve(context) as? String
|
||||||
|
try expect(result) == "Kyle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("throws for invalid keypath syntax") {
|
||||||
|
try context.push(dictionary: ["prop": "name"]) {
|
||||||
|
let samples = [
|
||||||
|
".",
|
||||||
|
"..",
|
||||||
|
".test",
|
||||||
|
"test..test",
|
||||||
|
"[prop]",
|
||||||
|
"article.author[prop",
|
||||||
|
"article.author[[prop]",
|
||||||
|
"article.author[prop]]",
|
||||||
|
"article.author[]",
|
||||||
|
"article.author[[]]",
|
||||||
|
"article.author[prop][]",
|
||||||
|
"article.author[prop]comments",
|
||||||
|
"article.author[.]"
|
||||||
|
]
|
||||||
|
|
||||||
|
for lookup in samples {
|
||||||
|
let variable = Variable(lookup)
|
||||||
|
try expect(variable.resolve(context)).toThrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("RangeVariable") {
|
||||||
|
|
||||||
|
let context: Context = {
|
||||||
|
let ext = Extension()
|
||||||
|
ext.registerFilter("incr", filter: { (arg: Any?) in toNumber(value: arg!)! + 1 })
|
||||||
|
let environment = Environment(extensions: [ext])
|
||||||
|
return Context(dictionary: [:], environment: environment)
|
||||||
|
}()
|
||||||
|
|
||||||
|
func makeVariable(_ token: String) throws -> RangeVariable? {
|
||||||
|
return try RangeVariable(token, parser: TokenParser(tokens: [], environment: context.environment))
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can resolve closed range as array") {
|
||||||
|
let result = try makeVariable("1...3")?.resolve(context) as? [Int]
|
||||||
|
try expect(result) == [1, 2, 3]
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can resolve decreasing closed range as reversed array") {
|
||||||
|
let result = try makeVariable("3...1")?.resolve(context) as? [Int]
|
||||||
|
try expect(result) == [3, 2, 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can use filter on range variables") {
|
||||||
|
let result = try makeVariable("1|incr...3|incr")?.resolve(context) as? [Int]
|
||||||
|
try expect(result) == [2, 3, 4]
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("throws when left value is not int") {
|
||||||
|
let template: Template = "{% for i in k...j %}{{ i }}{% endfor %}"
|
||||||
|
try expect(try template.render(Context(dictionary: ["j": 3, "k": "1"]))).toThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("throws when right value is not int") {
|
||||||
|
let variable = try makeVariable("k...j")
|
||||||
|
try expect(try variable?.resolve(Context(dictionary: ["j": "3", "k": 1]))).toThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("throws is left range value is missing") {
|
||||||
|
try expect(makeVariable("...1")).toThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("throws is right range value is missing") {
|
||||||
|
try expect(makeVariable("1...")).toThrow()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,18 @@ The ``for`` tag can iterate over dictionaries.
|
|||||||
<li>{{ key }}: {{ value }}</li>
|
<li>{{ key }}: {{ value }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
It can also iterate over ranges, tuple elements, structs' and classes' stored properties (using ``Mirror``).
|
||||||
|
|
||||||
|
You can iterate over range literals created using ``N...M`` syntax, both in ascending and descending order:
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{% for i in 1...array.count %}
|
||||||
|
<li>{{ i }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
The ``for`` tag can contain optional ``where`` expression to filter out
|
The ``for`` tag can contain optional ``where`` expression to filter out
|
||||||
elements on which this expression evaluates to false.
|
elements on which this expression evaluates to false.
|
||||||
@@ -59,6 +71,7 @@ The for block sets a few variables available within the loop:
|
|||||||
- ``last`` - True if this is the last time through the loop
|
- ``last`` - True if this is the last time through the loop
|
||||||
- ``counter`` - The current iteration of the loop (1 indexed)
|
- ``counter`` - The current iteration of the loop (1 indexed)
|
||||||
- ``counter0`` - The current iteration of the loop (0 indexed)
|
- ``counter0`` - The current iteration of the loop (0 indexed)
|
||||||
|
- ``length`` - The total length of the loop
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
@@ -124,7 +137,7 @@ or to negate a variable.
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
You may use ``and``, ``or`` and ``not`` multiple times together. ``not`` has
|
You may use ``and``, ``or`` and ``not`` multiple times together. ``not`` has
|
||||||
higest prescidence followed by ``and``. For example:
|
higest precedence followed by ``and``. For example:
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. code-block:: html+django
|
||||||
|
|
||||||
@@ -247,6 +260,12 @@ You can include another template using the `include` tag.
|
|||||||
|
|
||||||
{% include "comment.html" %}
|
{% include "comment.html" %}
|
||||||
|
|
||||||
|
By default the included file gets passed the current context. You can pass a sub context by using an optional 2nd parameter as a lookup in the current context.
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
{% include "comment.html" comment %}
|
||||||
|
|
||||||
The `include` tag requires you to provide a loader which will be used to lookup
|
The `include` tag requires you to provide a loader which will be used to lookup
|
||||||
the template.
|
the template.
|
||||||
|
|
||||||
@@ -281,7 +300,7 @@ Built-in Filters
|
|||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
The capitalize filter allows you to capitalize a string.
|
The capitalize filter allows you to capitalize a string.
|
||||||
For example, `stencil` to `Stencil`.
|
For example, `stencil` to `Stencil`. Can be applied to array of strings to change each string.
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. code-block:: html+django
|
||||||
|
|
||||||
@@ -291,7 +310,7 @@ For example, `stencil` to `Stencil`.
|
|||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
The uppercase filter allows you to transform a string to uppercase.
|
The uppercase filter allows you to transform a string to uppercase.
|
||||||
For example, `Stencil` to `STENCIL`.
|
For example, `Stencil` to `STENCIL`. Can be applied to array of strings to change each string.
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. code-block:: html+django
|
||||||
|
|
||||||
@@ -301,7 +320,7 @@ For example, `Stencil` to `STENCIL`.
|
|||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
The uppercase filter allows you to transform a string to lowercase.
|
The uppercase filter allows you to transform a string to lowercase.
|
||||||
For example, `Stencil` to `stencil`.
|
For example, `Stencil` to `stencil`. Can be applied to array of strings to change each string.
|
||||||
|
|
||||||
.. code-block:: html+django
|
.. code-block:: html+django
|
||||||
|
|
||||||
@@ -326,4 +345,31 @@ Join an array of items.
|
|||||||
|
|
||||||
{{ value|join:", " }}
|
{{ value|join:", " }}
|
||||||
|
|
||||||
.. note:: The value MUST be an array.
|
.. note:: The value MUST be an array. Default argument value is empty string.
|
||||||
|
|
||||||
|
``split``
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
Split string into substrings by separator.
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
{{ value|split:", " }}
|
||||||
|
|
||||||
|
.. note:: The value MUST be a String. Default argument value is a single-space string.
|
||||||
|
|
||||||
|
``indent``
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
Indents lines of rendered value or block.
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
{{ value|indent:2," ",true }}
|
||||||
|
|
||||||
|
Filter accepts several arguments:
|
||||||
|
|
||||||
|
* indentation width: number of indentation characters to indent lines with. Default is ``4``.
|
||||||
|
* indentation character: character to be used for indentation. Default is a space.
|
||||||
|
* indent first line: whether first line of output should be indented or not. Default is ``false``.
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,24 @@ For example, if `people` was an array:
|
|||||||
There are {{ people.count }} people. {{ people.first }} is the first
|
There are {{ people.count }} people. {{ people.first }} is the first
|
||||||
person, followed by {{ people.1 }}.
|
person, followed by {{ people.1 }}.
|
||||||
|
|
||||||
|
You can also use the subscript operator for indirect evaluation. The expression
|
||||||
|
between brackets will be evaluated first, before the actual lookup will happen.
|
||||||
|
|
||||||
|
For example, if you have the following context:
|
||||||
|
|
||||||
|
.. code-block:: swift
|
||||||
|
|
||||||
|
[
|
||||||
|
"item": [
|
||||||
|
"name": "John"
|
||||||
|
],
|
||||||
|
"key": "name"
|
||||||
|
]
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
The result of {{ item[key] }} will be the same as {{ item.name }}. It will first evaluate the result of {{ key }}, and only then evaluate the lookup expression.
|
||||||
|
|
||||||
Filters
|
Filters
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user