Skip to content

Commit

Permalink
Merge branch 'master' into map-compact
Browse files Browse the repository at this point in the history
  • Loading branch information
ilyapuchka committed Jan 29, 2018
2 parents 9273bac + fa68ba9 commit 94acc6e
Show file tree
Hide file tree
Showing 13 changed files with 269 additions and 43 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
- 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`, `map`, `compact` and `filter` filters
- Allow default string filters to be applied to arrays
- Similar filters are suggested when unknown filter is used
- Added `indent`, `split`, `map`, `compact` and `filter` filters

### Bug Fixes

Expand Down
7 changes: 3 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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),
]
)
10 changes: 10 additions & 0 deletions [email protected]
Original file line number Diff line number Diff line change
@@ -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),
]
)
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Sources/Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class DefaultExtension: Extension {
registerFilter("lowercase", filter: lowercase)
registerFilter("join", filter: joinFilter)
registerFilter("split", filter: splitFilter)
registerFilter("indent", filter: indentFilter)
registerFilter("map", filter: mapFilter)
registerFilter("compact", filter: compactFilter)
registerFilter("filter", filter: filterFilter)
Expand Down
61 changes: 58 additions & 3 deletions Sources/Filters.swift
Original file line number Diff line number Diff line change
@@ -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? {
Expand Down Expand Up @@ -54,6 +66,49 @@ func splitFilter(value: Any?, arguments: [Any?]) throws -> Any? {
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")
}

func mapFilter(value: Any?, arguments: [Any?], context: Context) throws -> Any? {
guard arguments.count >= 1 && arguments.count <= 2 else {
Expand Down
63 changes: 62 additions & 1 deletion Sources/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,68 @@ extension Environment {
}
}

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 = 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 })
}

}

// 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]
}

}
4 changes: 4 additions & 0 deletions Sources/Variable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ public struct Variable : Equatable, Resolvable {
if let number = Number(variable) {
return number
}
// Boolean literal
if let bool = Bool(variable) {
return bool
}

for bit in lookup() {
current = normalize(current)
Expand Down
24 changes: 12 additions & 12 deletions Tests/StencilTests/ExpressionSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,19 @@ func testExpressions() {

$0.describe("expression parsing") {
$0.it("can parse a variable expression") {
let expression = try parseExpression(components: ["value"], tokenParser: parser)
let expression = try parser.compileExpression(components: ["value"])
try expect(expression.evaluate(context: Context())).to.beFalse()
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue()
}

$0.it("can parse a not expression") {
let expression = try parseExpression(components: ["not", "value"], tokenParser: parser)
let expression = try parser.compileExpression(components: ["not", "value"])
try expect(expression.evaluate(context: Context())).to.beTrue()
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse()
}

$0.describe("and expression") {
let expression = try! parseExpression(components: ["lhs", "and", "rhs"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["lhs", "and", "rhs"])

$0.it("evaluates to false with lhs false") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse()
Expand All @@ -137,7 +137,7 @@ func testExpressions() {
}

$0.describe("or expression") {
let expression = try! parseExpression(components: ["lhs", "or", "rhs"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["lhs", "or", "rhs"])

$0.it("evaluates to true with lhs true") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue()
Expand All @@ -157,7 +157,7 @@ func testExpressions() {
}

$0.describe("equality expression") {
let expression = try! parseExpression(components: ["lhs", "==", "rhs"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["lhs", "==", "rhs"])

$0.it("evaluates to true with equal lhs/rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue()
Expand Down Expand Up @@ -193,7 +193,7 @@ func testExpressions() {
}

$0.describe("inequality expression") {
let expression = try! parseExpression(components: ["lhs", "!=", "rhs"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["lhs", "!=", "rhs"])

$0.it("evaluates to true with inequal lhs/rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue()
Expand All @@ -205,7 +205,7 @@ func testExpressions() {
}

$0.describe("more than expression") {
let expression = try! parseExpression(components: ["lhs", ">", "rhs"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["lhs", ">", "rhs"])

$0.it("evaluates to true with lhs > rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue()
Expand All @@ -217,7 +217,7 @@ func testExpressions() {
}

$0.describe("more than equal expression") {
let expression = try! parseExpression(components: ["lhs", ">=", "rhs"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["lhs", ">=", "rhs"])

$0.it("evaluates to true with lhs == rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
Expand All @@ -229,7 +229,7 @@ func testExpressions() {
}

$0.describe("less than expression") {
let expression = try! parseExpression(components: ["lhs", "<", "rhs"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["lhs", "<", "rhs"])

$0.it("evaluates to true with lhs < rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue()
Expand All @@ -241,7 +241,7 @@ func testExpressions() {
}

$0.describe("less than equal expression") {
let expression = try! parseExpression(components: ["lhs", "<=", "rhs"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["lhs", "<=", "rhs"])

$0.it("evaluates to true with lhs == rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
Expand All @@ -253,7 +253,7 @@ func testExpressions() {
}

$0.describe("multiple expression") {
let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["one", "or", "two", "and", "not", "three"])

$0.it("evaluates to true with one") {
try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue()
Expand Down Expand Up @@ -281,7 +281,7 @@ func testExpressions() {
}

$0.describe("in expression") {
let expression = try! parseExpression(components: ["lhs", "in", "rhs"], tokenParser: parser)
let expression = try! parser.compileExpression(components: ["lhs", "in", "rhs"])

$0.it("evaluates to true when rhs contains lhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue()
Expand Down
Loading

0 comments on commit 94acc6e

Please sign in to comment.