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 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
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?) {
ilyapuchka marked this conversation as resolved.
Show resolved Hide resolved
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 {
ilyapuchka marked this conversation as resolved.
Show resolved Hide resolved
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