diff --git a/CHANGELOG.md b/CHANGELOG.md index fbc1c1a..e1a5af3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Added `split` filter - Allow default string filters to be applied to arrays - Similar filters are suggested when unknown filter is used +- Added `indent` filter ### Bug Fixes diff --git a/Sources/Extension.swift b/Sources/Extension.swift index a389276..33a9925 100644 --- a/Sources/Extension.swift +++ b/Sources/Extension.swift @@ -58,6 +58,7 @@ class DefaultExtension: Extension { 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 aa54443..fece6eb 100644 --- a/Sources/Filters.swift +++ b/Sources/Filters.swift @@ -65,3 +65,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") +} + diff --git a/Sources/Variable.swift b/Sources/Variable.swift index c17b966..baa5594 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -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) diff --git a/Tests/StencilTests/FilterSpec.swift b/Tests/StencilTests/FilterSpec.swift index b072477..5224c12 100644 --- a/Tests/StencilTests/FilterSpec.swift +++ b/Tests/StencilTests/FilterSpec.swift @@ -244,4 +244,30 @@ func testFilter() { } + + describe("indent filter") { + $0.it("indents content") { + let template = Template(templateString: "{{ value|indent:2 }}") + let result = try template.render(Context(dictionary: ["value": "One\nTwo"])) + try expect(result) == "One\n Two" + } + + $0.it("can indent with arbitrary character") { + let template = Template(templateString: "{{ value|indent:2,\"\t\" }}") + let result = try template.render(Context(dictionary: ["value": "One\nTwo"])) + try expect(result) == "One\n\t\tTwo" + } + + $0.it("can indent first line") { + let template = Template(templateString: "{{ value|indent:2,\" \",true }}") + let result = try template.render(Context(dictionary: ["value": "One\nTwo"])) + try expect(result) == " One\n Two" + } + + $0.it("does not indent empty lines") { + let template = Template(templateString: "{{ value|indent }}") + let result = try template.render(Context(dictionary: ["value": "One\n\n\nTwo\n\n"])) + try expect(result) == "One\n\n\n Two\n\n" + } + } } diff --git a/Tests/StencilTests/FilterTagSpec.swift b/Tests/StencilTests/FilterTagSpec.swift index 2470450..e280668 100644 --- a/Tests/StencilTests/FilterTagSpec.swift +++ b/Tests/StencilTests/FilterTagSpec.swift @@ -21,5 +21,14 @@ func testFilterTag() { try expect(try template.render()).toThrow() } + $0.it("can render filters with arguments") { + let ext = Extension() + ext.registerFilter("split", filter: { + return ($0 as! String).components(separatedBy: $1[0] as! String) + }) + let env = Environment(extensions: [ext]) + let result = try env.renderTemplate(string: "{% filter split:\",\"|join:\";\" %}{{ items|join:\",\" }}{% endfilter %}", context: ["items": [1, 2]]) + try expect(result) == "1;2" + } } } diff --git a/Tests/StencilTests/VariableSpec.swift b/Tests/StencilTests/VariableSpec.swift index 1a567d8..6937e35 100644 --- a/Tests/StencilTests/VariableSpec.swift +++ b/Tests/StencilTests/VariableSpec.swift @@ -71,6 +71,13 @@ func testVariable() { try expect(result) == 3.14 } + $0.it("can resolve boolean literal") { + try expect(Variable("true").resolve(context) as? Bool) == true + try expect(Variable("false").resolve(context) as? Bool) == false + try expect(Variable("0").resolve(context) as? Int) == 0 + try expect(Variable("1").resolve(context) as? Int) == 1 + } + $0.it("can resolve a string variable") { let variable = Variable("name") let result = try variable.resolve(context) as? String