diff --git a/CHANGELOG.md b/CHANGELOG.md index 59cb9c1..981fb6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ - Added support for resolving superclass properties for not-NSObject subclasses - The `{% for %}` tag can now iterate over tuples, structures and classes via their stored properties. +- Added `split` filter +- Allow default string filters to be applied to arrays +- Similar filters are suggested when unknown filter is used +- Added `indent` filter +- Allow using new lines inside tags - Added support for iterating arrays of tuples ### Bug Fixes diff --git a/Package.swift b/Package.swift index e366cf9..abda948 100644 --- a/Package.swift +++ b/Package.swift @@ -1,11 +1,10 @@ +// swift-tools-version:3.1 import PackageDescription let package = Package( name: "Stencil", dependencies: [ - .Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 8), - - // https://github.com/apple/swift-package-manager/pull/597 - .Package(url: "https://github.com/kylef/Spectre.git", majorVersion: 0, minor: 7), + .Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 9), + .Package(url: "https://github.com/kylef/Spectre.git", majorVersion: 0, minor: 8), ] ) diff --git a/Package@swift-3.swift b/Package@swift-3.swift new file mode 100644 index 0000000..704b083 --- /dev/null +++ b/Package@swift-3.swift @@ -0,0 +1,10 @@ +// swift-tools-version:3.1 +import PackageDescription + +let package = Package( + name: "Stencil", + dependencies: [ + .Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 8), + .Package(url: "https://github.com/kylef/Spectre.git", majorVersion: 0, minor: 7), + ] +) diff --git a/README.md b/README.md index ded2ca1..668c7cd 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,12 @@ Resources to help you integrate Stencil into a Swift project: - [API Reference](http://stencil.fuller.li/en/latest/api.html) - [Custom Template Tags and Filters](http://stencil.fuller.li/en/latest/custom-template-tags-and-filters.html) +## Projects that use Stencil + +[Sourcery](https://github.com/krzysztofzablocki/Sourcery), +[SwiftGen](https://github.com/SwiftGen/SwiftGen), +[Kitura](https://github.com/IBM-Swift/Kitura) + ## License Stencil is licensed under the BSD license. See [LICENSE](LICENSE) for more diff --git a/Sources/Extension.swift b/Sources/Extension.swift index 9dfa879..33a9925 100644 --- a/Sources/Extension.swift +++ b/Sources/Extension.swift @@ -57,6 +57,8 @@ class DefaultExtension: Extension { registerFilter("uppercase", filter: uppercase) registerFilter("lowercase", filter: lowercase) registerFilter("join", filter: joinFilter) + registerFilter("split", filter: splitFilter) + registerFilter("indent", filter: indentFilter) } } diff --git a/Sources/Filters.swift b/Sources/Filters.swift index f84a534..fece6eb 100644 --- a/Sources/Filters.swift +++ b/Sources/Filters.swift @@ -1,13 +1,25 @@ func capitalise(_ value: Any?) -> Any? { - return stringify(value).capitalized + if let array = value as? [Any?] { + return array.map { stringify($0).capitalized } + } else { + return stringify(value).capitalized + } } func uppercase(_ value: Any?) -> Any? { - return stringify(value).uppercased() + if let array = value as? [Any?] { + return array.map { stringify($0).uppercased() } + } else { + return stringify(value).uppercased() + } } func lowercase(_ value: Any?) -> Any? { - return stringify(value).lowercased() + if let array = value as? [Any?] { + return array.map { stringify($0).lowercased() } + } else { + return stringify(value).lowercased() + } } func defaultFilter(value: Any?, arguments: [Any?]) -> Any? { @@ -40,3 +52,62 @@ func joinFilter(value: Any?, arguments: [Any?]) throws -> Any? { return value } + +func splitFilter(value: Any?, arguments: [Any?]) throws -> Any? { + guard arguments.count < 2 else { + throw TemplateSyntaxError("'split' filter takes a single argument") + } + + let separator = stringify(arguments.first ?? " ") + if let value = value as? String { + return value.components(separatedBy: separator) + } + + return value +} + +func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? { + guard arguments.count <= 3 else { + throw TemplateSyntaxError("'indent' filter can take at most 3 arguments") + } + + var indentWidth = 4 + if arguments.count > 0 { + guard let value = arguments[0] as? Int else { + throw TemplateSyntaxError("'indent' filter width argument must be an Integer (\(String(describing: arguments[0])))") + } + indentWidth = value + } + + var indentationChar = " " + if arguments.count > 1 { + guard let value = arguments[1] as? String else { + throw TemplateSyntaxError("'indent' filter indentation argument must be a String (\(String(describing: arguments[1]))") + } + indentationChar = value + } + + var indentFirst = false + if arguments.count > 2 { + guard let value = arguments[2] as? Bool else { + throw TemplateSyntaxError("'indent' filter indentFirst argument must be a Bool") + } + indentFirst = value + } + + let indentation = [String](repeating: indentationChar, count: indentWidth).joined(separator: "") + return indent(stringify(value), indentation: indentation, indentFirst: indentFirst) +} + + +func indent(_ content: String, indentation: String, indentFirst: Bool) -> String { + guard !indentation.isEmpty else { return content } + + var lines = content.components(separatedBy: .newlines) + let firstLine = (indentFirst ? indentation : "") + lines.removeFirst() + let result = lines.reduce([firstLine]) { (result, line) in + return result + [(line.isEmpty ? "" : "\(indentation)\(line)")] + } + return result.joined(separator: "\n") +} + diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 5bd590d..b221775 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -10,7 +10,12 @@ struct Lexer { guard string.characters.count > 4 else { return "" } let start = string.index(string.startIndex, offsetBy: 2) let end = string.index(string.endIndex, offsetBy: -2) - return String(string[start.. [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..