Merge branch 'master' into dynamic-filter
This commit is contained in:
91
CHANGELOG.md
91
CHANGELOG.md
@@ -6,19 +6,22 @@
|
|||||||
|
|
||||||
- Added an optional second parameter to the `include` tag for passing a sub context to the included file.
|
- Added an optional second parameter to the `include` tag for passing a sub context to the included file.
|
||||||
[Yonas Kolb](https://github.com/yonaskolb)
|
[Yonas Kolb](https://github.com/yonaskolb)
|
||||||
[#394](https://github.com/stencilproject/Stencil/pull/214)
|
[#214](https://github.com/stencilproject/Stencil/pull/214)
|
||||||
|
- Variables now support the subscript notation. For example, if you have a variable `key = "name"`, and an
|
||||||
- Adds support for using spaces in filter expression.
|
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)
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
[#178](https://github.com/stencilproject/Stencil/pull/178)
|
[#178](https://github.com/stencilproject/Stencil/pull/178)
|
||||||
|
- Added support for dynamic filter using `filter` filter. With that you can define a variable with a name of filter
|
||||||
- Added support for dynamic filter using `filter` filter.
|
, i.e. `myfilter = "uppercase"` and then use it to invoke this filter with `{{ string|filter:myfilter }}`.
|
||||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
[#203](https://github.com/stencilproject/Stencil/pull/203)
|
[#203](https://github.com/stencilproject/Stencil/pull/203)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
- Fixed using quote as a filter parameter
|
- Fixed using quote as a filter parameter.
|
||||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
[#210](https://github.com/stencilproject/Stencil/pull/210)
|
[#210](https://github.com/stencilproject/Stencil/pull/210)
|
||||||
|
|
||||||
@@ -27,28 +30,64 @@
|
|||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
- Added support for resolving superclass properties for not-NSObject subclasses
|
- 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
|
- The `{% for %}` tag can now iterate over tuples, structures and classes via
|
||||||
their stored properties.
|
their stored properties.
|
||||||
- Added `split` filter
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
- Allow default string filters to be applied to arrays
|
[#172](https://github.com/stencilproject/Stencil/pull/173)
|
||||||
- Similar filters are suggested when unknown filter is used
|
- Added `split` filter.
|
||||||
- Added `indent` filter
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
- Allow using new lines inside tags
|
[#187](https://github.com/stencilproject/Stencil/pull/187)
|
||||||
- Added support for iterating arrays of tuples
|
- Allow default string filters to be applied to arrays.
|
||||||
- Added support for ranges in if-in expression
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
- Added property `forloop.length` to get number of items in the loop
|
[#190](https://github.com/stencilproject/Stencil/pull/190)
|
||||||
- Now you can construct ranges for loops using `a...b` syntax, i.e. `for i in 1...array.count`
|
- 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
|
### Bug Fixes
|
||||||
|
|
||||||
- Fixed rendering `{{ block.super }}` with several levels of inheritance
|
- Fixed rendering `{{ block.super }}` with several levels of inheritance.
|
||||||
- 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.
|
[#154](https://github.com/stencilproject/Stencil/pull/154)
|
||||||
- Integer literals now resolve into Int values, not Float
|
- Fixed checking dictionary values for nil in `default` filter.
|
||||||
- Fixed accessing properties of optional properties via reflection
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
- No longer render optional values in arrays as `Optional(..)`
|
[#162](https://github.com/stencilproject/Stencil/pull/162)
|
||||||
- Fixed subscription tuples by value index, i.e. `{{ tuple.0 }}`
|
- 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
|
||||||
@@ -249,10 +288,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.
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ Resources to help you integrate Stencil into a Swift project:
|
|||||||
|
|
||||||
[Sourcery](https://github.com/krzysztofzablocki/Sourcery),
|
[Sourcery](https://github.com/krzysztofzablocki/Sourcery),
|
||||||
[SwiftGen](https://github.com/SwiftGen/SwiftGen),
|
[SwiftGen](https://github.com/SwiftGen/SwiftGen),
|
||||||
[Kitura](https://github.com/IBM-Swift/Kitura)
|
[Kitura](https://github.com/IBM-Swift/Kitura),
|
||||||
|
[Weaver](https://github.com/scribd/Weaver)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
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)'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,8 +50,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
|
||||||
@@ -75,7 +77,7 @@ public struct Variable : Equatable, Resolvable {
|
|||||||
return bool
|
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 {
|
||||||
|
|||||||
@@ -188,6 +188,98 @@ func testVariable() {
|
|||||||
let result = try variable.resolve(context) as? Int
|
let result = try variable.resolve(context) as? Int
|
||||||
try expect(result) == 2
|
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") {
|
describe("RangeVariable") {
|
||||||
|
|||||||
@@ -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