Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2e25f25ac | ||
|
|
fe01beb4bb | ||
|
|
2e6a7215c5 | ||
|
|
f457cddd3f | ||
|
|
6b02fccf84 | ||
|
|
29e859f1aa | ||
|
|
8fa0bd275c | ||
|
|
91847208a3 | ||
|
|
86ed8770e1 | ||
|
|
0bc6bd974e | ||
|
|
fa68ba9df8 | ||
|
|
4827fb8e20 | ||
|
|
359d086c02 | ||
|
|
24c9746689 | ||
|
|
c4a84a6375 | ||
|
|
c30597457f | ||
|
|
b4dc8dbb76 | ||
|
|
2e80f70f67 | ||
|
|
a6dba67828 | ||
|
|
691fe523b3 | ||
|
|
c0e66eb96f | ||
|
|
0156f6f37b | ||
|
|
79a16854e7 | ||
|
|
a4b75f3c89 | ||
|
|
0f3a302108 | ||
|
|
1223efbc7e | ||
|
|
9357df35d1 | ||
|
|
a96fcff680 | ||
|
|
0017aee5a8 | ||
|
|
1e6846867e | ||
|
|
93c07e22b1 | ||
|
|
98461c75b0 | ||
|
|
9994972a24 | ||
|
|
cf7acea440 | ||
|
|
9e24ab658b | ||
|
|
a52ee21b72 | ||
|
|
4a93815d4c | ||
|
|
7e88cbde11 | ||
|
|
e7a0738bda | ||
|
|
46f179e3ed | ||
|
|
bf4be38377 | ||
|
|
14bac03990 | ||
|
|
3180b26673 | ||
|
|
000e9a7f1a | ||
|
|
7b9817ed50 | ||
|
|
482d595d01 | ||
|
|
f1fc747897 | ||
|
|
0444f45d2b | ||
|
|
86bfbf215f | ||
|
|
039bf4b7cb | ||
|
|
4308baf5f0 | ||
|
|
2455fb9ed0 | ||
|
|
64571464d9 | ||
|
|
5821e4849e | ||
|
|
793773f191 | ||
|
|
e217a9c873 | ||
|
|
584ed916ab |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
.conche/
|
||||
.build/
|
||||
Packages/
|
||||
Package.resolved
|
||||
Package.pins
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
3.1
|
||||
21
.travis.yml
21
.travis.yml
@@ -1,11 +1,22 @@
|
||||
os:
|
||||
- osx
|
||||
- linux
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
osx_image: xcode8.3
|
||||
env: SWIFT_VERSION=3.1.1
|
||||
- os: osx
|
||||
osx_image: xcode9
|
||||
env: SWIFT_VERSION=4.0
|
||||
- os: osx
|
||||
osx_image: xcode9.1
|
||||
env: SWIFT_VERSION=4.0
|
||||
- os: linux
|
||||
env: SWIFT_VERSION=3.1.1
|
||||
- os: linux
|
||||
env: SWIFT_VERSION=4.0
|
||||
language: generic
|
||||
sudo: required
|
||||
dist: trusty
|
||||
osx_image: xcode8
|
||||
install:
|
||||
- eval "$(curl -sL https://gist.githubusercontent.com/kylef/5c0475ff02b7c7671d2a/raw/9f442512a46d7a2af7b850d65a7e9bd31edfb09b/swiftenv-install.sh)"
|
||||
- eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"
|
||||
script:
|
||||
- swift test
|
||||
|
||||
57
CHANGELOG.md
57
CHANGELOG.md
@@ -1,5 +1,60 @@
|
||||
# Stencil Changelog
|
||||
|
||||
## 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 `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
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Add support for Xcode 9.1.
|
||||
|
||||
## 0.10.0
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Adds `counter0` to for loop context allowing you to get the current index of
|
||||
the for loop 0 indexed.
|
||||
- Introduces a new `DictionaryLoader` for loading templates from a Swift
|
||||
Dictionary.
|
||||
- Added `in` expression in if tag for strings and arrays of hashable types
|
||||
- You can now access the amount of items in a dictionary using the `count`
|
||||
property.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixes a potential crash when using the `{% for %}` template tag with the
|
||||
incorrect amount of arguments.
|
||||
- Fixes a potential crash when using incomplete tokens in a template for
|
||||
example, `{%%}` or `{{}}`.
|
||||
- Fixes evaluating nil properties as true
|
||||
|
||||
|
||||
## 0.9.0
|
||||
|
||||
### Enhancements
|
||||
@@ -20,7 +75,7 @@
|
||||
- `for` block now allows you to iterate over array of tuples or dictionaries.
|
||||
|
||||
```html+django
|
||||
{% for key, value in thing %}
|
||||
{% for key,value in thing %}
|
||||
<li>{{ key }}: {{ value }}</li>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
@@ -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
Package@swift-3.swift
Normal file
10
Package@swift-3.swift
Normal file
@@ -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),
|
||||
]
|
||||
)
|
||||
@@ -34,7 +34,7 @@ let context = [
|
||||
]
|
||||
|
||||
let environment = Environment(loader: FileSystemLoader(paths: ["templates/"]))
|
||||
let rendered = try environment.renderTemplate(name: context)
|
||||
let rendered = try environment.renderTemplate(name: "article_list.html", context: context)
|
||||
|
||||
print(rendered)
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
public struct Environment {
|
||||
public let templateClass: Template.Type
|
||||
public let extensions: [Extension]
|
||||
public var extensions: [Extension]
|
||||
|
||||
public var loader: Loader?
|
||||
|
||||
|
||||
@@ -85,6 +85,41 @@ final class NotExpression: Expression, PrefixOperator, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
final class InExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
let lhs: Expression
|
||||
let rhs: Expression
|
||||
|
||||
init(lhs: Expression, rhs: Expression) {
|
||||
self.lhs = lhs
|
||||
self.rhs = rhs
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "(\(lhs) in \(rhs))"
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression {
|
||||
let lhsValue = try lhs.variable.resolve(context)
|
||||
let rhsValue = try rhs.variable.resolve(context)
|
||||
|
||||
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 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final class OrExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
let lhs: Expression
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
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? {
|
||||
if let value = value {
|
||||
// value can be optional wrapping nil, so this way we check for underlying value
|
||||
if let value = value, String(describing: value) != "nil" {
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -39,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")
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,15 @@ class ForNode : NodeType {
|
||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
|
||||
guard components.count >= 2 && 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
|
||||
@@ -20,8 +26,6 @@ class ForNode : NodeType {
|
||||
.map(String.init)
|
||||
.map { $0.trimmingCharacters(in: CharacterSet.whitespaces) }
|
||||
|
||||
let variable = components[3]
|
||||
|
||||
var emptyNodes = [NodeType]()
|
||||
|
||||
let forNodes = try parser.parse(until(["endfor", "empty"]))
|
||||
@@ -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) {
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -90,6 +94,26 @@ class ForNode : NodeType {
|
||||
values = dictionary.map { ($0.key, $0.value) }
|
||||
} else if let array = resolved as? [Any] {
|
||||
values = array
|
||||
} else if let range = resolved as? CountableClosedRange<Int> {
|
||||
values = Array(range)
|
||||
} else if let range = resolved as? CountableRange<Int> {
|
||||
values = Array(range)
|
||||
} else if let resolved = resolved {
|
||||
let mirror = Mirror(reflecting: resolved)
|
||||
switch mirror.displayStyle {
|
||||
case .struct?, .tuple?:
|
||||
values = Array(mirror.children)
|
||||
case .class?:
|
||||
var children = Array(mirror.children)
|
||||
var currentMirror: Mirror? = mirror
|
||||
while let superclassMirror = currentMirror?.superclassMirror {
|
||||
children.append(contentsOf: superclassMirror.children)
|
||||
currentMirror = superclassMirror
|
||||
}
|
||||
values = Array(children)
|
||||
default:
|
||||
values = []
|
||||
}
|
||||
} else {
|
||||
values = []
|
||||
}
|
||||
@@ -110,6 +134,8 @@ class ForNode : NodeType {
|
||||
"first": index == 0,
|
||||
"last": index == (count - 1),
|
||||
"counter": index + 1,
|
||||
"counter0": index,
|
||||
"length": count
|
||||
]
|
||||
|
||||
return try context.push(dictionary: ["forloop": forContext]) {
|
||||
|
||||
@@ -14,6 +14,7 @@ enum Operator {
|
||||
|
||||
|
||||
let operators: [Operator] = [
|
||||
.infix("in", 5, InExpression.self),
|
||||
.infix("or", 6, OrExpression.self),
|
||||
.infix("and", 7, AndExpression.self),
|
||||
.prefix("not", 8, NotExpression.self),
|
||||
@@ -110,7 +111,7 @@ final class IfExpressionParser {
|
||||
}
|
||||
}
|
||||
|
||||
return .variable(try tokenParser.compileFilter(component))
|
||||
return .variable(try tokenParser.compileResolvable(component))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,36 @@
|
||||
class BlockContext {
|
||||
class var contextKey: String { return "block_context" }
|
||||
|
||||
var blocks: [String: BlockNode]
|
||||
var blocks: [String: [BlockNode]]
|
||||
|
||||
init(blocks: [String: BlockNode]) {
|
||||
self.blocks = blocks
|
||||
self.blocks = [:]
|
||||
blocks.forEach { (key, value) in
|
||||
self.blocks[key] = [value]
|
||||
}
|
||||
}
|
||||
|
||||
func push(_ block: BlockNode, forKey blockName: String) {
|
||||
if var blocks = blocks[blockName] {
|
||||
blocks.append(block)
|
||||
self.blocks[blockName] = blocks
|
||||
} else {
|
||||
self.blocks[blockName] = [block]
|
||||
}
|
||||
}
|
||||
|
||||
func pop(_ blockName: String) -> BlockNode? {
|
||||
return blocks.removeValue(forKey: blockName)
|
||||
if var blocks = blocks[blockName] {
|
||||
let block = blocks.removeFirst()
|
||||
if blocks.isEmpty {
|
||||
self.blocks.removeValue(forKey: blockName)
|
||||
} else {
|
||||
self.blocks[blockName] = blocks
|
||||
}
|
||||
return block
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,9 +92,7 @@ class ExtendsNode : NodeType {
|
||||
blockContext = context
|
||||
|
||||
for (key, value) in blocks {
|
||||
if !blockContext.blocks.keys.contains(key) {
|
||||
blockContext.blocks[key] = value
|
||||
}
|
||||
blockContext.push(value, forKey: key)
|
||||
}
|
||||
} else {
|
||||
blockContext = BlockContext(blocks: blocks)
|
||||
@@ -109,7 +129,11 @@ class BlockNode : NodeType {
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
if let blockContext = context[BlockContext.contextKey] as? BlockContext, let node = blockContext.pop(name) {
|
||||
return try context.push(dictionary: ["block": ["super": self]]) {
|
||||
let newContext: [String: Any] = [
|
||||
BlockContext.contextKey: blockContext,
|
||||
"block": ["super": try self.render(context)]
|
||||
]
|
||||
return try context.push(dictionary: newContext) {
|
||||
return try node.render(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,17 @@ struct Lexer {
|
||||
self.templateString = templateString
|
||||
}
|
||||
|
||||
func createToken(string:String) -> Token {
|
||||
func createToken(string: String) -> Token {
|
||||
func strip() -> String {
|
||||
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[start..<end].trim(character: " ")
|
||||
let trimmed = String(string[start..<end])
|
||||
.components(separatedBy: "\n")
|
||||
.filter({ !$0.isEmpty })
|
||||
.map({ $0.trim(character: " ") })
|
||||
.joined(separator: " ")
|
||||
return trimmed
|
||||
}
|
||||
|
||||
if string.hasPrefix("{{") {
|
||||
@@ -90,6 +96,7 @@ class Scanner {
|
||||
index = content.index(after: index)
|
||||
}
|
||||
|
||||
content = ""
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -147,6 +154,6 @@ extension String {
|
||||
func trim(character: Character) -> String {
|
||||
let first = findFirstNot(character: character) ?? startIndex
|
||||
let last = findLastNot(character: character) ?? endIndex
|
||||
return self[first..<last]
|
||||
return String(self[first..<last])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,33 @@ public class FileSystemLoader: Loader, CustomStringConvertible {
|
||||
}
|
||||
|
||||
|
||||
public class DictionaryLoader: Loader {
|
||||
public let templates: [String: String]
|
||||
|
||||
public init(templates: [String: String]) {
|
||||
self.templates = templates
|
||||
}
|
||||
|
||||
public func loadTemplate(name: String, environment: Environment) throws -> Template {
|
||||
if let content = templates[name] {
|
||||
return environment.templateClass.init(templateString: content, environment: environment, name: name)
|
||||
}
|
||||
|
||||
throw TemplateDoesNotExist(templateNames: [name], loader: self)
|
||||
}
|
||||
|
||||
public func loadTemplate(names: [String], environment: Environment) throws -> Template {
|
||||
for name in names {
|
||||
if let content = templates[name] {
|
||||
return environment.templateClass.init(templateString: content, environment: environment, name: name)
|
||||
}
|
||||
}
|
||||
|
||||
throw TemplateDoesNotExist(templateNames: names, loader: self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Path {
|
||||
func safeJoin(path: Path) throws -> Path {
|
||||
let newPath = self + path
|
||||
|
||||
@@ -78,6 +78,8 @@ public class VariableNode : NodeType {
|
||||
func stringify(_ result: Any?) -> String {
|
||||
if let result = result as? String {
|
||||
return result
|
||||
} else if let array = result as? [Any?] {
|
||||
return unwrap(array).description
|
||||
} else if let result = result as? CustomStringConvertible {
|
||||
return result.description
|
||||
} else if let result = result as? NSObject {
|
||||
@@ -86,3 +88,16 @@ func stringify(_ result: Any?) -> String {
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func unwrap(_ array: [Any?]) -> [Any] {
|
||||
return array.map { (item: Any?) -> Any in
|
||||
if let item = item {
|
||||
if let items = item as? [Any?] {
|
||||
return unwrap(items)
|
||||
} else {
|
||||
return item
|
||||
}
|
||||
}
|
||||
else { return item as Any }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ public class TokenParser {
|
||||
case .text(let text):
|
||||
nodes.append(TextNode(text: text))
|
||||
case .variable:
|
||||
nodes.append(VariableNode(variable: try compileFilter(token.contents)))
|
||||
nodes.append(VariableNode(variable: try compileResolvable(token.contents)))
|
||||
case .block:
|
||||
if let parse_until = parse_until , parse_until(self, token) {
|
||||
prependToken(token)
|
||||
@@ -88,11 +88,77 @@ public class TokenParser {
|
||||
}
|
||||
}
|
||||
|
||||
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 = 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 {
|
||||
return try FilterExpression(token: token, parser: self)
|
||||
}
|
||||
|
||||
public func compileResolvable(_ token: String) throws -> Resolvable {
|
||||
return try RangeVariable(token, parser: self)
|
||||
?? compileFilter(token)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 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]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -60,13 +60,20 @@ public struct Variable : Equatable, Resolvable {
|
||||
|
||||
if (variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\"")) {
|
||||
// String literal
|
||||
return variable[variable.characters.index(after: variable.startIndex) ..< variable.characters.index(before: variable.endIndex)]
|
||||
return String(variable[variable.characters.index(after: variable.startIndex) ..< variable.characters.index(before: variable.endIndex)])
|
||||
}
|
||||
|
||||
// Number literal
|
||||
if let int = Int(variable) {
|
||||
return int
|
||||
}
|
||||
if let number = Number(variable) {
|
||||
// Number literal
|
||||
return number
|
||||
}
|
||||
// Boolean literal
|
||||
if let bool = Bool(variable) {
|
||||
return bool
|
||||
}
|
||||
|
||||
for bit in lookup() {
|
||||
current = normalize(current)
|
||||
@@ -74,7 +81,11 @@ public struct Variable : Equatable, Resolvable {
|
||||
if let context = current as? Context {
|
||||
current = context[bit]
|
||||
} else if let dictionary = current as? [String: Any] {
|
||||
current = dictionary[bit]
|
||||
if bit == "count" {
|
||||
current = dictionary.count
|
||||
} else {
|
||||
current = dictionary[bit]
|
||||
}
|
||||
} else if let array = current as? [Any] {
|
||||
if let index = Int(bit) {
|
||||
if index >= 0 && index < array.count {
|
||||
@@ -96,9 +107,7 @@ public struct Variable : Equatable, Resolvable {
|
||||
current = object.value(forKey: bit)
|
||||
#endif
|
||||
} else if let value = current {
|
||||
let mirror = Mirror(reflecting: value)
|
||||
current = mirror.descendant(bit)
|
||||
|
||||
current = Mirror(reflecting: value).getValue(for: bit)
|
||||
if current == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -121,6 +130,42 @@ public func ==(lhs: Variable, rhs: Variable) -> Bool {
|
||||
return lhs.variable == rhs.variable
|
||||
}
|
||||
|
||||
/// A structure used to represet range of two integer values expressed as `from...to`.
|
||||
/// Values should be numbers (they will be converted to integers).
|
||||
/// Rendering this variable produces array from range `from...to`.
|
||||
/// If `from` is more than `to` array will contain values of reversed range.
|
||||
public struct RangeVariable: Resolvable {
|
||||
public let from: Resolvable
|
||||
public let to: Resolvable
|
||||
|
||||
public init?(_ token: String, parser: TokenParser) throws {
|
||||
let components = token.components(separatedBy: "...")
|
||||
guard components.count == 2 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.from = try parser.compileFilter(components[0])
|
||||
self.to = try parser.compileFilter(components[1])
|
||||
}
|
||||
|
||||
public func resolve(_ context: Context) throws -> Any? {
|
||||
let fromResolved = try from.resolve(context)
|
||||
let toResolved = try to.resolve(context)
|
||||
|
||||
guard let from = fromResolved.flatMap(toNumber(value:)).flatMap(Int.init) else {
|
||||
throw TemplateSyntaxError("'from' value is not an Integer (\(fromResolved ?? "nil"))")
|
||||
}
|
||||
|
||||
guard let to = toResolved.flatMap(toNumber(value:)).flatMap(Int.init) else {
|
||||
throw TemplateSyntaxError("'to' value is not an Integer (\(toResolved ?? "nil") )")
|
||||
}
|
||||
|
||||
let range = min(from, to)...max(from, to)
|
||||
return from > to ? Array(range.reversed()) : Array(range)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func normalize(_ current: Any?) -> Any? {
|
||||
if let current = current as? Normalizable {
|
||||
@@ -171,3 +216,40 @@ func parseFilterComponents(token: String) -> (String, [Variable]) {
|
||||
.map { Variable($0) }
|
||||
return (name, variables)
|
||||
}
|
||||
|
||||
extension Mirror {
|
||||
func getValue(for key: String) -> Any? {
|
||||
let result = descendant(key) ?? Int(key).flatMap({ descendant($0) })
|
||||
if result == nil {
|
||||
// go through inheritance chain to reach superclass properties
|
||||
return superclassMirror?.getValue(for: key)
|
||||
} else if let result = result {
|
||||
guard String(describing: result) != "nil" else {
|
||||
// mirror returns non-nil value even for nil-containing properties
|
||||
// so we have to check if its value is actually nil or not
|
||||
return nil
|
||||
}
|
||||
if let result = (result as? AnyOptional)?.wrapped {
|
||||
return result
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
protocol AnyOptional {
|
||||
var wrapped: Any? { get }
|
||||
}
|
||||
|
||||
extension Optional: AnyOptional {
|
||||
var wrapped: Any? {
|
||||
switch self {
|
||||
case let .some(value): return value
|
||||
case .none: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Stencil",
|
||||
"version": "0.9.0",
|
||||
"version": "0.11.0",
|
||||
"summary": "Stencil is a simple and powerful template language for Swift.",
|
||||
"homepage": "https://stencil.fuller.li",
|
||||
"license": {
|
||||
@@ -13,7 +13,7 @@
|
||||
"social_media_url": "https://twitter.com/kylefuller",
|
||||
"source": {
|
||||
"git": "https://github.com/kylef/Stencil.git",
|
||||
"tag": "0.9.0"
|
||||
"tag": "0.11.0"
|
||||
},
|
||||
"source_files": [
|
||||
"Sources/*.swift"
|
||||
@@ -25,6 +25,8 @@
|
||||
},
|
||||
"requires_arc": true,
|
||||
"dependencies": {
|
||||
"PathKit": [ "~> 0.8.0" ]
|
||||
"PathKit": [
|
||||
"~> 0.8.0"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,6 +279,26 @@ func testExpressions() {
|
||||
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("in expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "in", "rhs"], tokenParser: parser)
|
||||
|
||||
$0.it("evaluates to true when rhs contains lhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["a", "b", "c"]]))).to.beTrue()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "abc"]))).to.beTrue()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1...3]))).to.beTrue()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1..<3]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when rhs does not contain lhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [2, 3, 4]]))).to.beFalse()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["b", "c", "d"]]))).to.beFalse()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "bcd"]))).to.beFalse()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 1...3]))).to.beFalse()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 3, "rhs": 1..<3]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,32 +89,45 @@ func testFilter() {
|
||||
}
|
||||
}
|
||||
|
||||
describe("string filters") {
|
||||
$0.context("given string") {
|
||||
$0.it("transforms a string to be capitalized") {
|
||||
let template = Template(templateString: "{{ name|capitalize }}")
|
||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
|
||||
describe("capitalize filter") {
|
||||
let template = Template(templateString: "{{ name|capitalize }}")
|
||||
$0.it("transforms a string to be uppercase") {
|
||||
let template = Template(templateString: "{{ name|uppercase }}")
|
||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
||||
try expect(result) == "KYLE"
|
||||
}
|
||||
|
||||
$0.it("capitalizes a string") {
|
||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
||||
try expect(result) == "Kyle"
|
||||
$0.it("transforms a string to be lowercase") {
|
||||
let template = Template(templateString: "{{ name|lowercase }}")
|
||||
let result = try template.render(Context(dictionary: ["name": "Kyle"]))
|
||||
try expect(result) == "kyle"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$0.context("given array of strings") {
|
||||
$0.it("transforms a string to be capitalized") {
|
||||
let template = Template(templateString: "{{ names|capitalize }}")
|
||||
let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]]))
|
||||
try expect(result) == "[\"Kyle\", \"Kyle\"]"
|
||||
}
|
||||
|
||||
describe("uppercase filter") {
|
||||
let template = Template(templateString: "{{ name|uppercase }}")
|
||||
$0.it("transforms a string to be uppercase") {
|
||||
let template = Template(templateString: "{{ names|uppercase }}")
|
||||
let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]]))
|
||||
try expect(result) == "[\"KYLE\", \"KYLE\"]"
|
||||
}
|
||||
|
||||
$0.it("transforms a string to be uppercase") {
|
||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
||||
try expect(result) == "KYLE"
|
||||
}
|
||||
}
|
||||
|
||||
describe("lowercase filter") {
|
||||
let template = Template(templateString: "{{ name|lowercase }}")
|
||||
|
||||
$0.it("transforms a string to be lowercase") {
|
||||
let result = try template.render(Context(dictionary: ["name": "Kyle"]))
|
||||
try expect(result) == "kyle"
|
||||
$0.it("transforms a string to be lowercase") {
|
||||
let template = Template(templateString: "{{ names|lowercase }}")
|
||||
let result = try template.render(Context(dictionary: ["names": ["Kyle", "Kyle"]]))
|
||||
try expect(result) == "[\"kyle\", \"kyle\"]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +149,26 @@ func testFilter() {
|
||||
let result = try template.render(Context(dictionary: [:]))
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can use int as default") {
|
||||
let template = Template(templateString: "{{ value|default:1 }}")
|
||||
let result = try template.render(Context(dictionary: [:]))
|
||||
try expect(result) == "1"
|
||||
}
|
||||
|
||||
$0.it("can use float as default") {
|
||||
let template = Template(templateString: "{{ value|default:1.5 }}")
|
||||
let result = try template.render(Context(dictionary: [:]))
|
||||
try expect(result) == "1.5"
|
||||
}
|
||||
|
||||
$0.it("checks for underlying nil value correctly") {
|
||||
let template = Template(templateString: "Hello {{ user.name|default:\"anonymous\" }}")
|
||||
let nilName: String? = nil
|
||||
let user: [String: Any?] = ["name": nilName]
|
||||
let result = try template.render(Context(dictionary: ["user": user]))
|
||||
try expect(result) == "Hello anonymous"
|
||||
}
|
||||
}
|
||||
|
||||
describe("join filter") {
|
||||
@@ -163,4 +196,78 @@ func testFilter() {
|
||||
try expect(result) == "OneTwo"
|
||||
}
|
||||
}
|
||||
|
||||
describe("split filter") {
|
||||
let template = Template(templateString: "{{ value|split:\", \" }}")
|
||||
|
||||
$0.it("split a string into array") {
|
||||
let result = try template.render(Context(dictionary: ["value": "One, Two"]))
|
||||
try expect(result) == "[\"One\", \"Two\"]"
|
||||
}
|
||||
|
||||
$0.it("can split without arguments") {
|
||||
let template = Template(templateString: "{{ value|split }}")
|
||||
let result = try template.render(Context(dictionary: ["value": "One, Two"]))
|
||||
try expect(result) == "[\"One,\", \"Two\"]"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe("filter suggestion") {
|
||||
|
||||
$0.it("made for unknown filter") {
|
||||
let template = Template(templateString: "{{ value|unknownFilter }}")
|
||||
let expectedError = TemplateSyntaxError("Unknown filter 'unknownFilter'. Found similar filters: 'knownFilter'")
|
||||
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("knownFilter") { value, _ in value }
|
||||
|
||||
try expect(template.render(Context(dictionary: [:], environment: Environment(extensions: [filterExtension])))).toThrow(expectedError)
|
||||
}
|
||||
|
||||
$0.it("made for multiple similar filters") {
|
||||
let template = Template(templateString: "{{ value|lowerFirst }}")
|
||||
let expectedError = TemplateSyntaxError("Unknown filter 'lowerFirst'. Found similar filters: 'lowerFirstWord', 'lowercase'")
|
||||
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("lowerFirstWord") { value, _ in value }
|
||||
filterExtension.registerFilter("lowerFirstLetter") { value, _ in value }
|
||||
|
||||
try expect(template.render(Context(dictionary: [:], environment: Environment(extensions: [filterExtension])))).toThrow(expectedError)
|
||||
}
|
||||
|
||||
$0.it("not made when can't find similar filter") {
|
||||
let template = Template(templateString: "{{ value|unknownFilter }}")
|
||||
let expectedError = TemplateSyntaxError("Unknown filter 'unknownFilter'.")
|
||||
try expect(template.render(Context(dictionary: [:]))).toThrow(expectedError)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ func testForNode() {
|
||||
"dict": [
|
||||
"one": "I",
|
||||
"two": "II",
|
||||
]
|
||||
],
|
||||
"tuples": [(1, 2, 3), (4, 5, 6)]
|
||||
])
|
||||
|
||||
$0.it("renders the given nodes for each item") {
|
||||
@@ -37,6 +38,22 @@ func testForNode() {
|
||||
try expect(try node.render(any_context)) == "123"
|
||||
}
|
||||
|
||||
$0.it("renders a context variable of type CountableClosedRange<Int>") {
|
||||
let context = Context(dictionary: ["range": 1...3])
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(resolvable: Variable("range"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
|
||||
try expect(try node.render(context)) == "123"
|
||||
}
|
||||
|
||||
$0.it("renders a context variable of type CountableRange<Int>") {
|
||||
let context = Context(dictionary: ["range": 1..<4])
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(resolvable: Variable("range"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
|
||||
try expect(try node.render(context)) == "123"
|
||||
}
|
||||
|
||||
#if os(OSX)
|
||||
$0.it("renders a context variable of type NSArray") {
|
||||
let nsarray_context = Context(dictionary: [
|
||||
@@ -67,6 +84,18 @@ func testForNode() {
|
||||
try expect(try node.render(context)) == "112233"
|
||||
}
|
||||
|
||||
$0.it("renders the given nodes while providing item counter") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter0")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "102132"
|
||||
}
|
||||
|
||||
$0.it("renders the given nodes while providing loop length") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.length")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "132333"
|
||||
}
|
||||
|
||||
$0.it("renders the given nodes while filtering items using where expression") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
|
||||
let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment()))
|
||||
@@ -105,20 +134,185 @@ func testForNode() {
|
||||
try expect(result) == fixture
|
||||
}
|
||||
|
||||
$0.it("renders supports iterating over dictionary") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "key")]
|
||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||
let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key"], nodes: nodes, emptyNodes: emptyNodes, where: nil)
|
||||
try expect(try node.render(context)) == "onetwo"
|
||||
$0.context("given array of tuples") {
|
||||
$0.it("can iterate over all tuple values") {
|
||||
let templateString = "{% for first,second,third in tuples %}" +
|
||||
"{{ first }}, {{ second }}, {{ third }}\n" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
let result = try template.render(context)
|
||||
|
||||
let fixture = "1, 2, 3\n4, 5, 6\n\n"
|
||||
try expect(result) == fixture
|
||||
}
|
||||
|
||||
$0.it("can iterate with less number of variables") {
|
||||
let templateString = "{% for first,second in tuples %}" +
|
||||
"{{ first }}, {{ second }}\n" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
let result = try template.render(context)
|
||||
|
||||
let fixture = "1, 2\n4, 5\n\n"
|
||||
try expect(result) == fixture
|
||||
}
|
||||
|
||||
$0.it("can use _ to skip variables") {
|
||||
let templateString = "{% for first,_,third in tuples %}" +
|
||||
"{{ first }}, {{ third }}\n" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
let result = try template.render(context)
|
||||
|
||||
let fixture = "1, 3\n4, 6\n\n"
|
||||
try expect(result) == fixture
|
||||
}
|
||||
|
||||
$0.it("throws when number of variables is more than number of tuple values") {
|
||||
let templateString = "{% for key,value,smth in dict %}" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
try expect(template.render(context)).toThrow()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$0.it("can iterate over dictionary") {
|
||||
let templateString = "{% for key,value in dict %}" +
|
||||
"{{ key }}: {{ value }}," +
|
||||
"{% endfor %}"
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
let result = try template.render(context)
|
||||
|
||||
let sortedResult = result.characters.split(separator: ",").map(String.init).sorted(by: <)
|
||||
try expect(sortedResult) == ["one: I", "two: II"]
|
||||
}
|
||||
|
||||
$0.it("renders supports iterating over dictionary") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "key"), VariableNode(variable: "value")]
|
||||
let nodes: [NodeType] = [
|
||||
VariableNode(variable: "key"),
|
||||
TextNode(text: ","),
|
||||
]
|
||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||
let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key"], nodes: nodes, emptyNodes: emptyNodes, where: nil)
|
||||
let result = try node.render(context)
|
||||
|
||||
let sortedResult = result.characters.split(separator: ",").map(String.init).sorted(by: <)
|
||||
try expect(sortedResult) == ["one", "two"]
|
||||
}
|
||||
|
||||
$0.it("renders supports iterating over dictionary") {
|
||||
let nodes: [NodeType] = [
|
||||
VariableNode(variable: "key"),
|
||||
TextNode(text: "="),
|
||||
VariableNode(variable: "value"),
|
||||
TextNode(text: ","),
|
||||
]
|
||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||
let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key", "value"], nodes: nodes, emptyNodes: emptyNodes, where: nil)
|
||||
try expect(try node.render(context)) == "oneItwoII"
|
||||
|
||||
let result = try node.render(context)
|
||||
|
||||
let sortedResult = result.characters.split(separator: ",").map(String.init).sorted(by: <)
|
||||
try expect(sortedResult) == ["one=I", "two=II"]
|
||||
}
|
||||
|
||||
$0.it("handles invalid input") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "for i"),
|
||||
]
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let error = TemplateSyntaxError("'for' statements should use the syntax: `for <x> in <y> [where <condition>]")
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
|
||||
$0.it("can iterate over struct properties") {
|
||||
struct MyStruct {
|
||||
let string: String
|
||||
let number: Int
|
||||
}
|
||||
|
||||
let context = Context(dictionary: [
|
||||
"struct": MyStruct(string: "abc", number: 123)
|
||||
])
|
||||
|
||||
let nodes: [NodeType] = [
|
||||
VariableNode(variable: "property"),
|
||||
TextNode(text: "="),
|
||||
VariableNode(variable: "value"),
|
||||
TextNode(text: "\n"),
|
||||
]
|
||||
let node = ForNode(resolvable: Variable("struct"), loopVariables: ["property", "value"], nodes: nodes, emptyNodes: [])
|
||||
let result = try node.render(context)
|
||||
|
||||
try expect(result) == "string=abc\nnumber=123\n"
|
||||
}
|
||||
|
||||
$0.it("can iterate tuple items") {
|
||||
let context = Context(dictionary: [
|
||||
"tuple": (one: 1, two: "dva"),
|
||||
])
|
||||
|
||||
let nodes: [NodeType] = [
|
||||
VariableNode(variable: "label"),
|
||||
TextNode(text: "="),
|
||||
VariableNode(variable: "value"),
|
||||
TextNode(text: "\n"),
|
||||
]
|
||||
|
||||
let node = ForNode(resolvable: Variable("tuple"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: [])
|
||||
let result = try node.render(context)
|
||||
|
||||
try expect(result) == "one=1\ntwo=dva\n"
|
||||
}
|
||||
|
||||
$0.it("can iterate over class properties") {
|
||||
class MyClass {
|
||||
var baseString: String
|
||||
var baseInt: Int
|
||||
init(_ string: String, _ int: Int) {
|
||||
baseString = string
|
||||
baseInt = int
|
||||
}
|
||||
}
|
||||
|
||||
class MySubclass: MyClass {
|
||||
var childString: String
|
||||
init(_ childString: String, _ string: String, _ int: Int) {
|
||||
self.childString = childString
|
||||
super.init(string, int)
|
||||
}
|
||||
}
|
||||
|
||||
let context = Context(dictionary: [
|
||||
"class": MySubclass("child", "base", 1)
|
||||
])
|
||||
|
||||
let nodes: [NodeType] = [
|
||||
VariableNode(variable: "label"),
|
||||
TextNode(text: "="),
|
||||
VariableNode(variable: "value"),
|
||||
TextNode(text: "\n"),
|
||||
]
|
||||
|
||||
let node = ForNode(resolvable: Variable("class"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: [])
|
||||
let result = try node.render(context)
|
||||
|
||||
try expect(result) == "childString=child\nbaseString=base\nbaseInt=1\n"
|
||||
}
|
||||
|
||||
$0.it("can iterate in range of variables") {
|
||||
let template: Template = "{% for i in 1...j %}{{ i }}{% endfor %}"
|
||||
try expect(try template.render(Context(dictionary: ["j": 3]))) == "123"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -253,5 +253,39 @@ func testIfNode() {
|
||||
let result = try renderNodes(nodes, Context(dictionary: ["value": "test"]))
|
||||
try expect(result) == "true"
|
||||
}
|
||||
|
||||
$0.it("evaluates nil properties as false") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if instance.value"),
|
||||
.text(value: "true"),
|
||||
.block(value: "endif")
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
|
||||
struct SomeType {
|
||||
let value: String? = nil
|
||||
}
|
||||
let result = try renderNodes(nodes, Context(dictionary: ["instance": SomeType()]))
|
||||
try expect(result) == ""
|
||||
}
|
||||
|
||||
$0.it("supports closed range variables") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value in 1...3"),
|
||||
.text(value: "true"),
|
||||
.block(value: "else"),
|
||||
.text(value: "false"),
|
||||
.block(value: "endif")
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
|
||||
try expect(renderNodes(nodes, Context(dictionary: ["value": 3]))) == "true"
|
||||
try expect(renderNodes(nodes, Context(dictionary: ["value": 4]))) == "false"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,17 +11,17 @@ func testInheritence() {
|
||||
|
||||
$0.it("can inherit from another template") {
|
||||
let template = try environment.loadTemplate(name: "child.html")
|
||||
try expect(try template.render()) == "Header\nChild"
|
||||
try expect(try template.render()) == "Super_Header Child_Header\nChild_Body"
|
||||
}
|
||||
|
||||
$0.it("can inherit from another template inheriting from another template") {
|
||||
let template = try environment.loadTemplate(name: "child-child.html")
|
||||
try expect(try template.render()) == "Child Child Header\nChild"
|
||||
try expect(try template.render()) == "Super_Header Child_Header Child_Child_Header\nChild_Body"
|
||||
}
|
||||
|
||||
$0.it("can inherit from a template that calls a super block") {
|
||||
let template = try environment.loadTemplate(name: "child-super.html")
|
||||
try expect(try template.render()) == "Header\nChild Body"
|
||||
try expect(try template.render()) == "Header\nChild_Body"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ func testLexer() {
|
||||
let lexer = Lexer(templateString: "{# Comment #}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == (1)
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .comment(value: "Comment")
|
||||
}
|
||||
|
||||
@@ -28,6 +28,14 @@ func testLexer() {
|
||||
try expect(tokens.first) == .variable(value: "Variable")
|
||||
}
|
||||
|
||||
$0.it("can tokenize unclosed tag by ignoring it") {
|
||||
let lexer = Lexer(templateString: "{{ thing")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .text(value: "")
|
||||
}
|
||||
|
||||
$0.it("can tokenize a mixture of content") {
|
||||
let lexer = Lexer(templateString: "My name is {{ name }}.")
|
||||
let tokens = lexer.tokenize()
|
||||
@@ -46,5 +54,37 @@ func testLexer() {
|
||||
try expect(tokens[0]) == Token.variable(value: "thing")
|
||||
try expect(tokens[1]) == Token.variable(value: "name")
|
||||
}
|
||||
|
||||
$0.it("can tokenize an unclosed block") {
|
||||
let lexer = Lexer(templateString: "{%}")
|
||||
let _ = lexer.tokenize()
|
||||
}
|
||||
|
||||
$0.it("can tokenize an empty variable") {
|
||||
let lexer = Lexer(templateString: "{{}}")
|
||||
let _ = lexer.tokenize()
|
||||
}
|
||||
|
||||
$0.it("can tokenize with new lines") {
|
||||
let lexer = Lexer(templateString:
|
||||
"My name is {%\n" +
|
||||
" if name\n" +
|
||||
" and\n" +
|
||||
" name\n" +
|
||||
"%}{{\n" +
|
||||
"name\n" +
|
||||
"}}{%\n" +
|
||||
"endif %}.")
|
||||
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 5
|
||||
try expect(tokens[0]) == Token.text(value: "My name is ")
|
||||
try expect(tokens[1]) == Token.block(value: "if name and name")
|
||||
try expect(tokens[2]) == Token.variable(value: "name")
|
||||
try expect(tokens[3]) == Token.block(value: "endif")
|
||||
try expect(tokens[4]) == Token.text(value: ".")
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,4 +29,27 @@ func testTemplateLoader() {
|
||||
try expect(try environment.loadTemplate(name: "../LoaderSpec.swift")).toThrow()
|
||||
}
|
||||
}
|
||||
|
||||
describe("DictionaryLoader") {
|
||||
let loader = DictionaryLoader(templates: [
|
||||
"index.html": "Hello World"
|
||||
])
|
||||
let environment = Environment(loader: loader)
|
||||
|
||||
$0.it("errors when a template cannot be found") {
|
||||
try expect(try environment.loadTemplate(name: "unknown.html")).toThrow()
|
||||
}
|
||||
|
||||
$0.it("errors when an array of templates cannot be found") {
|
||||
try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow()
|
||||
}
|
||||
|
||||
$0.it("can load a template from a known templates") {
|
||||
_ = try environment.loadTemplate(name: "index.html")
|
||||
}
|
||||
|
||||
$0.it("can load a known template from a collection of templates") {
|
||||
_ = try environment.loadTemplate(names: ["unknown.html", "index.html"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,11 @@ import Spectre
|
||||
|
||||
|
||||
#if os(OSX)
|
||||
@objc class Object : NSObject {
|
||||
let title = "Hello World"
|
||||
@objc class Superclass: NSObject {
|
||||
@objc let name = "Foo"
|
||||
}
|
||||
@objc class Object : Superclass {
|
||||
@objc let title = "Hello World"
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -17,6 +20,14 @@ fileprivate struct Article {
|
||||
let author: Person
|
||||
}
|
||||
|
||||
fileprivate class WebSite {
|
||||
let url: String = "blog.com"
|
||||
}
|
||||
|
||||
fileprivate class Blog: WebSite {
|
||||
let articles: [Article] = [Article(author: Person(name: "Kyle"))]
|
||||
let featuring: Article? = Article(author: Person(name: "Jhon"))
|
||||
}
|
||||
|
||||
func testVariable() {
|
||||
describe("Variable") {
|
||||
@@ -26,12 +37,17 @@ func testVariable() {
|
||||
"profiles": [
|
||||
"github": "kylef",
|
||||
],
|
||||
"article": Article(author: Person(name: "Kyle"))
|
||||
"counter": [
|
||||
"count": "kylef",
|
||||
],
|
||||
"article": Article(author: Person(name: "Kyle")),
|
||||
"tuple": (one: 1, two: 2)
|
||||
])
|
||||
|
||||
#if os(OSX)
|
||||
context["object"] = Object()
|
||||
#endif
|
||||
context["blog"] = Blog()
|
||||
|
||||
$0.it("can resolve a string literal with double quotes") {
|
||||
let variable = Variable("\"name\"")
|
||||
@@ -47,7 +63,7 @@ func testVariable() {
|
||||
|
||||
$0.it("can resolve an integer literal") {
|
||||
let variable = Variable("5")
|
||||
let result = try variable.resolve(context) as? Number
|
||||
let result = try variable.resolve(context) as? Int
|
||||
try expect(result) == 5
|
||||
}
|
||||
|
||||
@@ -57,6 +73,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
|
||||
@@ -107,12 +130,111 @@ func testVariable() {
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("can get the count of a dictionary") {
|
||||
let variable = Variable("profiles.count")
|
||||
let result = try variable.resolve(context) as? Int
|
||||
try expect(result) == 1
|
||||
}
|
||||
|
||||
#if os(OSX)
|
||||
$0.it("can resolve a value via KVO") {
|
||||
let variable = Variable("object.title")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can resolve a superclass value via KVO") {
|
||||
let variable = Variable("object.name")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Foo"
|
||||
}
|
||||
#endif
|
||||
|
||||
$0.it("can resolve a value via reflection") {
|
||||
let variable = Variable("blog.articles.0.author.name")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("can resolve a superclass value via reflection") {
|
||||
let variable = Variable("blog.url")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "blog.com"
|
||||
}
|
||||
|
||||
$0.it("can resolve optional variable property using reflection") {
|
||||
let variable = Variable("blog.featuring.author.name")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Jhon"
|
||||
}
|
||||
|
||||
$0.it("does not render Optional") {
|
||||
var array: [Any?] = [1, nil]
|
||||
array.append(array)
|
||||
let context = Context(dictionary: ["values": array])
|
||||
|
||||
try expect(VariableNode(variable: "values").render(context)) == "[1, nil, [1, nil]]"
|
||||
try expect(VariableNode(variable: "values.1").render(context)) == ""
|
||||
}
|
||||
|
||||
$0.it("can subscript tuple by index") {
|
||||
let variable = Variable("tuple.0")
|
||||
let result = try variable.resolve(context) as? Int
|
||||
try expect(result) == 1
|
||||
}
|
||||
|
||||
$0.it("can subscript tuple by label") {
|
||||
let variable = Variable("tuple.two")
|
||||
let result = try variable.resolve(context) as? Int
|
||||
try expect(result) == 2
|
||||
}
|
||||
}
|
||||
|
||||
describe("RangeVariable") {
|
||||
|
||||
let context: Context = {
|
||||
let ext = Extension()
|
||||
ext.registerFilter("incr", filter: { (arg: Any?) in toNumber(value: arg!)! + 1 })
|
||||
let environment = Environment(extensions: [ext])
|
||||
return Context(dictionary: [:], environment: environment)
|
||||
}()
|
||||
|
||||
func makeVariable(_ token: String) throws -> RangeVariable? {
|
||||
return try RangeVariable(token, parser: TokenParser(tokens: [], environment: context.environment))
|
||||
}
|
||||
|
||||
$0.it("can resolve closed range as array") {
|
||||
let result = try makeVariable("1...3")?.resolve(context) as? [Int]
|
||||
try expect(result) == [1, 2, 3]
|
||||
}
|
||||
|
||||
$0.it("can resolve decreasing closed range as reversed array") {
|
||||
let result = try makeVariable("3...1")?.resolve(context) as? [Int]
|
||||
try expect(result) == [3, 2, 1]
|
||||
}
|
||||
|
||||
$0.it("can use filter on range variables") {
|
||||
let result = try makeVariable("1|incr...3|incr")?.resolve(context) as? [Int]
|
||||
try expect(result) == [2, 3, 4]
|
||||
}
|
||||
|
||||
$0.it("throws when left value is not int") {
|
||||
let template: Template = "{% for i in k...j %}{{ i }}{% endfor %}"
|
||||
try expect(try template.render(Context(dictionary: ["j": 3, "k": "1"]))).toThrow()
|
||||
}
|
||||
|
||||
$0.it("throws when right value is not int") {
|
||||
let variable = try makeVariable("k...j")
|
||||
try expect(try variable?.resolve(Context(dictionary: ["j": "3", "k": 1]))).toThrow()
|
||||
}
|
||||
|
||||
$0.it("throws is left range value is missing") {
|
||||
try expect(makeVariable("...1")).toThrow()
|
||||
}
|
||||
|
||||
$0.it("throws is right range value is missing") {
|
||||
try expect(makeVariable("1...")).toThrow()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
{% extends "child.html" %}
|
||||
{% block header %}Child Child Header{% endblock %}
|
||||
{% block header %}{{ block.super }} Child_Child_Header{% endblock %}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{% extends "base.html" %}
|
||||
{% block body %}Child {{ block.super }}{% endblock %}
|
||||
{% block body %}Child_{{ block.super }}{% endblock %}
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
{% extends "base.html" %}
|
||||
{% block body %}Child{% endblock %}
|
||||
{% block header %}Super_{{ block.super }} Child_Header{% endblock %}
|
||||
{% block body %}Child_Body{% endblock %}
|
||||
|
||||
10
docs/api.rst
10
docs/api.rst
@@ -73,6 +73,16 @@ on the file system.
|
||||
FileSystemLoader(bundle: [Bundle.main])
|
||||
|
||||
|
||||
DictionaryLoader
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Loads templates from a dictionary.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
DictionaryLoader(templates: ["index.html": "Hello World"])
|
||||
|
||||
|
||||
Custom Loaders
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -57,7 +57,25 @@ The for block sets a few variables available within the loop:
|
||||
|
||||
- ``first`` - True if this is the first time through the loop
|
||||
- ``last`` - True if this is the last time through the loop
|
||||
- ``counter`` - The current iteration of the loop
|
||||
- ``counter`` - The current iteration of the loop (1 indexed)
|
||||
- ``counter0`` - The current iteration of the loop (0 indexed)
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% for user in users %}
|
||||
{% if forloop.first %}
|
||||
This is the first user.
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% for user in users %}
|
||||
This is user number {{ forloop.counter }} user.
|
||||
{% endfor %}
|
||||
|
||||
|
||||
``if``
|
||||
~~~~~~
|
||||
|
||||
@@ -42,7 +42,7 @@ Registering custom filters with arguments:
|
||||
}
|
||||
|
||||
if let value = value as? Int {
|
||||
return value * 2
|
||||
return value * amount
|
||||
}
|
||||
|
||||
return value
|
||||
|
||||
@@ -17,7 +17,7 @@ the ``renderTemplate`` convinience method.
|
||||
let environment = Environment()
|
||||
|
||||
let context = ["name": "kyle"]
|
||||
try template.renderTemplate(string: "Hello {{ name }}", context: context)
|
||||
try environment.renderTemplate(string: "Hello {{ name }}", context: context)
|
||||
|
||||
Template Loaders
|
||||
----------------
|
||||
@@ -34,4 +34,4 @@ For example, to render a template called ``index.html`` inside the
|
||||
let environment = Environment(loader: fsLoader)
|
||||
|
||||
let context = ["name": "kyle"]
|
||||
try template.renderTemplate(name: "index.html", context: context)
|
||||
try environment.renderTemplate(name: "index.html", context: context)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Installation
|
||||
============
|
||||
|
||||
Swift Package Mangaer
|
||||
Swift Package Manager
|
||||
---------------------
|
||||
|
||||
If you're using the Swift Package Manager, you can add ``Stencil`` to your
|
||||
|
||||
Reference in New Issue
Block a user