Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added filter to apply dynamic filters #203

Merged
merged 9 commits into from
Oct 1, 2018
Merged
Show file tree
Hide file tree
Changes from 4 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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@
[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)
[Ilya Puchka](https://github.com/ilyapuchka)
[#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
, 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

Expand Down
2 changes: 1 addition & 1 deletion Sources/Expression.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
protocol Expression: CustomStringConvertible {
public protocol Expression: CustomStringConvertible {
func evaluate(context: Context) throws -> Bool
}

Expand Down
14 changes: 9 additions & 5 deletions Sources/Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ open class Extension {

/// Registers a template filter with the given name
public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?]) throws -> Any?) {
ilyapuchka marked this conversation as resolved.
Show resolved Hide resolved
filters[name] = .arguments({ value, args, _ in try filter(value, args) })
}

public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?], Context) throws -> Any?) {
filters[name] = .arguments(filter)
}
}
Expand Down Expand Up @@ -59,28 +63,28 @@ class DefaultExtension: Extension {
registerFilter("join", filter: joinFilter)
registerFilter("split", filter: splitFilter)
registerFilter("indent", filter: indentFilter)
registerFilter("filter", filter: filterFilter)
}
}


protocol FilterType {
func invoke(value: Any?, arguments: [Any?]) throws -> Any?
func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any?
}

enum Filter: FilterType {
case simple(((Any?) throws -> Any?))
case arguments(((Any?, [Any?]) throws -> Any?))
case arguments(((Any?, [Any?], Context) throws -> Any?))

func invoke(value: Any?, arguments: [Any?]) throws -> Any? {
func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any? {
switch self {
case let .simple(filter):
if !arguments.isEmpty {
throw TemplateSyntaxError("cannot invoke filter with an argument")
}

return try filter(value)
case let .arguments(filter):
return try filter(value, arguments)
return try filter(value, arguments, context)
}
}
}
17 changes: 15 additions & 2 deletions Sources/Filters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func defaultFilter(value: Any?, arguments: [Any?]) -> Any? {

func joinFilter(value: Any?, arguments: [Any?]) throws -> Any? {
guard arguments.count < 2 else {
throw TemplateSyntaxError("'join' filter takes a single argument")
throw TemplateSyntaxError("'join' filter takes at most one argument")
}

let separator = stringify(arguments.first ?? "")
Expand All @@ -55,7 +55,7 @@ func joinFilter(value: Any?, arguments: [Any?]) throws -> Any? {

func splitFilter(value: Any?, arguments: [Any?]) throws -> Any? {
guard arguments.count < 2 else {
throw TemplateSyntaxError("'split' filter takes a single argument")
throw TemplateSyntaxError("'split' filter takes at most one argument")
}

let separator = stringify(arguments.first ?? " ")
Expand Down Expand Up @@ -111,3 +111,16 @@ func indent(_ content: String, indentation: String, indentFirst: Bool) -> String
return result.joined(separator: "\n")
}

func filterFilter(value: Any?, arguments: [Any?], context: Context) throws -> Any? {
guard let value = value else { return nil }
guard arguments.count == 1 else {
throw TemplateSyntaxError("'filter' filter takes one argument")
}

let attribute = stringify(arguments[0])

let expr = try context.environment.compileFilter("$0|\(attribute)")
return try context.push(dictionary: ["$0": value]) {
try expr.resolve(context)
}
}
2 changes: 1 addition & 1 deletion Sources/ForTag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class ForNode : NodeType {
let resolvable = try parser.compileResolvable(components[3])

let `where` = hasToken("where", at: 4)
? try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser)
? try parser.compileExpression(components: Array(components.suffix(from: 5)))
: nil

return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`)
Expand Down
16 changes: 5 additions & 11 deletions Sources/IfTag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ final class IfExpressionParser {
let tokens: [IfToken]
var position: Int = 0

init(components: [String], tokenParser: TokenParser) throws {
init(components: [String], environment: Environment) throws {
self.tokens = try components.map { component in
if let op = findOperator(name: component) {
switch op {
Expand All @@ -111,7 +111,7 @@ final class IfExpressionParser {
}
}

return .variable(try tokenParser.compileResolvable(component))
return .variable(try environment.compileResolvable(component))
}
}

Expand Down Expand Up @@ -155,12 +155,6 @@ final class IfExpressionParser {
}


func parseExpression(components: [String], tokenParser: TokenParser) throws -> Expression {
let parser = try IfExpressionParser(components: components, tokenParser: tokenParser)
return try parser.parse()
}


/// Represents an if condition and the associated nodes when the condition
/// evaluates
final class IfCondition {
Expand All @@ -187,7 +181,7 @@ class IfNode : NodeType {
var components = token.components()
components.removeFirst()

let expression = try parseExpression(components: components, tokenParser: parser)
let expression = try parser.compileExpression(components: components)
let nodes = try parser.parse(until(["endif", "elif", "else"]))
var conditions: [IfCondition] = [
IfCondition(expression: expression, nodes: nodes)
Expand All @@ -197,7 +191,7 @@ class IfNode : NodeType {
while let current = token, current.contents.hasPrefix("elif") {
var components = current.components()
components.removeFirst()
let expression = try parseExpression(components: components, tokenParser: parser)
let expression = try parser.compileExpression(components: components)

let nodes = try parser.parse(until(["endif", "elif", "else"]))
token = parser.nextToken()
Expand Down Expand Up @@ -236,7 +230,7 @@ class IfNode : NodeType {
_ = parser.nextToken()
}

let expression = try parseExpression(components: components, tokenParser: parser)
let expression = try parser.compileExpression(components: components)
return IfNode(conditions: [
IfCondition(expression: expression, nodes: trueNodes),
IfCondition(expression: nil, nodes: falseNodes),
Expand Down
32 changes: 26 additions & 6 deletions Sources/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public class TokenParser {
}

if let tag = token.components().first {
let parser = try findTag(name: tag)
let parser = try environment.findTag(name: tag)
nodes.append(try parser(self, token))
}
case .comment:
Expand All @@ -71,8 +71,24 @@ public class TokenParser {
tokens.insert(token, at: 0)
}

public func compileFilter(_ token: String) throws -> Resolvable {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do these methods (compileFilter, compileExpression and compileResolvable) have to be public?

Note: if they can be internal, don't forget to reset the Expression protocol back to internal.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So that they can be used by 3rd party extensions

Copy link
Contributor

Choose a reason for hiding this comment

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

Ehm, do they need to be used by 3rd party extensions? Do you have a scenario where that'd be necessary?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I cant think of any now but if I needed it in filter then it might be needed in something else complex too. Anyway, I don't see any harm in making these methods public.

Copy link
Contributor

Choose a reason for hiding this comment

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

Honestly, a library should only expose the minimal surface necessary for a developer to use it. If at any point, something is missing, it can be added later on.

Maybe someone else has some thoughts on this point? @AliSoftware?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd normally agree with limiting the surface of the public API, but given all other stuff like parse(until:), nextToken() and prependToken are already public too, I say it's fine by me.

TBH I haven't used Stencil as a library to be extended for a while now (only use case for us is StencilSwiftKit to extend Stencil), not sure there are many people using those features — main uses of this lib is probably just using it as-is to render templates, adding maybe some custom filters and that's all), that shouldn't really bother them.

BUT, if we're gonna have so much public API, then we should then definitely add doc-comments to them to explain to end users what they're supposed to be used for (especially to those not completely familiar with the concepts of a parser vs lexer etc, and proper definition of token etc).

Copy link
Contributor

Choose a reason for hiding this comment

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

Good idea on the docs then. It took me quite a while to understand the codebase a bit, and the documentation in the code is quite sparse.

return try environment.compileFilter(token)
}

public func compileExpression(components: [String]) throws -> Expression {
return try environment.compileExpression(components: components)
}

public func compileResolvable(_ token: String) throws -> Resolvable {
return try environment.compileResolvable(token)
}

}

extension Environment {

func findTag(name: String) throws -> Extension.TagParser {
for ext in environment.extensions {
for ext in extensions {
if let filter = ext.tags[name] {
return filter
}
Expand All @@ -82,7 +98,7 @@ public class TokenParser {
}

func findFilter(_ name: String) throws -> FilterType {
for ext in environment.extensions {
for ext in extensions {
if let filter = ext.filters[name] {
return filter
}
Expand All @@ -97,7 +113,7 @@ public class TokenParser {
}

private func suggestedFilters(for name: String) -> [String] {
let allFilters = environment.extensions.flatMap({ $0.filters.keys })
let allFilters = extensions.flatMap({ $0.filters.keys })

let filtersWithDistance = allFilters
.map({ (filterName: $0, distance: $0.levenshteinDistance(name)) })
Expand All @@ -111,11 +127,15 @@ public class TokenParser {
}

public func compileFilter(_ token: String) throws -> Resolvable {
ilyapuchka marked this conversation as resolved.
Show resolved Hide resolved
return try FilterExpression(token: token, parser: self)
return try FilterExpression(token: token, environment: self)
}

public func compileExpression(components: [String]) throws -> Expression {
return try IfExpressionParser(components: components, environment: self).parse()
}

public func compileResolvable(_ token: String) throws -> Resolvable {
return try RangeVariable(token, parser: self)
return try RangeVariable(token, environment: self)
?? compileFilter(token)
}

Expand Down
14 changes: 7 additions & 7 deletions Sources/Variable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ class FilterExpression : Resolvable {
let filters: [(FilterType, [Variable])]
let variable: Variable

init(token: String, parser: TokenParser) throws {
let bits = token.characters.split(separator: "|").map({ String($0).trim(character: " ") })
init(token: String, environment: Environment) throws {
let bits = token.smartSplit(separator: "|").map({ String($0).trim(character: " ") })
if bits.isEmpty {
filters = []
variable = Variable("")
Expand All @@ -22,7 +22,7 @@ class FilterExpression : Resolvable {
do {
filters = try filterBits.map {
let (name, arguments) = parseFilterComponents(token: $0)
let filter = try parser.findFilter(name)
let filter = try environment.findFilter(name)
return (filter, arguments)
}
} catch {
Expand All @@ -36,7 +36,7 @@ class FilterExpression : Resolvable {

return try filters.reduce(result) { x, y in
let arguments = try y.1.map { try $0.resolve(context) }
return try y.0.invoke(value: x, arguments: arguments)
return try y.0.invoke(value: x, arguments: arguments, context: context)
}
}
}
Expand Down Expand Up @@ -140,14 +140,14 @@ public struct RangeVariable: Resolvable {
public let from: Resolvable
public let to: Resolvable

public init?(_ token: String, parser: TokenParser) throws {
public init?(_ token: String, environment: Environment) throws {
let components = token.components(separatedBy: "...")
guard components.count == 2 else {
return nil
}

self.from = try parser.compileFilter(components[0])
self.to = try parser.compileFilter(components[1])
self.from = try environment.compileFilter(components[0])
self.to = try environment.compileFilter(components[1])
}

public func resolve(_ context: Context) throws -> Any? {
Expand Down
24 changes: 12 additions & 12 deletions Tests/StencilTests/ExpressionSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,19 @@ func testExpressions() {

$0.describe("expression parsing") {
$0.it("can parse a variable expression") {
let expression = try parseExpression(components: ["value"], tokenParser: parser)
let expression = try parser.compileExpression(components: ["value"])
try expect(expression.evaluate(context: Context())).to.beFalse()
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue()
}

$0.it("can parse a not expression") {
let expression = try parseExpression(components: ["not", "value"], tokenParser: parser)
let expression = try parser.compileExpression(components: ["not", "value"])
try expect(expression.evaluate(context: Context())).to.beTrue()
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse()
}

$0.describe("and expression") {
let expression = try! parseExpression(components: ["lhs", "and", "rhs"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["lhs", "and", "rhs"])

$0.it("evaluates to false with lhs false") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse()
Expand All @@ -137,7 +137,7 @@ func testExpressions() {
}

$0.describe("or expression") {
let expression = try! parseExpression(components: ["lhs", "or", "rhs"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["lhs", "or", "rhs"])

$0.it("evaluates to true with lhs true") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue()
Expand All @@ -157,7 +157,7 @@ func testExpressions() {
}

$0.describe("equality expression") {
let expression = try! parseExpression(components: ["lhs", "==", "rhs"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["lhs", "==", "rhs"])

$0.it("evaluates to true with equal lhs/rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue()
Expand Down Expand Up @@ -193,7 +193,7 @@ func testExpressions() {
}

$0.describe("inequality expression") {
let expression = try! parseExpression(components: ["lhs", "!=", "rhs"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["lhs", "!=", "rhs"])

$0.it("evaluates to true with inequal lhs/rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue()
Expand All @@ -205,7 +205,7 @@ func testExpressions() {
}

$0.describe("more than expression") {
let expression = try! parseExpression(components: ["lhs", ">", "rhs"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["lhs", ">", "rhs"])

$0.it("evaluates to true with lhs > rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue()
Expand All @@ -217,7 +217,7 @@ func testExpressions() {
}

$0.describe("more than equal expression") {
let expression = try! parseExpression(components: ["lhs", ">=", "rhs"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["lhs", ">=", "rhs"])

$0.it("evaluates to true with lhs == rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
Expand All @@ -229,7 +229,7 @@ func testExpressions() {
}

$0.describe("less than expression") {
let expression = try! parseExpression(components: ["lhs", "<", "rhs"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["lhs", "<", "rhs"])

$0.it("evaluates to true with lhs < rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue()
Expand All @@ -241,7 +241,7 @@ func testExpressions() {
}

$0.describe("less than equal expression") {
let expression = try! parseExpression(components: ["lhs", "<=", "rhs"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["lhs", "<=", "rhs"])

$0.it("evaluates to true with lhs == rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
Expand All @@ -253,7 +253,7 @@ func testExpressions() {
}

$0.describe("multiple expression") {
let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["one", "or", "two", "and", "not", "three"])

$0.it("evaluates to true with one") {
try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue()
Expand Down Expand Up @@ -281,7 +281,7 @@ func testExpressions() {
}

$0.describe("in expression") {
let expression = try! parseExpression(components: ["lhs", "in", "rhs"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["lhs", "in", "rhs"])

$0.it("evaluates to true when rhs contains lhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue()
Expand Down
Loading