Skip to content

Commit

Permalink
Merge branch 'master' into boolean-filters
Browse files Browse the repository at this point in the history
  • Loading branch information
ilyapuchka committed May 20, 2018
2 parents 17ecb61 + 2e18892 commit 4697c56
Show file tree
Hide file tree
Showing 27 changed files with 971 additions and 98 deletions.
41 changes: 40 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,56 @@

### Enhancements

- Added an optional second parameter to the `include` tag for passing a sub context to the included file.
[Yonas Kolb](https://github.com/yonaskolb)
[#214](https://github.com/stencilproject/Stencil/pull/214)
- Variables now support the subscript notation. For example, if you have a variable `key = "name"`, and an
object `item = ["name": "John"]`, then `{{ item[key] }}` will evaluate to "John".
[David Jennes](https://github.com/djbe)
[#215](https://github.com/stencilproject/Stencil/pull/215)

- Adds support for using spaces in filter expression
[Ilya Puchka](https://github.com/yonaskolb)
[#178](https://github.com/stencilproject/Stencil/pull/178)

- Added method to add boolean filters with their negative counterparts
[Ilya Puchka](https://github.com/yonaskolb)
[#160](https://github.com/stencilproject/Stencil/pull/160)


### Bug Fixes

- Fixed using quote as a filter parameter.
[Ilya Puchka](https://github.com/ilyapuchka)
[#210](https://github.com/stencilproject/Stencil/pull/210)


## 0.11.0 (2018-04-04)

### Enhancements

- 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 method to add boolean filters with their negative counterparts
- 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
- Added support for ranges in if-in expression
- Added property `forloop.length` to get number of items in the loop
- Now you can construct ranges for loops using `a...b` syntax, i.e. `for i in 1...array.count`

### Bug Fixes

- Fixed rendering `{{ block.super }}` with several levels of inheritance
- Fixed checking dictionary values for nil in `default` filter
- Fixed comparing string variables with string literals, in Swift 4 string literals became `Substring` and thus couldn't be directly compared to strings.
- Integer literals now resolve into Int values, not Float
- Fixed accessing properties of optional properties via reflection
- No longer render optional values in arrays as `Optional(..)`
- Fixed subscription tuples by value index, i.e. `{{ tuple.0 }}`


## 0.10.1
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 Package@swift-3.swift
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),
]
)
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Stencil

[![Build Status](https://travis-ci.org/kylef/Stencil.svg?branch=master)](https://travis-ci.org/kylef/Stencil)
[![Build Status](https://travis-ci.org/stencilproject/Stencil.svg?branch=master)](https://travis-ci.org/stencilproject/Stencil)

Stencil is a simple and powerful template language for Swift. It provides a
syntax similar to Django and Mustache. If you're familiar with these, you will
Expand Down 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
4 changes: 4 additions & 0 deletions Sources/Expression.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ final class InExpression: Expression, InfixOperator, CustomStringConvertible {

if let lhs = lhsValue as? AnyHashable, let rhs = rhsValue as? [AnyHashable] {
return rhs.contains(lhs)
} else if let lhs = lhsValue as? Int, let rhs = rhsValue as? CountableClosedRange<Int> {
return rhs.contains(lhs)
} else if let lhs = lhsValue as? Int, let rhs = rhsValue as? CountableRange<Int> {
return rhs.contains(lhs)
} else if let lhs = lhsValue as? String, let rhs = rhsValue as? String {
return rhs.contains(lhs)
} else if lhsValue == nil && rhsValue == nil {
Expand Down
2 changes: 2 additions & 0 deletions Sources/Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ class DefaultExtension: Extension {
registerFilter("uppercase", filter: uppercase)
registerFilter("lowercase", filter: lowercase)
registerFilter("join", filter: joinFilter)
registerFilter("split", filter: splitFilter)
registerFilter("indent", filter: indentFilter)
}
}

Expand Down
77 changes: 74 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 @@ -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")
}

55 changes: 30 additions & 25 deletions Sources/ForTag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@ class ForNode : NodeType {
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
let components = token.components()

guard components.count >= 3 && components[2] == "in" &&
(components.count == 4 || (components.count >= 6 && components[4] == "where")) else {
throw TemplateSyntaxError("'for' statements should use the following 'for x in y where condition' `\(token.contents)`.")
func hasToken(_ token: String, at index: Int) -> Bool {
return components.count > (index + 1) && components[index] == token
}
func endsOrHasToken(_ token: String, at index: Int) -> Bool {
return components.count == index || hasToken(token, at: index)
}

guard hasToken("in", at: 2) && endsOrHasToken("where", at: 4) else {
throw TemplateSyntaxError("'for' statements should use the syntax: `for <x> in <y> [where <condition>]")
}

let loopVariables = components[1].characters
.split(separator: ",")
.map(String.init)
.map { $0.trimmingCharacters(in: CharacterSet.whitespaces) }

let variable = components[3]
.map { $0.trim(character: " ") }

var emptyNodes = [NodeType]()

Expand All @@ -35,14 +39,13 @@ class ForNode : NodeType {
_ = parser.nextToken()
}

let filter = try parser.compileFilter(variable)
let `where`: Expression?
if components.count >= 6 {
`where` = try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser)
} else {
`where` = nil
}
return ForNode(resolvable: filter, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`)
let resolvable = try parser.compileResolvable(components[3])

let `where` = hasToken("where", at: 4)
? try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser)
: nil

return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`)
}

init(resolvable: Resolvable, loopVariables: [String], nodes:[NodeType], emptyNodes:[NodeType], where: Expression? = nil) {
Expand All @@ -53,25 +56,26 @@ class ForNode : NodeType {
self.where = `where`
}

func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) rethrows -> Result {
func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result {
if loopVariables.isEmpty {
return try context.push() {
return try closure()
}
}

if let value = value as? (Any, Any) {
let first = loopVariables[0]

if loopVariables.count == 2 {
let second = loopVariables[1]

return try context.push(dictionary: [first: value.0, second: value.1]) {
return try closure()
}
let valueMirror = Mirror(reflecting: value)
if case .tuple? = valueMirror.displayStyle {
if loopVariables.count > Int(valueMirror.children.count) {
throw TemplateSyntaxError("Tuple '\(value)' has less values than loop variables")
}
var variablesContext = [String: Any]()
valueMirror.children.prefix(loopVariables.count).enumerated().forEach({ (offset, element) in
if loopVariables[offset] != "_" {
variablesContext[loopVariables[offset]] = element.value
}
})

return try context.push(dictionary: [first: value.0]) {
return try context.push(dictionary: variablesContext) {
return try closure()
}
}
Expand Down Expand Up @@ -131,6 +135,7 @@ class ForNode : NodeType {
"last": index == (count - 1),
"counter": index + 1,
"counter0": index,
"length": count
]

return try context.push(dictionary: ["forloop": forContext]) {
Expand Down
2 changes: 1 addition & 1 deletion Sources/IfTag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ final class IfExpressionParser {
}
}

return .variable(try tokenParser.compileFilter(component))
return .variable(try tokenParser.compileResolvable(component))
}
}

Expand Down
13 changes: 8 additions & 5 deletions Sources/Include.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@ import PathKit

class IncludeNode : NodeType {
let templateName: Variable
let includeContext: String?

class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
let bits = token.components()

guard bits.count == 2 else {
throw TemplateSyntaxError("'include' tag takes one argument, the template file to be included")
guard bits.count == 2 || bits.count == 3 else {
throw TemplateSyntaxError("'include' tag requires one argument, the template file to be included. A second optional argument can be used to specify the context that will be passed to the included file")
}

return IncludeNode(templateName: Variable(bits[1]))
return IncludeNode(templateName: Variable(bits[1]), includeContext: bits.count == 3 ? bits[2] : nil)
}

init(templateName: Variable) {
init(templateName: Variable, includeContext: String? = nil) {
self.templateName = templateName
self.includeContext = includeContext
}

func render(_ context: Context) throws -> String {
Expand All @@ -25,7 +27,8 @@ class IncludeNode : NodeType {

let template = try context.environment.loadTemplate(name: templateName)

return try context.push {
let subContext = includeContext.flatMap { context[$0] as? [String: Any] }
return try context.push(dictionary: subContext) {
return try template.render(context)
}
}
Expand Down
Loading

0 comments on commit 4697c56

Please sign in to comment.