Merge branch 'master' into dynamic-filter

This commit is contained in:
Ilya Puchka
2018-08-04 20:04:48 +01:00
6 changed files with 294 additions and 30 deletions

View File

@@ -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.

View File

@@ -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
View 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)'")
}
}
}

View File

@@ -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 {

View File

@@ -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") {

View File

@@ -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
~~~~~~~ ~~~~~~~