Conversation
|
Note: the reason I went with the |
Sources/Variable.swift
Outdated
| current = normalize(current) | ||
|
|
||
| // try to evaluate bit if it's an indirection | ||
| var resolvedBit = bit |
There was a problem hiding this comment.
you can do for var bit and mutate bit itself
Sources/Variable.swift
Outdated
| var resolvedBit = bit | ||
| if bit.hasPrefix("$"), | ||
| let resolved = try? Variable(String(bit.dropFirst())).resolve(context), | ||
| let property = resolved { |
There was a problem hiding this comment.
maybe better to do resolved as? String?
There was a problem hiding this comment.
I had that first, but things break then if the resolved value is an Int for example (see the tests with array indirect lookup)
|
I love the idea. It's a shame that we can't use subscript for that though
if bit.hasPrefix("[") && bit.hasSuffix("]"),
let resolved = try? Variable(String(bit.dropFirst().dropLast())).resolve(context),
…
|
|
Jinja uses braces for lookup: |
|
Damn so that would still be better to find a way to do that with braces without dot, both because it feels more natural and because it's similar to Jinja… but I guess you tried to find a solution for that already? |
|
I'm honestly not sure how to tackle that without regular expressions, and without blowing this code up into some massive thing. |
03cf1db to
efc5ad3
Compare
AliSoftware
left a comment
There was a problem hiding this comment.
The fact that this solution uses o.$p and not o[p] also means that p can't be a compound expression 😢
So we can't express object[property.name|lowercase] for example with the $ syntax (except by using an intermediate {% set %} but that's not as pretty then)
|
I didn't test this code, but this might work: fileprivate func lookup() -> [String] {
return variable.split(separator: ".")
.flatMap({ $0.split(separator: "[", maxSplits: 1) })
.map({ String($0).trim(character: "]") })
}This will though only work on single brackets, like @AliSoftware good point about filter. |
|
Can also try |
|
@ilyapuchka Mmmh interesting idea to use a filter to extract the property. But that would still mean we couldn't apply a filter on that filter 😅 |
|
I really think the right syntax is to use the same syntax as Jinja, the question is how to implement it in Stencil without breaking all the code… |
|
@AliSoftware true. We can for that add support for brackets in filter expressions |
|
I'm working on a solution right now, might take a bit. I'm going to iterate over the characters (in the |
CHANGELOG.md
Outdated
| [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 indirect evaluation. For example, if you have a variable `key = "name"`, and an |
There was a problem hiding this comment.
This would be a breaking change right? If so then it should go into ### Breaking section too.
For example, the following that works in Stencil 0.11 would no longer work?:
let schema = [
"$schema": "http://json-schema.org/draft-06/schema#",
"type": "object",
"description" "A geographical coordinate",
"properties": [
"latitude": ["type": "number"],
"longitude": ["type": "number"],
]
]<h2>{{ schema.description|default:"Schema" }}</h2>
<a href="{{ schema.$schema }}">View Schema Specification</a>
<code>
{{ schema|serialiseJSON }}
</code>Perhaps an alternative would be to make this a filter itself instead of overloading special sub-syntax into this, then the same can be achieved by the following syntax which perhaps it a little clearer for the reader of the template:
{{ item|valueForKey:"key" }}There was a problem hiding this comment.
Yeah, I'm rewriting it to use object[property] syntax like jinja.
258e876 to
3e6bbf2
Compare
|
Since the force push is hiding the commit comments:
We should define the syntax a bit more. To make things a bit easer:
|
Sources/LookupParser.swift
Outdated
| } | ||
|
|
||
| guard !partialBits.isEmpty else { | ||
| throw TemplateSyntaxError("Attempting to dereference empty object in variable '\(variable)'") |
There was a problem hiding this comment.
What does this mean? Maybe we can say the same simpler? I guess this handles .. case? The we should just say that this is invalid syntax.
There was a problem hiding this comment.
It's more than just .., it could be stuff.., or stuff..other.stuff, or other cases. It's already a TemplateSyntaxError, this just specifies what's wrong.
There was a problem hiding this comment.
If I understand code flow correctly here errors will all fall into unexpected '.' in \(variable) kind of description.
Sources/LookupParser.swift
Outdated
| struct LookupParser { | ||
| private var bits = [String]() | ||
| private var current = "" | ||
| private var partialBits = [String]() |
There was a problem hiding this comment.
What partialBits stands for?
There was a problem hiding this comment.
In a lookup of my[a.b].stuff[ref][ref2].here, the parser will go through every bit, which might contain bracket syntax. So in this case, the partialBits would end up being:
my[a.b]stuff[ref][ref2]here
It's called bit because the result is a bunch of bits (old name, from before this PR), and partial, because it's still parsing the bit. Naming things is hard, and it's not like stuffBeforeDot is better?
Sources/LookupParser.swift
Outdated
|
|
||
| /// A structure used to represent a template variable, and to resolve it in a given context. | ||
| struct LookupParser { | ||
| private var bits = [String]() |
There was a problem hiding this comment.
Maybe rename it to keyPath?
There was a problem hiding this comment.
That'd be more a question for @kylef, what's the reason for the bits naming?
Sources/LookupParser.swift
Outdated
| // when opening the first bracket, we must have a partial bit | ||
| private mutating func openBracket() throws { | ||
| guard !partialBits.isEmpty || !current.isEmpty else { | ||
| throw TemplateSyntaxError("Attempting to dereference an empty object in variable '\(variable)'") |
There was a problem hiding this comment.
same as before, if this handles [[ syntax then let's just say that in error message
There was a problem hiding this comment.
Same answer as before. [[ might be just one of the cases, we can't assume that.
There was a problem hiding this comment.
And here it can be probably unexpected '[' in '\(variable)'
Sources/LookupParser.swift
Outdated
| import Foundation | ||
|
|
||
| /// A structure used to represent a template variable, and to resolve it in a given context. | ||
| struct LookupParser { |
There was a problem hiding this comment.
Maybe VariableLookup will be a better name? I would say that's the main purpose of this type where parsing is secondary.
There was a problem hiding this comment.
Also probably this can be a class, so you can remove mutating attribute (which will also probably save from unnecessary memory allocations in runtime)
There was a problem hiding this comment.
Naming things is hard. Maybe KeypathParser, using your keypath suggestion from before?
Input from others is welcome, so we can reach a consensus on this.
| try context.push(dictionary: ["property": 5]) { | ||
| let variable = Variable("contacts[property]") | ||
| let result = try variable.resolve(context) as? String | ||
| try expect(result).to.beNil() |
There was a problem hiding this comment.
it is probably unrelated to this PR, but I wonder if it would be better to raise runtime exceptions in such cases, I find it strange that Stencil just returns nils in such cases, it can be really hard to debug templates with such behaviour. I.e. I think EJS will raise exception in this case, but I don't know what is the common approach in other templating languages.
There was a problem hiding this comment.
I think django/jinja just return nils, like here. It's up to the developer to check for empty cases and handle these accordingly. How would you otherwise check if something exists or not, without triggering an exception?
There was a problem hiding this comment.
I see it like that. Lookup function should return nil if variable is not defined in the context, but if it is array subscript or type introspection it should throw. But even in the first case I don't think nil lookup result should be rendered as empty strings, it should be exception. In Jinja I believe one can check if variable is not nil using is defined ({% if loop.previtem is defined and value > loop.previtem %}). But I'm not very well familiar with Jinja and Django to advocate for that and it is not related to this PR so lets leave it for another time.
There was a problem hiding this comment.
Quote from Jinja template docs:
If a variable or attribute does not exist, you will get back an undefined value. What you can do with that kind of value depends on the application configuration: the default behavior is to evaluate to an empty string if printed or iterated over, and to fail for every other operation.
So it returns an invalid value (doesn't throw), even for subscripts, that can be printed, iterated (and tested in ifs I assume), but other operations will fail (exception) for it.
| } | ||
| } | ||
|
|
||
| $0.it("can resolve multiple indirections") { |
| } | ||
| } | ||
|
|
||
| $0.it("can resolve multiple indirections") { |
There was a problem hiding this comment.
I think in implementation you call it as references, I guess it should be used here too
There was a problem hiding this comment.
I'm not sure if it should be called indirections, references, subscripts, ... 😕
There was a problem hiding this comment.
For those with Swift background the most clear name would be probably subscript.
| } | ||
| } | ||
|
|
||
| $0.it("can resolve recursive indirections") { |
| "article.author[prop]]", | ||
| "article.author[]", | ||
| "article.author[[]]", | ||
| "article.author[prop][]", |
There was a problem hiding this comment.
also article.author[prop]comments (missing . after ])
8d76b79 to
e395a10
Compare
ilyapuchka
left a comment
There was a problem hiding this comment.
I think this is awesome 👏
This allows users to refer to variables (and properties) using indirection.
Let's say for example we have the following context:
A user can then write:
To produce: