Skip to content

Commit

Permalink
Merge branch 'master' into dynamic-filter
Browse files Browse the repository at this point in the history
  • Loading branch information
ilyapuchka committed Aug 4, 2018
2 parents f3d5843 + 4f14b4b commit bd1872a
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 29 deletions.
89 changes: 64 additions & 25 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@

- Added an optional second parameter to the `include` tag for passing a sub context to the included file.
[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
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)

- Added support for dynamic filter using `filter` filter.
- Added support for dynamic filter using `filter` filter. With that you can define a variable with a name of filter
, i.e. `myfilter = "uppercase"` and then use it to invoke this filter with `{{ string|filter:myfilter }}`.
[Ilya Puchka](https://github.com/ilyapuchka)
[#203](https://github.com/stencilproject/Stencil/pull/203)

### Bug Fixes

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

Expand All @@ -27,28 +30,64 @@

### 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
their stored properties.
- Added `split` filter
- Allow default string filters to be applied to arrays
- Similar filters are suggested when unknown filter is used
- Added `indent` filter
- Allow using new lines inside tags
- Added support for iterating arrays of tuples
- Added support for ranges in if-in expression
- Added property `forloop.length` to get number of items in the loop
- Now you can construct ranges for loops using `a...b` syntax, i.e. `for i in 1...array.count`
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
- Fixed checking dictionary values for nil in `default` filter
- Fixed comparing string variables with string literals, in Swift 4 string literals became `Substring` and thus couldn't be directly compared to strings.
- Integer literals now resolve into Int values, not Float
- Fixed accessing properties of optional properties via reflection
- No longer render optional values in arrays as `Optional(..)`
- Fixed subscription tuples by value index, i.e. `{{ tuple.0 }}`
- 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
Expand Down Expand Up @@ -249,10 +288,10 @@
### Bug Fixes

- 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)

- 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)

- If comparisons will now treat 0 and below numbers as negative.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ Resources to help you integrate Stencil into a Swift project:

[Sourcery](https://github.com/krzysztofzablocki/Sourcery),
[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

Expand Down
112 changes: 112 additions & 0 deletions Sources/KeyPath.swift
Original file line number Diff line number Diff line change
@@ -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)'")
}
}
}
8 changes: 5 additions & 3 deletions Sources/Variable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ public struct Variable : Equatable, Resolvable {
self.variable = variable
}

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

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

for bit in lookup() {
for bit in try lookup(context) {
current = normalize(current)

if let context = current as? Context {
Expand Down
92 changes: 92 additions & 0 deletions Tests/StencilTests/VariableSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,98 @@ func testVariable() {
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") {
Expand Down
18 changes: 18 additions & 0 deletions docs/templates.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,24 @@ For example, if `people` was an array:
There are {{ people.count }} people. {{ people.first }} is the first
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
~~~~~~~

Expand Down

0 comments on commit bd1872a

Please sign in to comment.