Merge pull request #203 from stencilproject/dynamic-filter
Added filter to apply dynamic filters
This commit is contained in:
@@ -8,7 +8,10 @@ _None_
|
|||||||
|
|
||||||
### Enhancements
|
### 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
|
### Deprecations
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
protocol Expression: CustomStringConvertible {
|
public protocol Expression: CustomStringConvertible {
|
||||||
func evaluate(context: Context) throws -> Bool
|
func evaluate(context: Context) throws -> Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ open class Extension {
|
|||||||
|
|
||||||
/// Registers a template filter with the given name
|
/// Registers a template filter with the given name
|
||||||
public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?]) throws -> Any?) {
|
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)
|
filters[name] = .arguments(filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,28 +73,28 @@ class DefaultExtension: Extension {
|
|||||||
registerFilter("join", filter: joinFilter)
|
registerFilter("join", filter: joinFilter)
|
||||||
registerFilter("split", filter: splitFilter)
|
registerFilter("split", filter: splitFilter)
|
||||||
registerFilter("indent", filter: indentFilter)
|
registerFilter("indent", filter: indentFilter)
|
||||||
|
registerFilter("filter", filter: filterFilter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protocol FilterType {
|
protocol FilterType {
|
||||||
func invoke(value: Any?, arguments: [Any?]) throws -> Any?
|
func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any?
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Filter: FilterType {
|
enum Filter: FilterType {
|
||||||
case simple(((Any?) throws -> Any?))
|
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 {
|
switch self {
|
||||||
case let .simple(filter):
|
case let .simple(filter):
|
||||||
if !arguments.isEmpty {
|
if !arguments.isEmpty {
|
||||||
throw TemplateSyntaxError("cannot invoke filter with an argument")
|
throw TemplateSyntaxError("cannot invoke filter with an argument")
|
||||||
}
|
}
|
||||||
|
|
||||||
return try filter(value)
|
return try filter(value)
|
||||||
case let .arguments(filter):
|
case let .arguments(filter):
|
||||||
return try filter(value, arguments)
|
return try filter(value, arguments, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func defaultFilter(value: Any?, arguments: [Any?]) -> Any? {
|
|||||||
|
|
||||||
func joinFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
func joinFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||||
guard arguments.count < 2 else {
|
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 ?? "")
|
let separator = stringify(arguments.first ?? "")
|
||||||
@@ -55,7 +55,7 @@ func joinFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
|||||||
|
|
||||||
func splitFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
func splitFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||||
guard arguments.count < 2 else {
|
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 ?? " ")
|
let separator = stringify(arguments.first ?? " ")
|
||||||
@@ -115,3 +115,16 @@ func indent(_ content: String, indentation: String, indentFirst: Bool) -> String
|
|||||||
return result.joined(separator: "\n")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class ForNode : NodeType {
|
|||||||
let resolvable = try parser.compileResolvable(components[3], containedIn: token)
|
let resolvable = try parser.compileResolvable(components[3], containedIn: token)
|
||||||
|
|
||||||
let `where` = hasToken("where", at: 4)
|
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
|
: nil
|
||||||
|
|
||||||
let forNodes = try parser.parse(until(["endfor", "empty"]))
|
let forNodes = try parser.parse(until(["endfor", "empty"]))
|
||||||
|
|||||||
@@ -111,11 +111,11 @@ final class IfExpressionParser {
|
|||||||
self.tokens = tokens
|
self.tokens = tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
static func parser(components: [String], tokenParser: TokenParser, token: Token) throws -> IfExpressionParser {
|
static func parser(components: [String], environment: Environment, token: Token) throws -> IfExpressionParser {
|
||||||
return try IfExpressionParser(components: ArraySlice(components), tokenParser: tokenParser, token: token)
|
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 parsedComponents = Set<Int>()
|
||||||
var bracketsBalance = 0
|
var bracketsBalance = 0
|
||||||
self.tokens = try zip(components.indices, components).compactMap { (index, component) in
|
self.tokens = try zip(components.indices, components).compactMap { (index, component) in
|
||||||
@@ -125,7 +125,7 @@ final class IfExpressionParser {
|
|||||||
bracketsBalance += 1
|
bracketsBalance += 1
|
||||||
let (expression, parsedCount) = try IfExpressionParser.subExpression(
|
let (expression, parsedCount) = try IfExpressionParser.subExpression(
|
||||||
from: components.suffix(from: index + 1),
|
from: components.suffix(from: index + 1),
|
||||||
tokenParser: tokenParser,
|
environment: environment,
|
||||||
token: token
|
token: token
|
||||||
)
|
)
|
||||||
parsedComponents.formUnion(Set(index...(index + parsedCount)))
|
parsedComponents.formUnion(Set(index...(index + parsedCount)))
|
||||||
@@ -147,12 +147,12 @@ final class IfExpressionParser {
|
|||||||
return .prefix(name: name, bindingPower: bindingPower, operatorType: operatorType)
|
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
|
var bracketsBalance = 1
|
||||||
let subComponents = components
|
let subComponents = components
|
||||||
.prefix(while: {
|
.prefix(while: {
|
||||||
@@ -167,7 +167,7 @@ final class IfExpressionParser {
|
|||||||
throw TemplateSyntaxError("'if' expression error: missing closing bracket")
|
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()
|
let expression = try expressionParser.parse()
|
||||||
return (expression, subComponents.count)
|
return (expression, subComponents.count)
|
||||||
}
|
}
|
||||||
@@ -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
|
/// Represents an if condition and the associated nodes when the condition
|
||||||
/// evaluates
|
/// evaluates
|
||||||
@@ -243,7 +239,7 @@ class IfNode : NodeType {
|
|||||||
var components = token.components
|
var components = token.components
|
||||||
components.removeFirst()
|
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"]))
|
let nodes = try parser.parse(until(["endif", "elif", "else"]))
|
||||||
var conditions: [IfCondition] = [
|
var conditions: [IfCondition] = [
|
||||||
IfCondition(expression: expression, nodes: nodes)
|
IfCondition(expression: expression, nodes: nodes)
|
||||||
@@ -253,7 +249,7 @@ class IfNode : NodeType {
|
|||||||
while let current = nextToken, current.contents.hasPrefix("elif") {
|
while let current = nextToken, current.contents.hasPrefix("elif") {
|
||||||
var components = current.components
|
var components = current.components
|
||||||
components.removeFirst()
|
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"]))
|
let nodes = try parser.parse(until(["endif", "elif", "else"]))
|
||||||
nextToken = parser.nextToken()
|
nextToken = parser.nextToken()
|
||||||
@@ -281,7 +277,7 @@ class IfNode : NodeType {
|
|||||||
var trueNodes = [NodeType]()
|
var trueNodes = [NodeType]()
|
||||||
var falseNodes = [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"]))
|
falseNodes = try parser.parse(until(["endif", "else"]))
|
||||||
|
|
||||||
guard let token = parser.nextToken() else {
|
guard let token = parser.nextToken() else {
|
||||||
|
|||||||
@@ -74,11 +74,11 @@ public class VariableNode : NodeType {
|
|||||||
if hasToken("if", at: 1) {
|
if hasToken("if", at: 1) {
|
||||||
let components = components.suffix(from: 2)
|
let components = components.suffix(from: 2)
|
||||||
if let elseIndex = components.index(of: "else") {
|
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: " ")
|
let elseToken = components.suffix(from: elseIndex.advanced(by: 1)).joined(separator: " ")
|
||||||
elseExpression = try parser.compileResolvable(elseToken, containedIn: token)
|
elseExpression = try parser.compileResolvable(elseToken, containedIn: token)
|
||||||
} else {
|
} else {
|
||||||
condition = try parseExpression(components: Array(components), tokenParser: parser, token: token)
|
condition = try parser.compileExpression(components: Array(components), token: token)
|
||||||
elseExpression = nil
|
elseExpression = nil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public class TokenParser {
|
|||||||
|
|
||||||
if let tag = token.components.first {
|
if let tag = token.components.first {
|
||||||
do {
|
do {
|
||||||
let parser = try findTag(name: tag)
|
let parser = try environment.findTag(name: tag)
|
||||||
let node = try parser(self, token)
|
let node = try parser(self, token)
|
||||||
nodes.append(node)
|
nodes.append(node)
|
||||||
} catch {
|
} catch {
|
||||||
@@ -76,8 +76,27 @@ public class TokenParser {
|
|||||||
tokens.insert(token, at: 0)
|
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 {
|
func findTag(name: String) throws -> Extension.TagParser {
|
||||||
for ext in environment.extensions {
|
for ext in extensions {
|
||||||
if let filter = ext.tags[name] {
|
if let filter = ext.tags[name] {
|
||||||
return filter
|
return filter
|
||||||
}
|
}
|
||||||
@@ -87,7 +106,7 @@ public class TokenParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func findFilter(_ name: String) throws -> FilterType {
|
func findFilter(_ name: String) throws -> FilterType {
|
||||||
for ext in environment.extensions {
|
for ext in extensions {
|
||||||
if let filter = ext.filters[name] {
|
if let filter = ext.filters[name] {
|
||||||
return filter
|
return filter
|
||||||
}
|
}
|
||||||
@@ -105,7 +124,7 @@ public class TokenParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func suggestedFilters(for name: String) -> [String] {
|
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
|
let filtersWithDistance = allFilters
|
||||||
.map({ (filterName: $0, distance: $0.levenshteinDistance(name)) })
|
.map({ (filterName: $0, distance: $0.levenshteinDistance(name)) })
|
||||||
@@ -118,9 +137,15 @@ public class TokenParser {
|
|||||||
return filtersWithDistance.filter({ $0.distance == minDistance }).map({ $0.filterName })
|
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 {
|
public func compileFilter(_ filterToken: String, containedIn containingToken: Token) throws -> Resolvable {
|
||||||
do {
|
do {
|
||||||
return try FilterExpression(token: filterToken, parser: self)
|
return try FilterExpression(token: filterToken, environment: self)
|
||||||
} catch {
|
} catch {
|
||||||
guard var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil else {
|
guard var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil else {
|
||||||
throw error
|
throw error
|
||||||
@@ -137,22 +162,23 @@ public class TokenParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(*, deprecated, message: "Use compileFilter(_:containedIn:)")
|
/// Create resolvable (i.e. range variable or filter expression) from a string
|
||||||
public func compileFilter(_ token: String) throws -> Resolvable {
|
|
||||||
return try FilterExpression(token: token, parser: self)
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(*, deprecated, message: "Use compileResolvable(_:containedIn:)")
|
|
||||||
public func compileResolvable(_ token: String) throws -> Resolvable {
|
public func compileResolvable(_ token: String) throws -> Resolvable {
|
||||||
return try RangeVariable(token, parser: self)
|
return try RangeVariable(token, environment: self)
|
||||||
?? compileFilter(token)
|
?? 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 {
|
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)
|
?? 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
|
// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ class FilterExpression : Resolvable {
|
|||||||
let filters: [(FilterType, [Variable])]
|
let filters: [(FilterType, [Variable])]
|
||||||
let variable: Variable
|
let variable: Variable
|
||||||
|
|
||||||
init(token: String, parser: TokenParser) throws {
|
init(token: String, environment: Environment) throws {
|
||||||
let bits = token.split(separator: "|").map({ String($0).trim(character: " ") })
|
let bits = token.smartSplit(separator: "|").map({ String($0).trim(character: " ") })
|
||||||
if bits.isEmpty {
|
if bits.isEmpty {
|
||||||
throw TemplateSyntaxError("Variable tags must include at least 1 argument")
|
throw TemplateSyntaxError("Variable tags must include at least 1 argument")
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ class FilterExpression : Resolvable {
|
|||||||
do {
|
do {
|
||||||
filters = try filterBits.map {
|
filters = try filterBits.map {
|
||||||
let (name, arguments) = parseFilterComponents(token: $0)
|
let (name, arguments) = parseFilterComponents(token: $0)
|
||||||
let filter = try parser.findFilter(name)
|
let filter = try environment.findFilter(name)
|
||||||
return (filter, arguments)
|
return (filter, arguments)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -34,7 +34,7 @@ class FilterExpression : Resolvable {
|
|||||||
|
|
||||||
return try filters.reduce(result) { x, y in
|
return try filters.reduce(result) { x, y in
|
||||||
let arguments = try y.1.map { try $0.resolve(context) }
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,25 +144,24 @@ public struct RangeVariable: Resolvable {
|
|||||||
public let from: Resolvable
|
public let from: Resolvable
|
||||||
public let to: Resolvable
|
public let to: Resolvable
|
||||||
|
|
||||||
@available(*, deprecated, message: "Use init?(_:parser:containedIn:)")
|
public init?(_ token: String, environment: Environment) throws {
|
||||||
public init?(_ token: String, parser: TokenParser) throws {
|
|
||||||
let components = token.components(separatedBy: "...")
|
let components = token.components(separatedBy: "...")
|
||||||
guard components.count == 2 else {
|
guard components.count == 2 else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
self.from = try parser.compileFilter(components[0])
|
self.from = try environment.compileFilter(components[0])
|
||||||
self.to = try parser.compileFilter(components[1])
|
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: "...")
|
let components = token.components(separatedBy: "...")
|
||||||
guard components.count == 2 else {
|
guard components.count == 2 else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
self.from = try parser.compileFilter(components[0], containedIn: containingToken)
|
self.from = try environment.compileFilter(components[0], containedIn: containingToken)
|
||||||
self.to = try parser.compileFilter(components[1], containedIn: containingToken)
|
self.to = try environment.compileFilter(components[1], containedIn: containingToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func resolve(_ context: Context) throws -> Any? {
|
public func resolve(_ context: Context) throws -> Any? {
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ import Spectre
|
|||||||
class ExpressionsTests: XCTestCase {
|
class ExpressionsTests: XCTestCase {
|
||||||
func testExpressions() {
|
func testExpressions() {
|
||||||
describe("Expression") {
|
describe("Expression") {
|
||||||
let parser = TokenParser(tokens: [], environment: Environment())
|
|
||||||
|
|
||||||
func parseExpression(components: [String]) throws -> Expression {
|
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()
|
return try parser.parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -386,5 +386,28 @@ class FilterTests: XCTestCase {
|
|||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe("dynamic filter") {
|
||||||
|
|
||||||
|
$0.it("can apply dynamic filter") {
|
||||||
|
let template = Template(templateString: "{{ name|filter:somefilter }}")
|
||||||
|
let result = try template.render(Context(dictionary: ["name": "Jhon", "somefilter": "uppercase"]))
|
||||||
|
try expect(result) == "JHON"
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can apply dynamic filter on array") {
|
||||||
|
let template = Template(templateString: "{{ values|filter:joinfilter }}")
|
||||||
|
let result = try template.render(Context(dictionary: ["values": [1, 2, 3], "joinfilter": "join:\", \""]))
|
||||||
|
try expect(result) == "1, 2, 3"
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("throws on unknown dynamic filter") {
|
||||||
|
let template = Template(templateString: "{{ values|filter:unknown }}")
|
||||||
|
let context = Context(dictionary: ["values": [1, 2, 3], "unknown": "absurd"])
|
||||||
|
try expect(try template.render(context)).toThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,8 @@ class ForNodeTests: XCTestCase {
|
|||||||
|
|
||||||
$0.it("renders the given nodes while filtering items using where expression") {
|
$0.it("renders the given nodes while filtering items using where expression") {
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
|
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
|
||||||
let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown))
|
let parser = TokenParser(tokens: [], environment: Environment())
|
||||||
|
let `where` = try parser.compileExpression(components: ["item", ">", "1"], token: .text(value: "", at: .unknown))
|
||||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`)
|
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`)
|
||||||
try expect(try node.render(context)) == "2132"
|
try expect(try node.render(context)) == "2132"
|
||||||
}
|
}
|
||||||
@@ -107,7 +108,8 @@ class ForNodeTests: XCTestCase {
|
|||||||
$0.it("renders the given empty nodes when all items filtered out with where expression") {
|
$0.it("renders the given empty nodes when all items filtered out with where expression") {
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||||
let `where` = try parseExpression(components: ["item", "==", "0"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown))
|
let parser = TokenParser(tokens: [], environment: Environment())
|
||||||
|
let `where` = try parser.compileExpression(components: ["item", "==", "0"], token: .text(value: "", at: .unknown))
|
||||||
let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`)
|
let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`)
|
||||||
try expect(try node.render(context)) == "empty"
|
try expect(try node.render(context)) == "empty"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -351,8 +351,7 @@ class VariableTests: XCTestCase {
|
|||||||
|
|
||||||
func makeVariable(_ token: String) throws -> RangeVariable? {
|
func makeVariable(_ token: String) throws -> RangeVariable? {
|
||||||
let token = Token.variable(value: token, at: .unknown)
|
let token = Token.variable(value: token, at: .unknown)
|
||||||
let parser = TokenParser(tokens: [token], environment: context.environment)
|
return try RangeVariable(token.contents, environment: context.environment, containedIn: token)
|
||||||
return try RangeVariable(token.contents, parser: parser, containedIn: token)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve closed range as array") {
|
$0.it("can resolve closed range as array") {
|
||||||
|
|||||||
@@ -386,3 +386,13 @@ Filter accepts several arguments:
|
|||||||
* indentation character: character to be used for indentation. Default is a space.
|
* indentation character: character to be used for indentation. Default is a space.
|
||||||
* indent first line: whether first line of output should be indented or not. Default is ``false``.
|
* indent first line: whether first line of output should be indented or not. Default is ``false``.
|
||||||
|
|
||||||
|
``filter``
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
Applies the filter with the name provided as an argument to the current expression.
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
{{ string|filter:myfilter }}
|
||||||
|
|
||||||
|
This expression will resolve the `myfilter` variable, find a filter named the same as resolved value, and will apply it to the `string` variable. I.e. if `myfilter` variable resolves to string `uppercase` this expression will apply file `uppercase` to `string` variable.
|
||||||
|
|||||||
Reference in New Issue
Block a user