Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@

- 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/yonaskolb)
[#178](https://github.com/stencilproject/Stencil/pull/178)

### Bug Fixes

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


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][]",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also article.author[prop]comments (missing . after ])

"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