feat(filters): Show similar filter names when missing filter(#186)
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
their stored properties.
|
their stored properties.
|
||||||
- Added `split` filter
|
- Added `split` filter
|
||||||
- Allow default string filters to be applied to arrays
|
- Allow default string filters to be applied to arrays
|
||||||
|
- Similar filters are suggested when unknown filter is used
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,26 @@ public class TokenParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw TemplateSyntaxError("Unknown filter '\(name)'")
|
let suggestedFilters = self.suggestedFilters(for: name)
|
||||||
|
if suggestedFilters.isEmpty {
|
||||||
|
throw TemplateSyntaxError("Unknown filter '\(name)'.")
|
||||||
|
} else {
|
||||||
|
throw TemplateSyntaxError("Unknown filter '\(name)'. Found similar filters: \(suggestedFilters.map({ "'\($0)'" }).joined(separator: ", "))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func suggestedFilters(for name: String) -> [String] {
|
||||||
|
let allFilters = environment.extensions.flatMap({ $0.filters.keys })
|
||||||
|
|
||||||
|
let filtersWithDistance = allFilters
|
||||||
|
.map({ (filterName: $0, distance: $0.levenshteinDistance(name)) })
|
||||||
|
// do not suggest filters which names are shorter than the distance
|
||||||
|
.filter({ $0.filterName.characters.count > $0.distance })
|
||||||
|
guard let minDistance = filtersWithDistance.min(by: { $0.distance < $1.distance })?.distance else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
// suggest all filters with the same distance
|
||||||
|
return filtersWithDistance.filter({ $0.distance == minDistance }).map({ $0.filterName })
|
||||||
}
|
}
|
||||||
|
|
||||||
public func compileFilter(_ token: String) throws -> Resolvable {
|
public func compileFilter(_ token: String) throws -> Resolvable {
|
||||||
@@ -96,3 +115,45 @@ public class TokenParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
|
||||||
|
extension String {
|
||||||
|
|
||||||
|
subscript(_ i: Int) -> Character {
|
||||||
|
return self[self.index(self.startIndex, offsetBy: i)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func levenshteinDistance(_ target: String) -> Int {
|
||||||
|
// create two work vectors of integer distances
|
||||||
|
var last, current: [Int]
|
||||||
|
|
||||||
|
// initialize v0 (the previous row of distances)
|
||||||
|
// this row is A[0][i]: edit distance for an empty s
|
||||||
|
// the distance is just the number of characters to delete from t
|
||||||
|
last = [Int](0...target.characters.count)
|
||||||
|
current = [Int](repeating: 0, count: target.characters.count + 1)
|
||||||
|
|
||||||
|
for i in 0..<self.characters.count {
|
||||||
|
// calculate v1 (current row distances) from the previous row v0
|
||||||
|
|
||||||
|
// first element of v1 is A[i+1][0]
|
||||||
|
// edit distance is delete (i+1) chars from s to match empty t
|
||||||
|
current[0] = i + 1
|
||||||
|
|
||||||
|
// use formula to fill in the rest of the row
|
||||||
|
for j in 0..<target.characters.count {
|
||||||
|
current[j+1] = Swift.min(
|
||||||
|
last[j+1] + 1,
|
||||||
|
current[j] + 1,
|
||||||
|
last[j] + (self[i] == target[j] ? 0 : 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy v1 (current row) to v0 (previous row) for next iteration
|
||||||
|
last = current
|
||||||
|
}
|
||||||
|
|
||||||
|
return current[target.characters.count]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -212,4 +212,36 @@ func testFilter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
describe("filter suggestion") {
|
||||||
|
|
||||||
|
$0.it("made for unknown filter") {
|
||||||
|
let template = Template(templateString: "{{ value|unknownFilter }}")
|
||||||
|
let expectedError = TemplateSyntaxError("Unknown filter 'unknownFilter'. Found similar filters: 'knownFilter'")
|
||||||
|
|
||||||
|
let filterExtension = Extension()
|
||||||
|
filterExtension.registerFilter("knownFilter") { value, _ in value }
|
||||||
|
|
||||||
|
try expect(template.render(Context(dictionary: [:], environment: Environment(extensions: [filterExtension])))).toThrow(expectedError)
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("made for multiple similar filters") {
|
||||||
|
let template = Template(templateString: "{{ value|lowerFirst }}")
|
||||||
|
let expectedError = TemplateSyntaxError("Unknown filter 'lowerFirst'. Found similar filters: 'lowerFirstWord', 'lowercase'")
|
||||||
|
|
||||||
|
let filterExtension = Extension()
|
||||||
|
filterExtension.registerFilter("lowerFirstWord") { value, _ in value }
|
||||||
|
filterExtension.registerFilter("lowerFirstLetter") { value, _ in value }
|
||||||
|
|
||||||
|
try expect(template.render(Context(dictionary: [:], environment: Environment(extensions: [filterExtension])))).toThrow(expectedError)
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("not made when can't find similar filter") {
|
||||||
|
let template = Template(templateString: "{{ value|unknownFilter }}")
|
||||||
|
let expectedError = TemplateSyntaxError("Unknown filter 'unknownFilter'.")
|
||||||
|
try expect(template.render(Context(dictionary: [:]))).toThrow(expectedError)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user