From c2eff20d9fa8cf96ef4f886150526309c021f4e5 Mon Sep 17 00:00:00 2001 From: David Jennes Date: Fri, 26 May 2017 20:58:43 +0200 Subject: [PATCH 1/5] Add removeNewlines filter --- Sources/Environment.swift | 9 ++++++--- Sources/Filters+Strings.swift | 10 ++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Sources/Environment.swift b/Sources/Environment.swift index 6fcbe726..5348e005 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -12,15 +12,18 @@ public extension Extension { registerTag("macro", parser: MacroNode.parse) registerTag("call", parser: CallNode.parse) registerTag("map", parser: MapNode.parse) - registerFilter("swiftIdentifier", filter: Filters.Strings.swiftIdentifier) + + registerFilter("camelToSnakeCase", filter: Filters.Strings.camelToSnakeCase) + registerFilter("escapeReservedKeywords", filter: Filters.Strings.escapeReservedKeywords) registerFilter("lowerFirstWord", filter: Filters.Strings.lowerFirstWord) + registerFilter("removeNewlines", filter: Filters.Strings.removeNewlines) registerFilter("snakeToCamelCase", filter: Filters.Strings.snakeToCamelCase) - registerFilter("camelToSnakeCase", filter: Filters.Strings.camelToSnakeCase) + registerFilter("swiftIdentifier", filter: Filters.Strings.swiftIdentifier) registerFilter("titlecase", filter: Filters.Strings.titlecase) + registerFilter("hexToInt", filter: Filters.Numbers.hexToInt) registerFilter("int255toFloat", filter: Filters.Numbers.int255toFloat) registerFilter("percent", filter: Filters.Numbers.percent) - registerFilter("escapeReservedKeywords", filter: Filters.Strings.escapeReservedKeywords) } } diff --git a/Sources/Filters+Strings.swift b/Sources/Filters+Strings.swift index 7cf5a189..558056f0 100644 --- a/Sources/Filters+Strings.swift +++ b/Sources/Filters+Strings.swift @@ -119,6 +119,16 @@ extension Filters { return escapeReservedKeywords(in: string) } + static func removeNewlines(_ value: Any?, arguments: [Any?]) throws -> Any? { + let removeSpaces = try Filters.parseBool(from: arguments, index: 0, required: false) ?? true + guard let string = value as? String else { throw Filters.Error.invalidInputType } + + let set: CharacterSet = removeSpaces ? .whitespacesAndNewlines : .newlines + let result = string.components(separatedBy: set).joined() + + return result + } + // MARK: - Private methods /// This returns the string with its first parameter uppercased. From e44467c7949dcf0ab7518a56ceaa3edfd92582bc Mon Sep 17 00:00:00 2001 From: David Jennes Date: Fri, 26 May 2017 20:58:48 +0200 Subject: [PATCH 2/5] Add tests --- .../StringFiltersTests.swift | 232 +++++++++++------- 1 file changed, 138 insertions(+), 94 deletions(-) diff --git a/Tests/StencilSwiftKitTests/StringFiltersTests.swift b/Tests/StencilSwiftKitTests/StringFiltersTests.swift index 61d257bc..d5811330 100644 --- a/Tests/StencilSwiftKitTests/StringFiltersTests.swift +++ b/Tests/StencilSwiftKitTests/StringFiltersTests.swift @@ -7,8 +7,99 @@ import XCTest @testable import StencilSwiftKit -class StringFiltersTests: XCTestCase { +final class StringFiltersTests: XCTestCase { + func testCamelToSnakeCase_WithNoArgsDefaultsToTrue() throws { + let result = try Filters.Strings.camelToSnakeCase("StringWithWords", arguments: []) as? String + XCTAssertEqual(result, "string_with_words") + } + + func testCamelToSnakeCase_WithTrue() throws { + let expectations = [ + "string": "string", + "String": "string", + "strIng": "str_ing", + "strING": "str_ing", + "X": "x", + "x": "x", + "SomeCapString": "some_cap_string", + "someCapString": "some_cap_string", + "string_with_words": "string_with_words", + "String_with_words": "string_with_words", + "String_With_Words": "string_with_words", + "String_With_WoRds": "string_with_wo_rds", + "STRing_with_words": "st_ring_with_words", + "string_wiTH_WOrds": "string_wi_th_w_ords", + "": "", + "URLChooser": "url_chooser", + "UrlChooser": "url_chooser", + "a__b__c": "a__b__c", + "__y_z!": "__y_z!", + "PLEASESTOPSCREAMING": "pleasestopscreaming", + "PLEASESTOPSCREAMING!": "pleasestopscreaming!", + "PLEASE_STOP_SCREAMING": "please_stop_screaming", + "PLEASE_STOP_SCREAMING!": "please_stop_screaming!" + ] + + for (input, expected) in expectations { + let trueArgResult = try Filters.Strings.camelToSnakeCase(input, arguments: ["true"]) as? String + XCTAssertEqual(trueArgResult, expected) + } + } + + func testCamelToSnakeCase_WithFalse() throws { + let expectations = [ + "string": "string", + "String": "String", + "strIng": "str_Ing", + "strING": "str_ING", + "X": "X", + "x": "x", + "SomeCapString": "Some_Cap_String", + "someCapString": "some_Cap_String", + "string_with_words": "string_with_words", + "String_with_words": "String_with_words", + "String_With_Words": "String_With_Words", + "String_With_WoRds": "String_With_Wo_Rds", + "STRing_with_words": "ST_Ring_with_words", + "string_wiTH_WOrds": "string_wi_TH_W_Ords", + "": "", + "URLChooser": "URL_Chooser", + "UrlChooser": "Url_Chooser", + "a__b__c": "a__b__c", + "__y_z!": "__y_z!", + "PLEASESTOPSCREAMING": "PLEASESTOPSCREAMING", + "PLEASESTOPSCREAMING!": "PLEASESTOPSCREAMING!", + "PLEASE_STOP_SCREAMING": "PLEASE_STOP_SCREAMING", + "PLEASE_STOP_SCREAMING!": "PLEASE_STOP_SCREAMING!" + ] + + for (input, expected) in expectations { + let falseArgResult = try Filters.Strings.camelToSnakeCase(input, arguments: ["false"]) as? String + XCTAssertEqual(falseArgResult, expected) + } + } +} + +extension StringFiltersTests { + func testEscapeReservedKeywords() throws { + let expectations = [ + "self": "`self`", + "foo": "foo", + "Type": "`Type`", + "": "", + "x": "x", + "Bar": "Bar", + "#imageLiteral": "`#imageLiteral`" + ] + for (input, expected) in expectations { + let result = try Filters.Strings.escapeReservedKeywords(value: input) as? String + XCTAssertEqual(result, expected) + } + } +} + +extension StringFiltersTests { func testLowerFirstWord() throws { let expectations = [ "string": "string", @@ -39,38 +130,46 @@ class StringFiltersTests: XCTestCase { XCTAssertEqual(result, expected) } } +} - func testTitlecase() throws { +extension StringFiltersTests { + func testRemoveNewlines_WithNoArgsDefaultsToTrue() throws { + let result = try Filters.Strings.removeNewlines("test\n \ntest ", arguments: []) as? String + XCTAssertEqual(result, "testtest") + } + + func testRemoveNewlines_WithTrue() throws { let expectations = [ - "string": "String", - "String": "String", - "strIng": "StrIng", - "strING": "StrING", - "X": "X", - "x": "X", - "SomeCapString": "SomeCapString", - "someCapString": "SomeCapString", - "string_with_words": "String_with_words", - "String_with_words": "String_with_words", - "String_With_Words": "String_With_Words", - "STRing_with_words": "STRing_with_words", - "string_wiTH_WOrds": "String_wiTH_WOrds", - "": "", - "URLChooser": "URLChooser", - "a__b__c": "A__b__c", - "__y_z!": "__y_z!", - "PLEASESTOPSCREAMING": "PLEASESTOPSCREAMING", - "PLEASESTOPSCREAMING!": "PLEASESTOPSCREAMING!", - "PLEASE_STOP_SCREAMING": "PLEASE_STOP_SCREAMING", - "PLEASE_STOP_SCREAMING!": "PLEASE_STOP_SCREAMING!" + "test": "test", + " \n test": "test", + "test \n ": "test", + "test\n \ntest": "testtest", + "\n test\n \ntest \n ": "testtest" ] for (input, expected) in expectations { - let result = try Filters.Strings.titlecase(input) as? String + let result = try Filters.Strings.removeNewlines(input, arguments: ["true"]) as? String + XCTAssertEqual(result, expected) + } + } + + func testRemoveNewlines_WithFalse() throws { + let expectations = [ + "test": "test", + " \n test": " test", + "test \n ": "test ", + "test\n \ntest": "test test", + "\n test\n \ntest \n ": " test test " + ] + + for (input, expected) in expectations { + let result = try Filters.Strings.removeNewlines(input, arguments: ["false"]) as? String XCTAssertEqual(result, expected) } } +} +extension StringFiltersTests { func testSnakeToCamelCase_WithNoArgsDefaultsToFalse() throws { let result = try Filters.Strings.snakeToCamelCase("__y_z!", arguments: []) as? String XCTAssertEqual(result, "__YZ!") @@ -137,65 +236,27 @@ class StringFiltersTests: XCTestCase { XCTAssertEqual(result, expected) } } +} - func testCamelToSnakeCase_WithNoArgsDefaultsToTrue() throws { - let result = try Filters.Strings.camelToSnakeCase("StringWithWords", arguments: []) as? String - XCTAssertEqual(result, "string_with_words") - } - - func testCamelToSnakeCase_WithTrue() throws { - let expectations = [ - "string": "string", - "String": "string", - "strIng": "str_ing", - "strING": "str_ing", - "X": "x", - "x": "x", - "SomeCapString": "some_cap_string", - "someCapString": "some_cap_string", - "string_with_words": "string_with_words", - "String_with_words": "string_with_words", - "String_With_Words": "string_with_words", - "String_With_WoRds": "string_with_wo_rds", - "STRing_with_words": "st_ring_with_words", - "string_wiTH_WOrds": "string_wi_th_w_ords", - "": "", - "URLChooser": "url_chooser", - "UrlChooser": "url_chooser", - "a__b__c": "a__b__c", - "__y_z!": "__y_z!", - "PLEASESTOPSCREAMING": "pleasestopscreaming", - "PLEASESTOPSCREAMING!": "pleasestopscreaming!", - "PLEASE_STOP_SCREAMING": "please_stop_screaming", - "PLEASE_STOP_SCREAMING!": "please_stop_screaming!" - ] - - for (input, expected) in expectations { - let trueArgResult = try Filters.Strings.camelToSnakeCase(input, arguments: ["true"]) as? String - XCTAssertEqual(trueArgResult, expected) - } - } - - func testCamelToSnakeCase_WithFalse() throws { +extension StringFiltersTests { + func testTitlecase() throws { let expectations = [ - "string": "string", + "string": "String", "String": "String", - "strIng": "str_Ing", - "strING": "str_ING", + "strIng": "StrIng", + "strING": "StrING", "X": "X", - "x": "x", - "SomeCapString": "Some_Cap_String", - "someCapString": "some_Cap_String", - "string_with_words": "string_with_words", + "x": "X", + "SomeCapString": "SomeCapString", + "someCapString": "SomeCapString", + "string_with_words": "String_with_words", "String_with_words": "String_with_words", "String_With_Words": "String_With_Words", - "String_With_WoRds": "String_With_Wo_Rds", - "STRing_with_words": "ST_Ring_with_words", - "string_wiTH_WOrds": "string_wi_TH_W_Ords", + "STRing_with_words": "STRing_with_words", + "string_wiTH_WOrds": "String_wiTH_WOrds", "": "", - "URLChooser": "URL_Chooser", - "UrlChooser": "Url_Chooser", - "a__b__c": "a__b__c", + "URLChooser": "URLChooser", + "a__b__c": "A__b__c", "__y_z!": "__y_z!", "PLEASESTOPSCREAMING": "PLEASESTOPSCREAMING", "PLEASESTOPSCREAMING!": "PLEASESTOPSCREAMING!", @@ -204,24 +265,7 @@ class StringFiltersTests: XCTestCase { ] for (input, expected) in expectations { - let falseArgResult = try Filters.Strings.camelToSnakeCase(input, arguments: ["false"]) as? String - XCTAssertEqual(falseArgResult, expected) - } - } - - func testEscapeReservedKeywords() throws { - let expectations = [ - "self": "`self`", - "foo": "foo", - "Type": "`Type`", - "": "", - "x": "x", - "Bar": "Bar", - "#imageLiteral": "`#imageLiteral`" - ] - - for (input, expected) in expectations { - let result = try Filters.Strings.escapeReservedKeywords(value: input) as? String + let result = try Filters.Strings.titlecase(input) as? String XCTAssertEqual(result, expected) } } From ab019e1cc74c696f4f1fea04858df2c02ff2d3da Mon Sep 17 00:00:00 2001 From: David Jennes Date: Fri, 26 May 2017 21:11:36 +0200 Subject: [PATCH 3/5] Changelog entry --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd7aa0c8..ed10b54e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,9 @@ Due to the removal of legacy code, there are a few breaking changes in this new ### New Features -_None_ +* Added the `removeNewlines` filter to remove newlines (and spaces) from a string. + [David Jennes](https://github.com/djbe) + [#47](https://github.com/SwiftGen/StencilSwiftKit/pull/47) ### Internal Changes From 306189b40780c0e657a2ad0ca6ccff09845a4733 Mon Sep 17 00:00:00 2001 From: David Jennes Date: Fri, 26 May 2017 21:30:00 +0200 Subject: [PATCH 4/5] Add documentation for new filter --- Documentation/filters-strings.md | 126 ++++++++++++++++++------------- 1 file changed, 73 insertions(+), 53 deletions(-) diff --git a/Documentation/filters-strings.md b/Documentation/filters-strings.md index e56eae33..16d3237d 100644 --- a/Documentation/filters-strings.md +++ b/Documentation/filters-strings.md @@ -2,15 +2,38 @@ This is a list of filters that are added by StencilSwiftKit on top of the filters already provided by Stencil (which you can [find here](http://stencil.fuller.li/en/latest/builtins.html#built-in-filters)). +## Filter: `camelToSnakeCase` + +Transforms text from camelCase to snake_case. + +| Input | Output | +|-------------------------|-------------------------| +| `SomeCapString` | `some_cap_string` | +| `string_with_words` | `string_with_words` | +| `STRing_with_words` | `st_ring_with_words` | +| `URLChooser` | `url_chooser` | +| `PLEASE_STOP_SCREAMING` | `please_stop_screaming` | + +By default it converts to lower case, unless a single optional argument is set to "false", "no" or "0": + +| Input | Output | +|--------------------------|--------------------------| +| `SomeCapString` | `Some_Cap_String` | +| `someCapString` | `some_Cap_String` | +| `String_With_WoRds` | `String_With_Wo_Rds` | +| `string_wiTH_WOrds` | `string_wi_TH_W_Ords` | +| `URLChooser` | `URL_Chooser` | +| `PLEASE_STOP_SCREAMING!` | `PLEASE_STOP_SCREAMING!` | + ## Filter: `escapeReservedKeywords` Checks if the given string matches a reserved Swift keyword. If it does, wrap the string in escape characters (backticks). -| Input | Output | -|-------|---------------------------------| -| hello | hello | -| self | \`self\` | -| Any | \`Any\` | +| Input | Output | +|---------|--------------| +| `hello` | `hello` | +| `self` | `` `self` `` | +| `Any` | `` `Any` `` | ## Filter: `lowerFirstWord` @@ -19,10 +42,30 @@ Transforms an arbitrary string so that only the first "word" is lowercased. - If the string starts with only one uppercase character, lowercase that first character. - If the string starts with multiple uppercase character, lowercase those first characters up to the one before the last uppercase one, but only if the last one is followed by a lowercase character. This allows to support strings beginnng with an acronym, like `URL`. -| Input | Output | -|--------------|--------------------------| -| PeoplePicker | peoplePicker | -| URLChooser | urlChooser | +| Input | Output | +|----------------|--------------------------| +| `PeoplePicker` | `peoplePicker` | +| `URLChooser` | `urlChooser` | + +## Filter: `removeNewlines` + +Removes all newlines and whitespace characters from the string. + +| Input | Output | +|-----------------------|-----------------------| +| ` \ntest` | `test` | +| `test \n\t ` | `test` | +| `test\n test` | `testtest` | +| `\r\ntest\n test\n` | `testtest` | + +By default it removes whitespace characters, unless a single optional argument is set to "false", "no" or "0": + +| Input | Output | +|-----------------------|-----------------------| +| ` \ntest` | ` test` | +| `test \n\t ` | `test \t ` | +| `test\n test` | `test test` | +| `\r\ntest\n test\n` | `test test` | ## Filter: `snakeToCamelCase` @@ -34,44 +77,21 @@ Transforms a string in "snake_case" format into one in "camelCase" format, follo If the whole starting "snake_case" string only contained uppercase characters, then each component will be capitalized: uppercase the first character and lowercase the other characters. -| Input | Output | -|--------------|--------------------------| -| snake_case | SnakeCase | -| snAke_case | SnAkeCase | -| SNAKE_CASE | SnakeCase | -| __snake_case | __SnakeCase | +| Input | Output | +|----------------|---------------| +| `snake_case` | `SnakeCase` | +| `snAke_case` | `SnAkeCase` | +| `SNAKE_CASE` | `SnakeCase` | +| `__snake_case` | `__SnakeCase` | This filter accepts a parameter (boolean, default `false`) that controls the prefixing behaviour. If set to `true`, it will trim empty components from the beginning of the string -| Input | Output | -|--------------|--------------------------| -| snake_case | SnakeCase | -| snAke_case | SnAkeCase | -| SNAKE_CASE | SnakeCase | -| __snake_case | SnakeCase | - -## Filter: `camelToSnakeCase` - -Transforms text from camelCase to snake_case. - -| Input | Output | -|-----------------------|-----------------------| -| SomeCapString | some_cap_string | -| string_with_words | string_with_words | -| STRing_with_words | st_ring_with_words | -| URLChooser | url_chooser | -| PLEASE_STOP_SCREAMING | please_stop_screaming | - -By default it converts to lower case, unless a single optional argument is set to "false", "no" or "0": - -| Input | Output | -|------------------------|--------------------------| -| SomeCapString | Some_Cap_String | -| someCapString | some_Cap_String | -| String_With_WoRds | String_With_Wo_Rds | -| string_wiTH_WOrds | string_wi_TH_W_Ords | -| URLChooser | URL_Chooser | -| PLEASE_STOP_SCREAMING! | PLEASE_STOP_SCREAMING! | +| Input | Output | +|----------------|-------------| +| `snake_case` | `SnakeCase` | +| `snAke_case` | `SnAkeCase` | +| `SNAKE_CASE` | `SnakeCase` | +| `__snake_case` | `SnakeCase` | ## Filter: `swiftIdentifier` @@ -84,11 +104,11 @@ Transforms an arbitrary string into a valid Swift identifier (using only valid c The list of allowed characters can be found here: https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/LexicalStructure.html -| Input | Output | -|----------|------------------------------| -| hello | Hello | -| 42hello | _42hello | -| some$URL | Some_URL | +| Input | Output | +|------------|------------| +| `hello` | `Hello` | +| `42hello` | `_42hello` | +| `some$URL` | `Some_URL` | ## Filter: `titlecase` @@ -96,7 +116,7 @@ Simply uppercases the first character, leaving the other characters untouched. Note that even if very similar, this filter differs from the `capitalized` filter, which uppercases the first character but also lowercases the remaining characters. -| Input | Output | -|---------------|-------------------------| -| hello | Hello | -| peopleChooser | PeopleChooser | +| Input | Output | +|-----------------|-----------------| +| `hello` | `Hello` | +| `peopleChooser` | `PeopleChooser` | From d3c984b19a7ff8f425adf716ead7ded71d2642b8 Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Sat, 27 May 2017 19:42:44 +0200 Subject: [PATCH 5/5] Fix indentation --- Sources/Filters+Strings.swift | 14 +++++++------- Sources/MapNode.swift | 34 +++++++++++++++++----------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Sources/Filters+Strings.swift b/Sources/Filters+Strings.swift index 558056f0..c785933a 100644 --- a/Sources/Filters+Strings.swift +++ b/Sources/Filters+Strings.swift @@ -119,15 +119,15 @@ extension Filters { return escapeReservedKeywords(in: string) } - static func removeNewlines(_ value: Any?, arguments: [Any?]) throws -> Any? { - let removeSpaces = try Filters.parseBool(from: arguments, index: 0, required: false) ?? true - guard let string = value as? String else { throw Filters.Error.invalidInputType } + static func removeNewlines(_ value: Any?, arguments: [Any?]) throws -> Any? { + let removeSpaces = try Filters.parseBool(from: arguments, index: 0, required: false) ?? true + guard let string = value as? String else { throw Filters.Error.invalidInputType } - let set: CharacterSet = removeSpaces ? .whitespacesAndNewlines : .newlines - let result = string.components(separatedBy: set).joined() + let set: CharacterSet = removeSpaces ? .whitespacesAndNewlines : .newlines + let result = string.components(separatedBy: set).joined() - return result - } + return result + } // MARK: - Private methods diff --git a/Sources/MapNode.swift b/Sources/MapNode.swift index 88c96587..41bcba0c 100644 --- a/Sources/MapNode.swift +++ b/Sources/MapNode.swift @@ -55,7 +55,7 @@ class MapNode: NodeType { if let values = values as? [Any], values.count > 0 { let mappedValues: [String] = try values.enumerated().map { (index, item) in - let mapContext = self.context(values: values, index: index, item: item) + let mapContext = self.context(values: values, index: index, item: item) return try context.push(dictionary: mapContext) { try renderNodes(nodes, context) @@ -68,20 +68,20 @@ class MapNode: NodeType { return "" } - func context(values: [Any], index: Int, item: Any) -> [String: Any] { - var result: [String: Any] = [ - "maploop": [ - "counter": index, - "first": index == 0, - "last": index == (values.count - 1), - "item": item - ] - ] - - if let mapVariable = mapVariable { - result[mapVariable] = item - } - - return result - } + func context(values: [Any], index: Int, item: Any) -> [String: Any] { + var result: [String: Any] = [ + "maploop": [ + "counter": index, + "first": index == 0, + "last": index == (values.count - 1), + "item": item + ] + ] + + if let mapVariable = mapVariable { + result[mapVariable] = item + } + + return result + } }