Skip to content

Commit

Permalink
Merge pull request #203 from stencilproject/dynamic-filter
Browse files Browse the repository at this point in the history
Added filter to apply dynamic filters
  • Loading branch information
djbe authored Oct 1, 2018
2 parents 4faf8f5 + 08fc21d commit 6f3ca60
Show file tree
Hide file tree
Showing 14 changed files with 131 additions and 56 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ _None_

### Enhancements

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

### Deprecations

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
15 changes: 10 additions & 5 deletions Sources/Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ open class Extension {

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

/// Registers a template filter with the given name
public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?], Context) throws -> Any?) {
filters[name] = .arguments(filter)
}
}
Expand Down Expand Up @@ -68,28 +73,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 @@ -115,3 +115,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 @@ -31,7 +31,7 @@ class ForNode : NodeType {
let resolvable = try parser.compileResolvable(components[3], containedIn: token)

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

let forNodes = try parser.parse(until(["endfor", "empty"]))
Expand Down
24 changes: 10 additions & 14 deletions Sources/IfTag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,11 @@ final class IfExpressionParser {
self.tokens = tokens
}

static func parser(components: [String], tokenParser: TokenParser, token: Token) throws -> IfExpressionParser {
return try IfExpressionParser(components: ArraySlice(components), tokenParser: tokenParser, token: token)
static func parser(components: [String], environment: Environment, token: Token) throws -> IfExpressionParser {
return try IfExpressionParser(components: ArraySlice(components), environment: environment, token: token)
}

private init(components: ArraySlice<String>, tokenParser: TokenParser, token: Token) throws {
private init(components: ArraySlice<String>, environment: Environment, token: Token) throws {
var parsedComponents = Set<Int>()
var bracketsBalance = 0
self.tokens = try zip(components.indices, components).compactMap { (index, component) in
Expand All @@ -125,7 +125,7 @@ final class IfExpressionParser {
bracketsBalance += 1
let (expression, parsedCount) = try IfExpressionParser.subExpression(
from: components.suffix(from: index + 1),
tokenParser: tokenParser,
environment: environment,
token: token
)
parsedComponents.formUnion(Set(index...(index + parsedCount)))
Expand All @@ -147,12 +147,12 @@ final class IfExpressionParser {
return .prefix(name: name, bindingPower: bindingPower, operatorType: operatorType)
}
}
return .variable(try tokenParser.compileResolvable(component, containedIn: token))
return .variable(try environment.compileResolvable(component, containedIn: token))
}
}
}

private static func subExpression(from components: ArraySlice<String>, tokenParser: TokenParser, token: Token) throws -> (Expression, Int) {
private static func subExpression(from components: ArraySlice<String>, environment: Environment, token: Token) throws -> (Expression, Int) {
var bracketsBalance = 1
let subComponents = components
.prefix(while: {
Expand All @@ -167,7 +167,7 @@ final class IfExpressionParser {
throw TemplateSyntaxError("'if' expression error: missing closing bracket")
}

let expressionParser = try IfExpressionParser(components: subComponents, tokenParser: tokenParser, token: token)
let expressionParser = try IfExpressionParser(components: subComponents, environment: environment, token: token)
let expression = try expressionParser.parse()
return (expression, subComponents.count)
}
Expand Down Expand Up @@ -211,10 +211,6 @@ final class IfExpressionParser {
}
}

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

/// Represents an if condition and the associated nodes when the condition
/// evaluates
Expand Down Expand Up @@ -243,7 +239,7 @@ class IfNode : NodeType {
var components = token.components
components.removeFirst()

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

let nodes = try parser.parse(until(["endif", "elif", "else"]))
nextToken = parser.nextToken()
Expand Down Expand Up @@ -281,7 +277,7 @@ class IfNode : NodeType {
var trueNodes = [NodeType]()
var falseNodes = [NodeType]()

let expression = try parseExpression(components: components, tokenParser: parser, token: token)
let expression = try parser.compileExpression(components: components, token: token)
falseNodes = try parser.parse(until(["endif", "else"]))

guard let token = parser.nextToken() else {
Expand Down
4 changes: 2 additions & 2 deletions Sources/Node.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ public class VariableNode : NodeType {
if hasToken("if", at: 1) {
let components = components.suffix(from: 2)
if let elseIndex = components.index(of: "else") {
condition = try parseExpression(components: Array(components.prefix(upTo: elseIndex)), tokenParser: parser, token: token)
condition = try parser.compileExpression(components: Array(components.prefix(upTo: elseIndex)), token: token)
let elseToken = components.suffix(from: elseIndex.advanced(by: 1)).joined(separator: " ")
elseExpression = try parser.compileResolvable(elseToken, containedIn: token)
} else {
condition = try parseExpression(components: Array(components), tokenParser: parser, token: token)
condition = try parser.compileExpression(components: Array(components), token: token)
elseExpression = nil
}
} else {
Expand Down
52 changes: 39 additions & 13 deletions Sources/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public class TokenParser {

if let tag = token.components.first {
do {
let parser = try findTag(name: tag)
let parser = try environment.findTag(name: tag)
let node = try parser(self, token)
nodes.append(node)
} catch {
Expand All @@ -76,8 +76,27 @@ public class TokenParser {
tokens.insert(token, at: 0)
}

/// Create filter expression from a string contained in provided token
public func compileFilter(_ filterToken: String, containedIn token: Token) throws -> Resolvable {
return try environment.compileFilter(filterToken, containedIn: token)
}

/// Create boolean expression from components contained in provided token
public func compileExpression(components: [String], token: Token) throws -> Expression {
return try environment.compileExpression(components: components, containedIn: token)
}

/// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token
public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable {
return try environment.compileResolvable(token, containedIn: containingToken)
}

}

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 @@ -87,7 +106,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 @@ -105,7 +124,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 @@ -118,9 +137,15 @@ public class TokenParser {
return filtersWithDistance.filter({ $0.distance == minDistance }).map({ $0.filterName })
}

/// Create filter expression from a string
public func compileFilter(_ token: String) throws -> Resolvable {
return try FilterExpression(token: token, environment: self)
}

/// Create filter expression from a string contained in provided token
public func compileFilter(_ filterToken: String, containedIn containingToken: Token) throws -> Resolvable {
do {
return try FilterExpression(token: filterToken, parser: self)
return try FilterExpression(token: filterToken, environment: self)
} catch {
guard var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil else {
throw error
Expand All @@ -137,22 +162,23 @@ public class TokenParser {
}
}

@available(*, deprecated, message: "Use compileFilter(_:containedIn:)")
public func compileFilter(_ token: String) throws -> Resolvable {
return try FilterExpression(token: token, parser: self)
}

@available(*, deprecated, message: "Use compileResolvable(_:containedIn:)")
/// Create resolvable (i.e. range variable or filter expression) from a string
public func compileResolvable(_ token: String) throws -> Resolvable {
return try RangeVariable(token, parser: self)
return try RangeVariable(token, environment: self)
?? compileFilter(token)
}

/// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token
public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable {
return try RangeVariable(token, parser: self, containedIn: containingToken)
return try RangeVariable(token, environment: self, containedIn: containingToken)
?? compileFilter(token, containedIn: containingToken)
}

/// Create boolean expression from components contained in provided token
public func compileExpression(components: [String], containedIn token: Token) throws -> Expression {
return try IfExpressionParser.parser(components: components, environment: self, token: token).parse()
}

}

// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
Expand Down
21 changes: 10 additions & 11 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.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 {
throw TemplateSyntaxError("Variable tags must include at least 1 argument")
}
Expand All @@ -20,7 +20,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 @@ -34,7 +34,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 @@ -144,25 +144,24 @@ public struct RangeVariable: Resolvable {
public let from: Resolvable
public let to: Resolvable

@available(*, deprecated, message: "Use init?(_:parser:containedIn:)")
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 init?(_ token: String, parser: TokenParser, containedIn containingToken: Token) throws {
public init?(_ token: String, environment: Environment, containedIn containingToken: Token) throws {
let components = token.components(separatedBy: "...")
guard components.count == 2 else {
return nil
}

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

public func resolve(_ context: Context) throws -> Any? {
Expand Down
3 changes: 1 addition & 2 deletions Tests/StencilTests/ExpressionSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import Spectre
class ExpressionsTests: XCTestCase {
func testExpressions() {
describe("Expression") {
let parser = TokenParser(tokens: [], environment: Environment())

func parseExpression(components: [String]) throws -> Expression {
let parser = try IfExpressionParser.parser(components: components, tokenParser: parser, token: .text(value: "", at: .unknown))
let parser = try IfExpressionParser.parser(components: components, environment: Environment(), token: .text(value: "", at: .unknown))
return try parser.parse()
}

Expand Down
Loading

0 comments on commit 6f3ca60

Please sign in to comment.