Skip to content

Commit 2c411ca

Browse files
committed
Merge branch 'master' into dynamic-filter
2 parents f3d5843 + 4f14b4b commit 2c411ca

File tree

6 files changed

+294
-30
lines changed

6 files changed

+294
-30
lines changed

CHANGELOG.md

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,22 @@
66

77
- Added an optional second parameter to the `include` tag for passing a sub context to the included file.
88
[Yonas Kolb](https://github.com/yonaskolb)
9-
[#394](https://github.com/stencilproject/Stencil/pull/214)
10-
11-
- Adds support for using spaces in filter expression.
9+
[#214](https://github.com/stencilproject/Stencil/pull/214)
10+
- Variables now support the subscript notation. For example, if you have a variable `key = "name"`, and an
11+
object `item = ["name": "John"]`, then `{{ item[key] }}` will evaluate to "John".
12+
[David Jennes](https://github.com/djbe)
13+
[#215](https://github.com/stencilproject/Stencil/pull/215)
14+
- Adds support for using spaces in filter expression.
1215
[Ilya Puchka](https://github.com/ilyapuchka)
1316
[#178](https://github.com/stencilproject/Stencil/pull/178)
14-
15-
- Added support for dynamic filter using `filter` filter.
17+
- Added support for dynamic filter using `filter` filter. With that you can define a variable with a name of filter
18+
, i.e. `myfilter = "uppercase"` and then use it to invoke this filter with `{{ string|filter:myfilter }}`.
1619
[Ilya Puchka](https://github.com/ilyapuchka)
1720
[#203](https://github.com/stencilproject/Stencil/pull/203)
1821

1922
### Bug Fixes
2023

21-
- Fixed using quote as a filter parameter
24+
- Fixed using quote as a filter parameter.
2225
[Ilya Puchka](https://github.com/ilyapuchka)
2326
[#210](https://github.com/stencilproject/Stencil/pull/210)
2427

@@ -27,28 +30,64 @@
2730

2831
### Enhancements
2932

30-
- Added support for resolving superclass properties for not-NSObject subclasses
33+
- Added support for resolving superclass properties for not-NSObject subclasses.
34+
[Ilya Puchka](https://github.com/ilyapuchka)
35+
[#152](https://github.com/stencilproject/Stencil/pull/152)
3136
- The `{% for %}` tag can now iterate over tuples, structures and classes via
32-
their stored properties.
33-
- Added `split` filter
34-
- Allow default string filters to be applied to arrays
35-
- Similar filters are suggested when unknown filter is used
36-
- Added `indent` filter
37-
- Allow using new lines inside tags
38-
- Added support for iterating arrays of tuples
39-
- Added support for ranges in if-in expression
40-
- Added property `forloop.length` to get number of items in the loop
41-
- Now you can construct ranges for loops using `a...b` syntax, i.e. `for i in 1...array.count`
37+
their stored properties.
38+
[Ilya Puchka](https://github.com/ilyapuchka)
39+
[#172](https://github.com/stencilproject/Stencil/pull/173)
40+
- Added `split` filter.
41+
[Ilya Puchka](https://github.com/ilyapuchka)
42+
[#187](https://github.com/stencilproject/Stencil/pull/187)
43+
- Allow default string filters to be applied to arrays.
44+
[Ilya Puchka](https://github.com/ilyapuchka)
45+
[#190](https://github.com/stencilproject/Stencil/pull/190)
46+
- Similar filters are suggested when unknown filter is used.
47+
[Ilya Puchka](https://github.com/ilyapuchka)
48+
[#186](https://github.com/stencilproject/Stencil/pull/186)
49+
- Added `indent` filter.
50+
[Ilya Puchka](https://github.com/ilyapuchka)
51+
[#188](https://github.com/stencilproject/Stencil/pull/188)
52+
- Allow using new lines inside tags.
53+
[Ilya Puchka](https://github.com/ilyapuchka)
54+
[#202](https://github.com/stencilproject/Stencil/pull/202)
55+
- Added support for iterating arrays of tuples.
56+
[Ilya Puchka](https://github.com/ilyapuchka)
57+
[#177](https://github.com/stencilproject/Stencil/pull/177)
58+
- Added support for ranges in if-in expression.
59+
[Ilya Puchka](https://github.com/ilyapuchka)
60+
[#193](https://github.com/stencilproject/Stencil/pull/193)
61+
- Added property `forloop.length` to get number of items in the loop.
62+
[Ilya Puchka](https://github.com/ilyapuchka)
63+
[#171](https://github.com/stencilproject/Stencil/pull/171)
64+
- Now you can construct ranges for loops using `a...b` syntax, i.e. `for i in 1...array.count`.
65+
[Ilya Puchka](https://github.com/ilyapuchka)
66+
[#192](https://github.com/stencilproject/Stencil/pull/192)
4267

4368
### Bug Fixes
4469

45-
- Fixed rendering `{{ block.super }}` with several levels of inheritance
46-
- Fixed checking dictionary values for nil in `default` filter
47-
- Fixed comparing string variables with string literals, in Swift 4 string literals became `Substring` and thus couldn't be directly compared to strings.
48-
- Integer literals now resolve into Int values, not Float
49-
- Fixed accessing properties of optional properties via reflection
50-
- No longer render optional values in arrays as `Optional(..)`
51-
- Fixed subscription tuples by value index, i.e. `{{ tuple.0 }}`
70+
- Fixed rendering `{{ block.super }}` with several levels of inheritance.
71+
[Ilya Puchka](https://github.com/ilyapuchka)
72+
[#154](https://github.com/stencilproject/Stencil/pull/154)
73+
- Fixed checking dictionary values for nil in `default` filter.
74+
[Ilya Puchka](https://github.com/ilyapuchka)
75+
[#162](https://github.com/stencilproject/Stencil/pull/162)
76+
- Fixed comparing string variables with string literals, in Swift 4 string literals became `Substring` and thus couldn't be directly compared to strings.
77+
[Ilya Puchka](https://github.com/ilyapuchka)
78+
[#168](https://github.com/stencilproject/Stencil/pull/168)
79+
- Integer literals now resolve into Int values, not Float.
80+
[Ilya Puchka](https://github.com/ilyapuchka)
81+
[#181](https://github.com/stencilproject/Stencil/pull/181)
82+
- Fixed accessing properties of optional properties via reflection.
83+
[Ilya Puchka](https://github.com/ilyapuchka)
84+
[#204](https://github.com/stencilproject/Stencil/pull/204)
85+
- No longer render optional values in arrays as `Optional(..)`.
86+
[Ilya Puchka](https://github.com/ilyapuchka)
87+
[#205](https://github.com/stencilproject/Stencil/pull/205)
88+
- Fixed subscription tuples by value index, i.e. `{{ tuple.0 }}`.
89+
[Ilya Puchka](https://github.com/ilyapuchka)
90+
[#172](https://github.com/stencilproject/Stencil/pull/172)
5291

5392

5493
## 0.10.1
@@ -249,10 +288,10 @@
249288
### Bug Fixes
250289
251290
- Variables (`{{ variable.5 }}`) that reference an array index at an unknown
252-
index will now resolve to `nil` instead of causing a crash.
291+
index will now resolve to `nil` instead of causing a crash.
253292
[#72](https://github.com/kylef/Stencil/issues/72)
254293
255-
- Templates can now extend templates that extend other templates.
294+
- Templates can now extend templates that extend other templates.
256295
[#60](https://github.com/kylef/Stencil/issues/60)
257296
258297
- If comparisons will now treat 0 and below numbers as negative.

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ Resources to help you integrate Stencil into a Swift project:
6767

6868
[Sourcery](https://github.com/krzysztofzablocki/Sourcery),
6969
[SwiftGen](https://github.com/SwiftGen/SwiftGen),
70-
[Kitura](https://github.com/IBM-Swift/Kitura)
70+
[Kitura](https://github.com/IBM-Swift/Kitura),
71+
[Weaver](https://github.com/scribd/Weaver)
7172

7273
## License
7374

Sources/KeyPath.swift

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import Foundation
2+
3+
/// A structure used to represent a template variable, and to resolve it in a given context.
4+
final class KeyPath {
5+
private var components = [String]()
6+
private var current = ""
7+
private var partialComponents = [String]()
8+
private var subscriptLevel = 0
9+
10+
let variable: String
11+
let context: Context
12+
13+
// Split the keypath string and resolve references if possible
14+
init(_ variable: String, in context: Context) {
15+
self.variable = variable
16+
self.context = context
17+
}
18+
19+
func parse() throws -> [String] {
20+
defer {
21+
components = []
22+
current = ""
23+
partialComponents = []
24+
subscriptLevel = 0
25+
}
26+
27+
for c in variable.characters {
28+
switch c {
29+
case "." where subscriptLevel == 0:
30+
try foundSeparator()
31+
case "[":
32+
try openBracket()
33+
case "]":
34+
try closeBracket()
35+
default:
36+
try addCharacter(c)
37+
}
38+
}
39+
try finish()
40+
41+
return components
42+
}
43+
44+
private func foundSeparator() throws {
45+
if !current.isEmpty {
46+
partialComponents.append(current)
47+
}
48+
49+
guard !partialComponents.isEmpty else {
50+
throw TemplateSyntaxError("Unexpected '.' in variable '\(variable)'")
51+
}
52+
53+
components += partialComponents
54+
current = ""
55+
partialComponents = []
56+
}
57+
58+
// when opening the first bracket, we must have a partial component
59+
private func openBracket() throws {
60+
guard !partialComponents.isEmpty || !current.isEmpty else {
61+
throw TemplateSyntaxError("Unexpected '[' in variable '\(variable)'")
62+
}
63+
64+
if subscriptLevel > 0 {
65+
current.append("[")
66+
} else if !current.isEmpty {
67+
partialComponents.append(current)
68+
current = ""
69+
}
70+
71+
subscriptLevel += 1
72+
}
73+
74+
// for a closing bracket at root level, try to resolve the reference
75+
private func closeBracket() throws {
76+
guard subscriptLevel > 0 else {
77+
throw TemplateSyntaxError("Unbalanced ']' in variable '\(variable)'")
78+
}
79+
80+
if subscriptLevel > 1 {
81+
current.append("]")
82+
} else if !current.isEmpty,
83+
let value = try Variable(current).resolve(context) {
84+
partialComponents.append("\(value)")
85+
current = ""
86+
} else {
87+
throw TemplateSyntaxError("Unable to resolve subscript '\(current)' in variable '\(variable)'")
88+
}
89+
90+
subscriptLevel -= 1
91+
}
92+
93+
private func addCharacter(_ c: Character) throws {
94+
guard partialComponents.isEmpty || subscriptLevel > 0 else {
95+
throw TemplateSyntaxError("Unexpected character '\(c)' in variable '\(variable)'")
96+
}
97+
98+
current.append(c)
99+
}
100+
101+
private func finish() throws {
102+
// check if we have a last piece
103+
if !current.isEmpty {
104+
partialComponents.append(current)
105+
}
106+
components += partialComponents
107+
108+
guard subscriptLevel == 0 else {
109+
throw TemplateSyntaxError("Unbalanced subscript brackets in variable '\(variable)'")
110+
}
111+
}
112+
}

Sources/Variable.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@ public struct Variable : Equatable, Resolvable {
5050
self.variable = variable
5151
}
5252

53-
fileprivate func lookup() -> [String] {
54-
return variable.characters.split(separator: ".").map(String.init)
53+
// Split the lookup string and resolve references if possible
54+
fileprivate func lookup(_ context: Context) throws -> [String] {
55+
var keyPath = KeyPath(variable, in: context)
56+
return try keyPath.parse()
5557
}
5658

5759
/// Resolve the variable in the given context
@@ -75,7 +77,7 @@ public struct Variable : Equatable, Resolvable {
7577
return bool
7678
}
7779

78-
for bit in lookup() {
80+
for bit in try lookup(context) {
7981
current = normalize(current)
8082

8183
if let context = current as? Context {

Tests/StencilTests/VariableSpec.swift

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,98 @@ func testVariable() {
188188
let result = try variable.resolve(context) as? Int
189189
try expect(result) == 2
190190
}
191+
192+
$0.describe("Subrscripting") {
193+
$0.it("can resolve a property subscript via reflection") {
194+
try context.push(dictionary: ["property": "name"]) {
195+
let variable = Variable("article.author[property]")
196+
let result = try variable.resolve(context) as? String
197+
try expect(result) == "Kyle"
198+
}
199+
}
200+
201+
$0.it("can subscript an array with a valid index") {
202+
try context.push(dictionary: ["property": 0]) {
203+
let variable = Variable("contacts[property]")
204+
let result = try variable.resolve(context) as? String
205+
try expect(result) == "Katie"
206+
}
207+
}
208+
209+
$0.it("can subscript an array with an unknown index") {
210+
try context.push(dictionary: ["property": 5]) {
211+
let variable = Variable("contacts[property]")
212+
let result = try variable.resolve(context) as? String
213+
try expect(result).to.beNil()
214+
}
215+
}
216+
217+
#if os(OSX)
218+
$0.it("can resolve a subscript via KVO") {
219+
try context.push(dictionary: ["property": "name"]) {
220+
let variable = Variable("object[property]")
221+
let result = try variable.resolve(context) as? String
222+
try expect(result) == "Foo"
223+
}
224+
}
225+
#endif
226+
227+
$0.it("can resolve an optional subscript via reflection") {
228+
try context.push(dictionary: ["property": "featuring"]) {
229+
let variable = Variable("blog[property].author.name")
230+
let result = try variable.resolve(context) as? String
231+
try expect(result) == "Jhon"
232+
}
233+
}
234+
235+
$0.it("can resolve multiple subscripts") {
236+
try context.push(dictionary: [
237+
"prop1": "articles",
238+
"prop2": 0,
239+
"prop3": "name"
240+
]) {
241+
let variable = Variable("blog[prop1][prop2].author[prop3]")
242+
let result = try variable.resolve(context) as? String
243+
try expect(result) == "Kyle"
244+
}
245+
}
246+
247+
$0.it("can resolve nested subscripts") {
248+
try context.push(dictionary: [
249+
"prop1": "prop2",
250+
"ref": ["prop2": "name"]
251+
]) {
252+
let variable = Variable("article.author[ref[prop1]]")
253+
let result = try variable.resolve(context) as? String
254+
try expect(result) == "Kyle"
255+
}
256+
}
257+
258+
$0.it("throws for invalid keypath syntax") {
259+
try context.push(dictionary: ["prop": "name"]) {
260+
let samples = [
261+
".",
262+
"..",
263+
".test",
264+
"test..test",
265+
"[prop]",
266+
"article.author[prop",
267+
"article.author[[prop]",
268+
"article.author[prop]]",
269+
"article.author[]",
270+
"article.author[[]]",
271+
"article.author[prop][]",
272+
"article.author[prop]comments",
273+
"article.author[.]"
274+
]
275+
276+
for lookup in samples {
277+
let variable = Variable(lookup)
278+
try expect(variable.resolve(context)).toThrow()
279+
}
280+
}
281+
}
282+
}
191283
}
192284

193285
describe("RangeVariable") {

docs/templates.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,24 @@ For example, if `people` was an array:
3131
There are {{ people.count }} people. {{ people.first }} is the first
3232
person, followed by {{ people.1 }}.
3333

34+
You can also use the subscript operator for indirect evaluation. The expression
35+
between brackets will be evaluated first, before the actual lookup will happen.
36+
37+
For example, if you have the following context:
38+
39+
.. code-block:: swift
40+
41+
[
42+
"item": [
43+
"name": "John"
44+
],
45+
"key": "name"
46+
]
47+
48+
.. code-block:: html+django
49+
50+
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.
51+
3452
Filters
3553
~~~~~~~
3654

0 commit comments

Comments
 (0)