From 359d086c0259480a05c1d5754a480539b345a029 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 22 Jan 2018 19:27:42 +0100 Subject: [PATCH] feat(filters): Show similar filter names when missing filter(#186) --- CHANGELOG.md | 1 + Sources/Parser.swift | 63 ++++++++++++++++++++++++++++- Tests/StencilTests/FilterSpec.swift | 32 +++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47ccabb..fbc1c1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ their stored properties. - Added `split` filter - Allow default string filters to be applied to arrays +- Similar filters are suggested when unknown filter is used ### Bug Fixes diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 1a59edb..5d9a1ec 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -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 { @@ -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..