Compare commits
47 Commits
0.4.0
...
0.6.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ffc888ba4 | ||
|
|
3c21975b97 | ||
|
|
df9065f5a8 | ||
|
|
05b71736aa | ||
|
|
aa1399be55 | ||
|
|
bdc14ab1e1 | ||
|
|
67d4c52535 | ||
|
|
48026cde2c | ||
|
|
dc4b965aaa | ||
|
|
2190afee0d | ||
|
|
9b7e6ba7ed | ||
|
|
bf0989d329 | ||
|
|
affd56ec99 | ||
|
|
070a82cb2d | ||
|
|
3ec009381d | ||
|
|
6deb93ac19 | ||
|
|
b4ba12bbde | ||
|
|
19d712b4a4 | ||
|
|
201b8e263c | ||
|
|
03928721c4 | ||
|
|
07835063ed | ||
|
|
3c13d81b21 | ||
|
|
1668830d9b | ||
|
|
14195b3199 | ||
|
|
ae75ea5911 | ||
|
|
9c9ebbe559 | ||
|
|
5cdf1d326b | ||
|
|
f78562a1fd | ||
|
|
0ccd8809e0 | ||
|
|
356393088b | ||
|
|
b792cd09b9 | ||
|
|
372b2e7576 | ||
|
|
0bfd4134f9 | ||
|
|
aca0a3181d | ||
|
|
a1a268d5ac | ||
|
|
465834d89c | ||
|
|
0af879ba8a | ||
|
|
a516de51ff | ||
|
|
1f4aae1859 | ||
|
|
cba1cbe388 | ||
|
|
3722998c35 | ||
|
|
22919dc5ce | ||
|
|
89b7da2e10 | ||
|
|
3bd3aec296 | ||
|
|
48a9a65bd5 | ||
|
|
c86ab9c5b9 | ||
|
|
dc774fe43b |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
||||
.conche/
|
||||
.build/
|
||||
Packages/
|
||||
|
||||
1
.swift-version
Normal file
1
.swift-version
Normal file
@@ -0,0 +1 @@
|
||||
DEVELOPMENT-SNAPSHOT-2016-01-25-a
|
||||
11
.travis.yml
Normal file
11
.travis.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
os:
|
||||
- osx
|
||||
- linux
|
||||
language: generic
|
||||
sudo: required
|
||||
dist: trusty
|
||||
osx_image: xcode7.2
|
||||
install:
|
||||
- eval "$(curl -sL https://gist.githubusercontent.com/kylef/5c0475ff02b7c7671d2a/raw/02090c7ede5a637b76e6df1710e83cd0bbe7dcdf/swiftenv-install.sh)"
|
||||
script:
|
||||
- make test
|
||||
@@ -96,7 +96,7 @@ When the `ForNode` is rendered in a context, it will look up the variable `artic
|
||||
|
||||
There are two ways to register custom template tags. A simple way which allows you to map 1:1 a block token to a Node. You can also register a more advanced template tag which has it’s own block of code for handling parsing if you want to parse up until another token such as if you are trying to provide flow-control.
|
||||
|
||||
The tags are registered onto the `TokenParser` which you can access from your `Template`.
|
||||
The tags are registered with a `Namespace` passed when rendering your `Template`.
|
||||
|
||||
#### Simple Tags
|
||||
|
||||
@@ -105,7 +105,7 @@ A simple tag is registered with a string for the tag name and a block of code wh
|
||||
Here’s an example. Registering a template tag called `custom` which just renders `Hello World` in the rendered template:
|
||||
|
||||
```swift
|
||||
parser.registerSimpleTag("custom") { context in
|
||||
namespace.registerSimpleTag("custom") { context in
|
||||
return "Hello World"
|
||||
}
|
||||
```
|
||||
@@ -120,7 +120,7 @@ You would use it as such in a template:
|
||||
|
||||
If you need more control or functionality than the simple tag’s above, you can use the node based API where you can provide a block of code to deal with parsing. There are a few examples of this in use over at `Node.swift` inside Stencil. There is an implementation of `if` and `for` template tags.
|
||||
|
||||
You would register a template tag using the `registerTag` API inside a `TokenParser` which accepts a name for the tag and a block of code to handle parsing. The block of code is invoked with the parser and the current token as an argument. This allows you to use the API on `TokenParser` to parse node’s further in the token array.
|
||||
You would register a template tag using the `registerTag` API inside a `Namespace` which accepts a name for the tag and a block of code to handle parsing. The block of code is invoked with the parser and the current token as an argument. This allows you to use the API on `TokenParser` to parse node’s further in the token array.
|
||||
|
||||
As an example, we’re going to create a template tag called `debug` which will optionally render nodes from `debug` up until `enddebug`. When rendering the `DebugNode`, it will only render the nodes inside if a variable called `debug` is set to `true` inside the template Context.
|
||||
|
||||
@@ -163,7 +163,7 @@ class DebugNode : Node {
|
||||
We will need to write a parser to parse up until the `enddebug` template block and create a `DebugNode` with the nodes in-between. If there was another error form another Node inside, then we will return that error.
|
||||
|
||||
```swift
|
||||
parser.registerTag("debug") { parser, token in
|
||||
namespace.registerTag("debug") { parser, token in
|
||||
// Use the parser to parse every token up until the `enddebug` block.
|
||||
let nodes = try until(["enddebug"]))
|
||||
return DebugNode(nodes)
|
||||
|
||||
7
Makefile
Normal file
7
Makefile
Normal file
@@ -0,0 +1,7 @@
|
||||
stencil:
|
||||
@echo "Building Stencil"
|
||||
@swift build
|
||||
|
||||
test: stencil
|
||||
@echo "Running Tests"
|
||||
@.build/debug/spectre-build
|
||||
11
Package.swift
Normal file
11
Package.swift
Normal file
@@ -0,0 +1,11 @@
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Stencil",
|
||||
dependencies: [
|
||||
.Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 6),
|
||||
],
|
||||
testDependencies: [
|
||||
.Package(url: "https://github.com/kylef/spectre-build", majorVersion: 0),
|
||||
]
|
||||
)
|
||||
56
README.md
56
README.md
@@ -1,6 +1,6 @@
|
||||
# Stencil
|
||||
|
||||
[](https://circleci.com/gh/kylef/Stencil)
|
||||
[](https://travis-ci.org/kylef/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
|
||||
@@ -25,8 +25,8 @@ let context = Context(dictionary: [
|
||||
])
|
||||
|
||||
do {
|
||||
let template = Template(named: "template.stencil")
|
||||
let rendered = template.render(context)
|
||||
let template = try Template(named: "template.stencil")
|
||||
let rendered = try template.render(context)
|
||||
print(rendered)
|
||||
} catch {
|
||||
print("Failed to render template \(error)")
|
||||
@@ -73,8 +73,8 @@ following lookup:
|
||||
For example, if `people` was an array:
|
||||
|
||||
```html+django
|
||||
There are {{ people.count }} people, {{ people.first }} is first person.
|
||||
Followed by {{ people.1 }}.
|
||||
There are {{ people.count }} people. {{ people.first }} is the first person,
|
||||
followed by {{ people.1 }}.
|
||||
```
|
||||
|
||||
#### Filters
|
||||
@@ -112,18 +112,6 @@ For example, `Stencil` to `stencil`.
|
||||
{{ "Stencil"|lowercase }}
|
||||
```
|
||||
|
||||
#### Registering custom filters
|
||||
|
||||
```swift
|
||||
template.parser.registerFilter("double") { value in
|
||||
if let value = value as? Int {
|
||||
return value * 2
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
```
|
||||
|
||||
### Tags
|
||||
|
||||
Tags are a mechanism to execute a piece of code, allowing you to have
|
||||
@@ -154,7 +142,7 @@ A for loop allows you to iterate over an array found by variable lookup.
|
||||
{% for item in items %}
|
||||
{{ item }}
|
||||
{% empty %}
|
||||
There we're no items.
|
||||
There were no items.
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
@@ -194,13 +182,37 @@ let context = Context(dictionary: [
|
||||
])
|
||||
```
|
||||
|
||||
### Customisation
|
||||
|
||||
You can build your own custom filters and tags and pass them down while
|
||||
rendering your template. Any custom filters or tags must be registered
|
||||
with a namespace which contains all filters and tags available to the template.
|
||||
|
||||
```swift
|
||||
let namespace = Namespace()
|
||||
// Register your filters and tags with the namespace
|
||||
let rendered = try template.render(context, namespace: namespace)
|
||||
```
|
||||
|
||||
#### Registering custom filters
|
||||
|
||||
```swift
|
||||
namespace.registerFilter("double") { value in
|
||||
if let value = value as? Int {
|
||||
return value * 2
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
```
|
||||
|
||||
#### Building custom tags
|
||||
|
||||
You can build a custom template tag. There are a couple of APIs to allow
|
||||
you to write your own custom tags. The following is the simplest form:
|
||||
|
||||
```swift
|
||||
template.parser.registerSimpleTag("custom") { context in
|
||||
namespace.registerSimpleTag("custom") { context in
|
||||
return "Hello World"
|
||||
}
|
||||
```
|
||||
@@ -214,9 +226,8 @@ of template tags. You will need to call the `registerTag` API which accepts a
|
||||
closure to handle the parsing. You can find examples of the `now`, `if` and
|
||||
`for` tags found inside `Node.swift`.
|
||||
|
||||
Custom template tags must be registered prior to calling `Template.render` the first time.
|
||||
|
||||
The architecture of Stencil along with how to build advanced plugins can be found in the [architecture](ARCHITECTURE.md) document.
|
||||
The architecture of Stencil along with how to build advanced plugins can be
|
||||
found in the [architecture](ARCHITECTURE.md) document.
|
||||
|
||||
### Comments
|
||||
|
||||
@@ -230,4 +241,3 @@ To comment out part of your template, you can use the following syntax:
|
||||
|
||||
Stencil is licensed under the BSD license. See [LICENSE](LICENSE) for more
|
||||
info.
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
/// A container for template variables.
|
||||
public class Context {
|
||||
var dictionaries:[[String: Any]]
|
||||
var dictionaries: [[String: Any]]
|
||||
let namespace: Namespace
|
||||
|
||||
/// Initialise a Context with a dictionary
|
||||
public init(dictionary:[String: Any]) {
|
||||
dictionaries = [dictionary]
|
||||
}
|
||||
/// Initialise a Context with an optional dictionary and optional namespace
|
||||
public init(dictionary: [String: Any]? = nil, namespace: Namespace = Namespace()) {
|
||||
if let dictionary = dictionary {
|
||||
dictionaries = [dictionary]
|
||||
} else {
|
||||
dictionaries = []
|
||||
}
|
||||
|
||||
/// Initialise an empty Context
|
||||
public init() {
|
||||
dictionaries = []
|
||||
self.namespace = namespace
|
||||
}
|
||||
|
||||
public subscript(key: String) -> Any? {
|
||||
@@ -26,28 +28,28 @@ public class Context {
|
||||
|
||||
/// Set a variable in the current context, deleting the variable if it's nil
|
||||
set(value) {
|
||||
if var dictionary = dictionaries.popLast() {
|
||||
dictionary[key] = value
|
||||
dictionaries.append(dictionary)
|
||||
if let dictionary = dictionaries.popLast() {
|
||||
var mutable_dictionary = dictionary
|
||||
mutable_dictionary[key] = value
|
||||
dictionaries.append(mutable_dictionary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a new level into the Context
|
||||
public func push(dictionary: [String: Any]? = nil) {
|
||||
private func push(dictionary: [String: Any]? = nil) {
|
||||
dictionaries.append(dictionary ?? [:])
|
||||
}
|
||||
|
||||
/// Pop the last level off of the Context
|
||||
public func pop() -> [String: Any]? {
|
||||
private func pop() -> [String: Any]? {
|
||||
return dictionaries.popLast()
|
||||
}
|
||||
|
||||
/// Push a new level onto the context for the duration of the execution of the given closure
|
||||
public func push<Result>(dictionary: [String: Any]? = nil, @noescape closure: (() throws -> Result)) rethrows -> Result {
|
||||
push(dictionary)
|
||||
let result = try closure()
|
||||
pop()
|
||||
return result
|
||||
defer { pop() }
|
||||
return try closure()
|
||||
}
|
||||
}
|
||||
62
Sources/ForTag.swift
Normal file
62
Sources/ForTag.swift
Normal file
@@ -0,0 +1,62 @@
|
||||
public class ForNode : NodeType {
|
||||
let variable:Variable
|
||||
let loopVariable:String
|
||||
let nodes:[NodeType]
|
||||
let emptyNodes: [NodeType]
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
|
||||
guard components.count == 4 && components[2] == "in" else {
|
||||
throw TemplateSyntaxError("'for' statements should use the following 'for x in y' `\(token.contents)`.")
|
||||
}
|
||||
|
||||
let loopVariable = components[1]
|
||||
let variable = components[3]
|
||||
|
||||
var emptyNodes = [NodeType]()
|
||||
|
||||
let forNodes = try parser.parse(until(["endfor", "empty"]))
|
||||
|
||||
guard let token = parser.nextToken() else {
|
||||
throw TemplateSyntaxError("`endfor` was not found.")
|
||||
}
|
||||
|
||||
if token.contents == "empty" {
|
||||
emptyNodes = try parser.parse(until(["endfor"]))
|
||||
parser.nextToken()
|
||||
}
|
||||
|
||||
return ForNode(variable: variable, loopVariable: loopVariable, nodes: forNodes, emptyNodes:emptyNodes)
|
||||
}
|
||||
|
||||
public init(variable:String, loopVariable:String, nodes:[NodeType], emptyNodes:[NodeType]) {
|
||||
self.variable = Variable(variable)
|
||||
self.loopVariable = loopVariable
|
||||
self.nodes = nodes
|
||||
self.emptyNodes = emptyNodes
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
let values = try variable.resolve(context)
|
||||
|
||||
if let values = values as? [Any] where values.count > 0 {
|
||||
let count = values.count
|
||||
return try values.enumerate().map { index, item in
|
||||
let forContext: [String: Any] = [
|
||||
"first": index == 0,
|
||||
"last": index == (count - 1),
|
||||
"counter": index + 1,
|
||||
]
|
||||
|
||||
return try context.push([loopVariable: item, "forloop": forContext]) {
|
||||
try renderNodes(nodes, context)
|
||||
}
|
||||
}.joinWithSeparator("")
|
||||
}
|
||||
|
||||
return try context.push {
|
||||
try renderNodes(emptyNodes, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
78
Sources/IfTag.swift
Normal file
78
Sources/IfTag.swift
Normal file
@@ -0,0 +1,78 @@
|
||||
public class IfNode : NodeType {
|
||||
public let variable:Variable
|
||||
public let trueNodes:[NodeType]
|
||||
public let falseNodes:[NodeType]
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
guard components.count == 2 else {
|
||||
throw TemplateSyntaxError("'if' statements should use the following 'if condition' `\(token.contents)`.")
|
||||
}
|
||||
let variable = components[1]
|
||||
var trueNodes = [NodeType]()
|
||||
var falseNodes = [NodeType]()
|
||||
|
||||
trueNodes = try parser.parse(until(["endif", "else"]))
|
||||
|
||||
guard let token = parser.nextToken() else {
|
||||
throw TemplateSyntaxError("`endif` was not found.")
|
||||
}
|
||||
|
||||
if token.contents == "else" {
|
||||
falseNodes = try parser.parse(until(["endif"]))
|
||||
parser.nextToken()
|
||||
}
|
||||
|
||||
return IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)
|
||||
}
|
||||
|
||||
public class func parse_ifnot(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
guard components.count == 2 else {
|
||||
throw TemplateSyntaxError("'ifnot' statements should use the following 'if condition' `\(token.contents)`.")
|
||||
}
|
||||
let variable = components[1]
|
||||
var trueNodes = [NodeType]()
|
||||
var falseNodes = [NodeType]()
|
||||
|
||||
falseNodes = try parser.parse(until(["endif", "else"]))
|
||||
|
||||
guard let token = parser.nextToken() else {
|
||||
throw TemplateSyntaxError("`endif` was not found.")
|
||||
}
|
||||
|
||||
if token.contents == "else" {
|
||||
trueNodes = try parser.parse(until(["endif"]))
|
||||
parser.nextToken()
|
||||
}
|
||||
|
||||
return IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)
|
||||
}
|
||||
|
||||
public init(variable:String, trueNodes:[NodeType], falseNodes:[NodeType]) {
|
||||
self.variable = Variable(variable)
|
||||
self.trueNodes = trueNodes
|
||||
self.falseNodes = falseNodes
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
let result = try variable.resolve(context)
|
||||
var truthy = false
|
||||
|
||||
if let result = result as? [Any] {
|
||||
truthy = !result.isEmpty
|
||||
} else if let result = result as? [String:Any] {
|
||||
truthy = !result.isEmpty
|
||||
} else if result != nil {
|
||||
truthy = true
|
||||
}
|
||||
|
||||
return try context.push {
|
||||
if truthy {
|
||||
return try renderNodes(trueNodes, context)
|
||||
} else {
|
||||
return try renderNodes(falseNodes, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
|
||||
public class IncludeNode : NodeType {
|
||||
public let templateName:String
|
||||
public let templateName: Variable
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let bits = token.contents.componentsSeparatedByString("\"")
|
||||
public class func parse(parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
|
||||
guard bits.count == 3 else {
|
||||
guard bits.count == 2 else {
|
||||
throw TemplateSyntaxError("'include' tag takes one argument, the template file to be included")
|
||||
}
|
||||
|
||||
return IncludeNode(templateName: bits[1])
|
||||
return IncludeNode(templateName: Variable(bits[1]))
|
||||
}
|
||||
|
||||
public init(templateName:String) {
|
||||
public init(templateName: Variable) {
|
||||
self.templateName = templateName
|
||||
}
|
||||
|
||||
@@ -24,6 +23,10 @@ public class IncludeNode : NodeType {
|
||||
throw TemplateSyntaxError("Template loader not in context")
|
||||
}
|
||||
|
||||
guard let templateName = try self.templateName.resolve(context) as? String else {
|
||||
throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string")
|
||||
}
|
||||
|
||||
guard let template = loader.loadTemplate(templateName) else {
|
||||
let paths = loader.paths.map { $0.description }.joinWithSeparator(", ")
|
||||
throw TemplateSyntaxError("'\(templateName)' template not found in \(paths)")
|
||||
@@ -1,58 +1,60 @@
|
||||
import Foundation
|
||||
|
||||
class BlockContext {
|
||||
class var contextKey:String { return "block_context" }
|
||||
class var contextKey: String { return "block_context" }
|
||||
|
||||
var blocks:[String:BlockNode]
|
||||
var blocks: [String:BlockNode]
|
||||
|
||||
init(blocks:[String:BlockNode]) {
|
||||
init(blocks: [String:BlockNode]) {
|
||||
self.blocks = blocks
|
||||
}
|
||||
|
||||
func pop(blockName:String) -> BlockNode? {
|
||||
func pop(blockName: String) -> BlockNode? {
|
||||
return blocks.removeValueForKey(blockName)
|
||||
}
|
||||
}
|
||||
|
||||
func any<Element>(elements:[Element], closure:(Element -> Bool)) -> Element? {
|
||||
for element in elements {
|
||||
if closure(element) {
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
extension CollectionType {
|
||||
func any(closure: Generator.Element -> Bool) -> Generator.Element? {
|
||||
for element in self {
|
||||
if closure(element) {
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ExtendsNode : NodeType {
|
||||
let templateName:String
|
||||
let blocks:[String:BlockNode]
|
||||
let templateName: Variable
|
||||
let blocks: [String:BlockNode]
|
||||
|
||||
class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let bits = token.contents.componentsSeparatedByString("\"")
|
||||
class func parse(parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
|
||||
guard bits.count == 3 else {
|
||||
guard bits.count == 2 else {
|
||||
throw TemplateSyntaxError("'extends' takes one argument, the template file to be extended")
|
||||
}
|
||||
|
||||
let parsedNodes = try parser.parse()
|
||||
guard (any(parsedNodes) { $0 is ExtendsNode }) == nil else {
|
||||
guard (parsedNodes.any { $0 is ExtendsNode }) == nil else {
|
||||
throw TemplateSyntaxError("'extends' cannot appear more than once in the same template")
|
||||
}
|
||||
|
||||
let blockNodes = parsedNodes.filter { node in node is BlockNode }
|
||||
|
||||
let nodes = blockNodes.reduce([String:BlockNode](), combine: { (accumulator, node:NodeType) -> [String:BlockNode] in
|
||||
let nodes = blockNodes.reduce([String:BlockNode]()) { (accumulator, node:NodeType) -> [String:BlockNode] in
|
||||
let node = (node as! BlockNode)
|
||||
var dict = accumulator
|
||||
dict[node.name] = node
|
||||
return dict
|
||||
})
|
||||
}
|
||||
|
||||
return ExtendsNode(templateName: bits[1], blocks: nodes)
|
||||
return ExtendsNode(templateName: Variable(bits[1]), blocks: nodes)
|
||||
}
|
||||
|
||||
init(templateName:String, blocks:[String:BlockNode]) {
|
||||
init(templateName: Variable, blocks: [String: BlockNode]) {
|
||||
self.templateName = templateName
|
||||
self.blocks = blocks
|
||||
}
|
||||
@@ -62,24 +64,28 @@ class ExtendsNode : NodeType {
|
||||
throw TemplateSyntaxError("Template loader not in context")
|
||||
}
|
||||
|
||||
guard let templateName = try self.templateName.resolve(context) as? String else {
|
||||
throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string")
|
||||
}
|
||||
|
||||
guard let template = loader.loadTemplate(templateName) else {
|
||||
let paths:String = loader.paths.map { $0.description }.joinWithSeparator(", ")
|
||||
throw TemplateSyntaxError("'\(templateName)' template not found in \(paths)")
|
||||
}
|
||||
|
||||
let blockContext = BlockContext(blocks: blocks)
|
||||
context.push([BlockContext.contextKey: blockContext])
|
||||
let result = try template.render(context)
|
||||
context.pop()
|
||||
return result
|
||||
return try context.push([BlockContext.contextKey: blockContext]) {
|
||||
return try template.render(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BlockNode : NodeType {
|
||||
let name:String
|
||||
let nodes:[NodeType]
|
||||
|
||||
class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
class BlockNode : NodeType {
|
||||
let name: String
|
||||
let nodes: [NodeType]
|
||||
|
||||
class func parse(parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
|
||||
guard bits.count == 2 else {
|
||||
@@ -92,7 +98,7 @@ class BlockNode : NodeType {
|
||||
return BlockNode(name:blockName, nodes:nodes)
|
||||
}
|
||||
|
||||
init(name:String, nodes:[NodeType]) {
|
||||
init(name: String, nodes: [NodeType]) {
|
||||
self.name = name
|
||||
self.nodes = nodes
|
||||
}
|
||||
147
Sources/Lexer.swift
Normal file
147
Sources/Lexer.swift
Normal file
@@ -0,0 +1,147 @@
|
||||
public struct Lexer {
|
||||
public let templateString: String
|
||||
|
||||
public init(templateString: String) {
|
||||
self.templateString = templateString
|
||||
}
|
||||
|
||||
func createToken(string:String) -> Token {
|
||||
func strip() -> String {
|
||||
return string[string.startIndex.successor().successor()..<string.endIndex.predecessor().predecessor()].trim(" ")
|
||||
}
|
||||
|
||||
if string.hasPrefix("{{") {
|
||||
return Token.Variable(value: strip())
|
||||
} else if string.hasPrefix("{%") {
|
||||
return Token.Block(value: strip())
|
||||
} else if string.hasPrefix("{#") {
|
||||
return Token.Comment(value: strip())
|
||||
}
|
||||
|
||||
return Token.Text(value: string)
|
||||
}
|
||||
|
||||
/// Returns an array of tokens from a given template string.
|
||||
public func tokenize() -> [Token] {
|
||||
var tokens: [Token] = []
|
||||
|
||||
let scanner = Scanner(templateString)
|
||||
|
||||
let map = [
|
||||
"{{": "}}",
|
||||
"{%": "%}",
|
||||
"{#": "#}",
|
||||
]
|
||||
|
||||
while !scanner.isEmpty {
|
||||
if let text = scanner.scan(until: ["{{", "{%", "{#"]) {
|
||||
if !text.1.isEmpty {
|
||||
tokens.append(createToken(text.1))
|
||||
}
|
||||
|
||||
let end = map[text.0]!
|
||||
let result = scanner.scan(until: end, returnUntil: true)
|
||||
tokens.append(createToken(result))
|
||||
} else {
|
||||
tokens.append(createToken(scanner.content))
|
||||
scanner.content = ""
|
||||
}
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Scanner {
|
||||
var content: String
|
||||
|
||||
init(_ content: String) {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var isEmpty: Bool {
|
||||
return content.isEmpty
|
||||
}
|
||||
|
||||
func scan(until until: String, returnUntil: Bool = false) -> String {
|
||||
if until.isEmpty {
|
||||
return ""
|
||||
}
|
||||
|
||||
var index = content.startIndex
|
||||
while index != content.endIndex {
|
||||
let substring = content[index..<content.endIndex]
|
||||
if substring.hasPrefix(until) {
|
||||
let result = content[content.startIndex..<index]
|
||||
content = substring
|
||||
|
||||
if returnUntil {
|
||||
content = content[until.endIndex..<content.endIndex]
|
||||
return result + until
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
index = index.successor()
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func scan(until until: [String]) -> (String, String)? {
|
||||
if until.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
var index = content.startIndex
|
||||
while index != content.endIndex {
|
||||
let substring = content[index..<content.endIndex]
|
||||
for string in until {
|
||||
if substring.hasPrefix(string) {
|
||||
let result = content[content.startIndex..<index]
|
||||
content = substring
|
||||
return (string, result)
|
||||
}
|
||||
}
|
||||
|
||||
index = index.successor()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension String {
|
||||
func findFirstNot(character: Character) -> String.Index? {
|
||||
var index = startIndex
|
||||
while index != endIndex {
|
||||
if character != self[index] {
|
||||
return index
|
||||
}
|
||||
index = index.successor()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findLastNot(character: Character) -> String.Index? {
|
||||
var index = endIndex.predecessor()
|
||||
while index != startIndex {
|
||||
if character != self[index] {
|
||||
return index.successor()
|
||||
}
|
||||
index = index.predecessor()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func trim(character: Character) -> String {
|
||||
let first = findFirstNot(character) ?? startIndex
|
||||
let last = findLastNot(character) ?? endIndex
|
||||
return self[first..<last]
|
||||
}
|
||||
}
|
||||
46
Sources/Namespace.swift
Normal file
46
Sources/Namespace.swift
Normal file
@@ -0,0 +1,46 @@
|
||||
public class Namespace {
|
||||
public typealias TagParser = (TokenParser, Token) throws -> NodeType
|
||||
|
||||
var tags = [String: TagParser]()
|
||||
var filters = [String: Filter]()
|
||||
|
||||
public init() {
|
||||
registerDefaultTags()
|
||||
registerDefaultFilters()
|
||||
}
|
||||
|
||||
private func registerDefaultTags() {
|
||||
registerTag("for", parser: ForNode.parse)
|
||||
registerTag("if", parser: IfNode.parse)
|
||||
registerTag("ifnot", parser: IfNode.parse_ifnot)
|
||||
#if !os(Linux)
|
||||
registerTag("now", parser: NowNode.parse)
|
||||
#endif
|
||||
registerTag("include", parser: IncludeNode.parse)
|
||||
registerTag("extends", parser: ExtendsNode.parse)
|
||||
registerTag("block", parser: BlockNode.parse)
|
||||
}
|
||||
|
||||
private func registerDefaultFilters() {
|
||||
registerFilter("capitalize", filter: capitalise)
|
||||
registerFilter("uppercase", filter: uppercase)
|
||||
registerFilter("lowercase", filter: lowercase)
|
||||
}
|
||||
|
||||
/// Registers a new template tag
|
||||
public func registerTag(name: String, parser: TagParser) {
|
||||
tags[name] = parser
|
||||
}
|
||||
|
||||
/// Registers a simple template tag with a name and a handler
|
||||
public func registerSimpleTag(name: String, handler: Context throws -> String) {
|
||||
registerTag(name, parser: { parser, token in
|
||||
return SimpleNode(handler: handler)
|
||||
})
|
||||
}
|
||||
|
||||
/// Registers a template filter with the given name
|
||||
public func registerFilter(name: String, filter: Filter) {
|
||||
filters[name] = filter
|
||||
}
|
||||
}
|
||||
84
Sources/Node.swift
Normal file
84
Sources/Node.swift
Normal file
@@ -0,0 +1,84 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
public struct TemplateSyntaxError : ErrorType, Equatable, CustomStringConvertible {
|
||||
public let description:String
|
||||
|
||||
public init(_ description:String) {
|
||||
self.description = description
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool {
|
||||
return lhs.description == rhs.description
|
||||
}
|
||||
|
||||
|
||||
public protocol NodeType {
|
||||
/// Render the node in the given context
|
||||
func render(context:Context) throws -> String
|
||||
}
|
||||
|
||||
|
||||
/// Render the collection of nodes in the given context
|
||||
public func renderNodes(nodes:[NodeType], _ context:Context) throws -> String {
|
||||
return try nodes.map { try $0.render(context) }.joinWithSeparator("")
|
||||
}
|
||||
|
||||
public class SimpleNode : NodeType {
|
||||
let handler:Context throws -> String
|
||||
|
||||
public init(handler:Context throws -> String) {
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
return try handler(context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class TextNode : NodeType {
|
||||
public let text:String
|
||||
|
||||
public init(text:String) {
|
||||
self.text = text
|
||||
}
|
||||
|
||||
public func render(context:Context) throws -> String {
|
||||
return self.text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public protocol Resolvable {
|
||||
func resolve(context: Context) throws -> Any?
|
||||
}
|
||||
|
||||
|
||||
public class VariableNode : NodeType {
|
||||
public let variable: Resolvable
|
||||
|
||||
public init(variable: Resolvable) {
|
||||
self.variable = variable
|
||||
}
|
||||
|
||||
public init(variable: String) {
|
||||
self.variable = Variable(variable)
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
let result = try variable.resolve(context)
|
||||
|
||||
if let result = result as? String {
|
||||
return result
|
||||
} else if let result = result as? CustomStringConvertible {
|
||||
return result.description
|
||||
} else if let result = result as? NSObject {
|
||||
return result.description
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
}
|
||||
43
Sources/NowTag.swift
Normal file
43
Sources/NowTag.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
#if !os(Linux)
|
||||
import Foundation
|
||||
|
||||
|
||||
public class NowNode : NodeType {
|
||||
public let format:Variable
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
var format:Variable?
|
||||
|
||||
let components = token.components()
|
||||
guard components.count <= 2 else {
|
||||
throw TemplateSyntaxError("'now' tags may only have one argument: the format string `\(token.contents)`.")
|
||||
}
|
||||
if components.count == 2 {
|
||||
format = Variable(components[1])
|
||||
}
|
||||
|
||||
return NowNode(format:format)
|
||||
}
|
||||
|
||||
public init(format:Variable?) {
|
||||
self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"")
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
let date = NSDate()
|
||||
let format = try self.format.resolve(context)
|
||||
var formatter:NSDateFormatter?
|
||||
|
||||
if let format = format as? NSDateFormatter {
|
||||
formatter = format
|
||||
} else if let format = format as? String {
|
||||
formatter = NSDateFormatter()
|
||||
formatter!.dateFormat = format
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
|
||||
return formatter!.stringFromDate(date)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,13 +1,15 @@
|
||||
public func until(tags:[String])(parser:TokenParser, token:Token) -> Bool {
|
||||
if let name = token.components().first {
|
||||
for tag in tags {
|
||||
if name == tag {
|
||||
return true
|
||||
public func until(tags: [String]) -> ((TokenParser, Token) -> Bool) {
|
||||
return { parser, token in
|
||||
if let name = token.components().first {
|
||||
for tag in tags {
|
||||
if name == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Filter = Any? throws -> Any?
|
||||
@@ -16,38 +18,12 @@ public typealias Filter = Any? throws -> Any?
|
||||
public class TokenParser {
|
||||
public typealias TagParser = (TokenParser, Token) throws -> NodeType
|
||||
|
||||
private var tokens:[Token]
|
||||
private var tags = [String:TagParser]()
|
||||
private var filters = [String: Filter]()
|
||||
private var tokens: [Token]
|
||||
private let namespace: Namespace
|
||||
|
||||
public init(tokens:[Token]) {
|
||||
public init(tokens: [Token], namespace: Namespace) {
|
||||
self.tokens = tokens
|
||||
registerTag("for", parser: ForNode.parse)
|
||||
registerTag("if", parser: IfNode.parse)
|
||||
registerTag("ifnot", parser: IfNode.parse_ifnot)
|
||||
registerTag("now", parser: NowNode.parse)
|
||||
registerTag("include", parser: IncludeNode.parse)
|
||||
registerTag("extends", parser: ExtendsNode.parse)
|
||||
registerTag("block", parser: BlockNode.parse)
|
||||
registerFilter("capitalize", filter: capitalise)
|
||||
registerFilter("uppercase", filter: uppercase)
|
||||
registerFilter("lowercase", filter: lowercase)
|
||||
}
|
||||
|
||||
/// Registers a new template tag
|
||||
public func registerTag(name:String, parser:TagParser) {
|
||||
tags[name] = parser
|
||||
}
|
||||
|
||||
/// Registers a simple template tag with a name and a handler
|
||||
public func registerSimpleTag(name:String, handler:(Context throws -> String)) {
|
||||
registerTag(name, parser: { parser, token in
|
||||
return SimpleNode(handler: handler)
|
||||
})
|
||||
}
|
||||
|
||||
public func registerFilter(name: String, filter: Filter) {
|
||||
filters[name] = filter
|
||||
self.namespace = namespace
|
||||
}
|
||||
|
||||
/// Parse the given tokens into nodes
|
||||
@@ -75,7 +51,7 @@ public class TokenParser {
|
||||
}
|
||||
|
||||
if let tag = tag {
|
||||
if let parser = self.tags[tag] {
|
||||
if let parser = namespace.tags[tag] {
|
||||
nodes.append(try parser(self, token))
|
||||
} else {
|
||||
throw TemplateSyntaxError("Unknown template tag '\(tag)'")
|
||||
@@ -102,7 +78,7 @@ public class TokenParser {
|
||||
}
|
||||
|
||||
public func findFilter(name: String) throws -> Filter {
|
||||
if let filter = filters[name] {
|
||||
if let filter = namespace.filters[name] {
|
||||
return filter
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
#if os(Linux)
|
||||
let NSFileNoSuchFileError = 4
|
||||
#endif
|
||||
|
||||
/// A class representing a template
|
||||
public class Template {
|
||||
public let parser:TokenParser
|
||||
private var nodes:[NodeType]? = nil
|
||||
let tokens: [Token]
|
||||
|
||||
/// Create a template with the given name inside the given bundle
|
||||
public convenience init(named:String, inBundle bundle:NSBundle? = nil) throws {
|
||||
@@ -29,16 +32,14 @@ public class Template {
|
||||
/// Create a template with a template string
|
||||
public init(templateString:String) {
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
parser = TokenParser(tokens: tokens)
|
||||
tokens = lexer.tokenize()
|
||||
}
|
||||
|
||||
/// Render the given template
|
||||
public func render(context:Context? = nil) throws -> String {
|
||||
if nodes == nil {
|
||||
nodes = try parser.parse()
|
||||
}
|
||||
|
||||
return try renderNodes(nodes!, context ?? Context())
|
||||
public func render(context: Context? = nil) throws -> String {
|
||||
let context = context ?? Context()
|
||||
let parser = TokenParser(tokens: tokens, namespace: context.namespace)
|
||||
let nodes = try parser.parse()
|
||||
return try renderNodes(nodes, context)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,26 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
|
||||
// A class for loading a template from disk
|
||||
public class TemplateLoader {
|
||||
public let paths:[Path]
|
||||
public let paths: [Path]
|
||||
|
||||
public init(paths:[Path]) {
|
||||
public init(paths: [Path]) {
|
||||
self.paths = paths
|
||||
}
|
||||
|
||||
public init(bundle:[NSBundle]) {
|
||||
public init(bundle: [NSBundle]) {
|
||||
self.paths = bundle.map {
|
||||
return Path($0.bundlePath)
|
||||
}
|
||||
}
|
||||
|
||||
public func loadTemplate(templateName:String) -> Template? {
|
||||
public func loadTemplate(templateName: String) -> Template? {
|
||||
return loadTemplate([templateName])
|
||||
}
|
||||
|
||||
public func loadTemplate(templateNames:[String]) -> Template? {
|
||||
public func loadTemplate(templateNames: [String]) -> Template? {
|
||||
for path in paths {
|
||||
for templateName in templateNames {
|
||||
let templatePath = path + Path(templateName)
|
||||
@@ -1,41 +1,68 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
/// Split a string by spaces leaving quoted phrases together
|
||||
func smartSplit(value: String) -> [String] {
|
||||
var word = ""
|
||||
var separator: Character = " "
|
||||
var components: [String] = []
|
||||
|
||||
for character in value.characters {
|
||||
if character == separator {
|
||||
if separator != " " {
|
||||
word.append(separator)
|
||||
}
|
||||
|
||||
if !word.isEmpty {
|
||||
components.append(word)
|
||||
word = ""
|
||||
}
|
||||
|
||||
separator = " "
|
||||
} else {
|
||||
if separator == " " && (character == "'" || character == "\"") {
|
||||
separator = character
|
||||
}
|
||||
word.append(character)
|
||||
}
|
||||
}
|
||||
|
||||
if !word.isEmpty {
|
||||
components.append(word)
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
|
||||
public enum Token : Equatable {
|
||||
/// A token representing a piece of text.
|
||||
case Text(value:String)
|
||||
case Text(value: String)
|
||||
|
||||
/// A token representing a variable.
|
||||
case Variable(value:String)
|
||||
case Variable(value: String)
|
||||
|
||||
/// A token representing a comment.
|
||||
case Comment(value:String)
|
||||
case Comment(value: String)
|
||||
|
||||
/// A token representing a template block.
|
||||
case Block(value:String)
|
||||
case Block(value: String)
|
||||
|
||||
/// Returns the underlying value as an array seperated by spaces
|
||||
public func components() -> [String] {
|
||||
// TODO: Make this smarter and treat quoted strings as a single component
|
||||
let characterSet = NSCharacterSet.whitespaceAndNewlineCharacterSet()
|
||||
|
||||
func strip(value: String) -> [String] {
|
||||
return value.stringByTrimmingCharactersInSet(characterSet).componentsSeparatedByCharactersInSet(characterSet)
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .Block(let value):
|
||||
return strip(value)
|
||||
return smartSplit(value)
|
||||
case .Variable(let value):
|
||||
return strip(value)
|
||||
return smartSplit(value)
|
||||
case .Text(let value):
|
||||
return strip(value)
|
||||
return smartSplit(value)
|
||||
case .Comment(let value):
|
||||
return strip(value)
|
||||
return smartSplit(value)
|
||||
}
|
||||
}
|
||||
|
||||
public var contents:String {
|
||||
public var contents: String {
|
||||
switch self {
|
||||
case .Block(let value):
|
||||
return value
|
||||
@@ -49,7 +76,8 @@ public enum Token : Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public func ==(lhs:Token, rhs:Token) -> Bool {
|
||||
|
||||
public func == (lhs: Token, rhs: Token) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.Text(let lhsValue), .Text(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
@@ -6,7 +6,7 @@ class FilterExpression : Resolvable {
|
||||
let variable: Variable
|
||||
|
||||
init(token: String, parser: TokenParser) throws {
|
||||
let bits = token.componentsSeparatedByString("|")
|
||||
let bits = token.characters.split("|").map({ String($0).trim(" ") })
|
||||
if bits.isEmpty {
|
||||
filters = []
|
||||
variable = Variable("")
|
||||
@@ -35,19 +35,19 @@ class FilterExpression : Resolvable {
|
||||
|
||||
/// A structure used to represent a template variable, and to resolve it in a given context.
|
||||
public struct Variable : Equatable, Resolvable {
|
||||
public let variable:String
|
||||
public let variable: String
|
||||
|
||||
/// Create a variable with a string representing the variable
|
||||
public init(_ variable:String) {
|
||||
public init(_ variable: String) {
|
||||
self.variable = variable
|
||||
}
|
||||
|
||||
private func lookup() -> [String] {
|
||||
return variable.componentsSeparatedByString(".")
|
||||
return variable.characters.split(".").map(String.init)
|
||||
}
|
||||
|
||||
/// Resolve the variable in the given context
|
||||
public func resolve(context:Context) throws -> Any? {
|
||||
public func resolve(context: Context) throws -> Any? {
|
||||
var current: Any? = context
|
||||
|
||||
if (variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\"")) {
|
||||
@@ -56,12 +56,12 @@ public struct Variable : Equatable, Resolvable {
|
||||
}
|
||||
|
||||
for bit in lookup() {
|
||||
current = normalize(current)
|
||||
|
||||
if let context = current as? Context {
|
||||
current = context[bit]
|
||||
} else if let dictionary = current as? [String: Any] {
|
||||
current = dictionary[bit]
|
||||
} else if let dictionary = current as? [String: AnyObject] {
|
||||
current = dictionary[bit]
|
||||
} else if let array = current as? [Any] {
|
||||
if let index = Int(bit) {
|
||||
current = array[index]
|
||||
@@ -72,27 +72,62 @@ public struct Variable : Equatable, Resolvable {
|
||||
} else if bit == "count" {
|
||||
current = array.count
|
||||
}
|
||||
} else if let array = current as? NSArray {
|
||||
if let index = Int(bit) {
|
||||
current = array[index]
|
||||
} else if bit == "first" {
|
||||
current = array.firstObject
|
||||
} else if bit == "last" {
|
||||
current = array.lastObject
|
||||
} else if bit == "count" {
|
||||
current = array.count
|
||||
}
|
||||
} else if let object = current as? NSObject {
|
||||
} else if let object = current as? NSObject { // NSKeyValueCoding
|
||||
#if os(Linux)
|
||||
return nil
|
||||
#else
|
||||
current = object.valueForKey(bit)
|
||||
#endif
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return current
|
||||
return normalize(current)
|
||||
}
|
||||
}
|
||||
|
||||
public func ==(lhs:Variable, rhs:Variable) -> Bool {
|
||||
public func ==(lhs: Variable, rhs: Variable) -> Bool {
|
||||
return lhs.variable == rhs.variable
|
||||
}
|
||||
|
||||
|
||||
func normalize(current: Any?) -> Any? {
|
||||
if let current = current as? Normalizable {
|
||||
return current.normalize()
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
protocol Normalizable {
|
||||
func normalize() -> Any?
|
||||
}
|
||||
|
||||
extension Array : Normalizable {
|
||||
func normalize() -> Any? {
|
||||
return map { $0 as Any }
|
||||
}
|
||||
}
|
||||
|
||||
extension NSArray : Normalizable {
|
||||
func normalize() -> Any? {
|
||||
return map { $0 as Any }
|
||||
}
|
||||
}
|
||||
|
||||
extension Dictionary : Normalizable {
|
||||
func normalize() -> Any? {
|
||||
var dictionary: [String: Any] = [:]
|
||||
|
||||
for (key, value) in self {
|
||||
if let key = key as? String {
|
||||
dictionary[key] = Stencil.normalize(value)
|
||||
} else if let key = key as? CustomStringConvertible {
|
||||
dictionary[key.description] = Stencil.normalize(value)
|
||||
}
|
||||
}
|
||||
|
||||
return dictionary
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Stencil",
|
||||
"version": "0.4.0",
|
||||
"version": "0.6.0-beta.1",
|
||||
"summary": "Stencil is a simple and powerful template language for Swift.",
|
||||
"homepage": "https://github.com/kylef/Stencil",
|
||||
"license": {
|
||||
@@ -8,16 +8,15 @@
|
||||
"file": "LICENSE"
|
||||
},
|
||||
"authors": {
|
||||
"Kyle Fuller": "inbox@kylefuller.co.uk"
|
||||
"Kyle Fuller": "kyle@fuller.li"
|
||||
},
|
||||
"social_media_url": "http://twitter.com/kylefuller",
|
||||
"source": {
|
||||
"git": "https://github.com/kylef/Stencil.git",
|
||||
"tag": "0.4.0"
|
||||
"tag": "0.6.0-beta.1"
|
||||
},
|
||||
"source_files": [
|
||||
"Stencil/*.swift",
|
||||
"Stencil/TemplateLoader/*.swift"
|
||||
"Sources/*.swift"
|
||||
],
|
||||
"platforms": {
|
||||
"ios": "8.0",
|
||||
@@ -25,17 +24,6 @@
|
||||
},
|
||||
"requires_arc": true,
|
||||
"dependencies": {
|
||||
"PathKit": [ "~> 0.5.0" ]
|
||||
},
|
||||
"test_specification": {
|
||||
"source_files": [
|
||||
"StencilSpecs/*.swift",
|
||||
"StencilSpecs/TemplateLoader/*.swift",
|
||||
"StencilSpecs/Nodes/*.swift"
|
||||
],
|
||||
"dependencies": {
|
||||
"Spectre": [ "~> 0.4.1" ],
|
||||
"PathKit": [ "~> 0.5.0" ]
|
||||
}
|
||||
"PathKit": [ "~> 0.6.0" ]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2014 Cocode. All rights reserved.</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,59 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public struct Lexer {
|
||||
public let templateString:String
|
||||
let regex = try! NSRegularExpression(pattern: "(\\{\\{.*?\\}\\}|\\{%.*?%\\}|\\{#.*?#\\})", options: [])
|
||||
|
||||
public init(templateString:String) {
|
||||
self.templateString = templateString
|
||||
}
|
||||
|
||||
func createToken(string:String) -> Token {
|
||||
func strip() -> String {
|
||||
return string[string.startIndex.successor().successor()..<string.endIndex.predecessor().predecessor()].stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
|
||||
}
|
||||
|
||||
if string.hasPrefix("{{") {
|
||||
return Token.Variable(value: strip())
|
||||
} else if string.hasPrefix("{%") {
|
||||
return Token.Block(value: strip())
|
||||
} else if string.hasPrefix("{#") {
|
||||
return Token.Comment(value: strip())
|
||||
}
|
||||
|
||||
return Token.Text(value: string)
|
||||
}
|
||||
|
||||
/// Returns an array of tokens from a given template string.
|
||||
public func tokenize() -> [Token] {
|
||||
// Unfortunately NSRegularExpression doesn't have a split.
|
||||
// So here's a really terrible implementation
|
||||
|
||||
var tokens = [Token]()
|
||||
|
||||
let range = NSMakeRange(0, templateString.characters.count)
|
||||
var lastIndex = 0
|
||||
let nsTemplateString = templateString as NSString
|
||||
let options = NSMatchingOptions(rawValue: 0)
|
||||
regex.enumerateMatchesInString(templateString, options: options, range: range) { (result, flags, b) in
|
||||
if let result = result {
|
||||
if result.range.location != lastIndex {
|
||||
let previousMatch = nsTemplateString.substringWithRange(NSMakeRange(lastIndex, result.range.location - lastIndex))
|
||||
tokens.append(self.createToken(previousMatch))
|
||||
}
|
||||
|
||||
let match = nsTemplateString.substringWithRange(result.range)
|
||||
tokens.append(self.createToken(match))
|
||||
|
||||
lastIndex = result.range.location + result.range.length
|
||||
}
|
||||
}
|
||||
|
||||
if lastIndex < templateString.characters.count {
|
||||
let substring = (templateString as NSString).substringFromIndex(lastIndex)
|
||||
tokens.append(Token.Text(value: substring))
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public struct TemplateSyntaxError : ErrorType, Equatable, CustomStringConvertible {
|
||||
public let description:String
|
||||
|
||||
public init(_ description:String) {
|
||||
self.description = description
|
||||
}
|
||||
}
|
||||
|
||||
public func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool {
|
||||
return lhs.description == rhs.description
|
||||
}
|
||||
|
||||
public protocol NodeType {
|
||||
/// Render the node in the given context
|
||||
func render(context:Context) throws -> String
|
||||
}
|
||||
|
||||
/// Render the collection of nodes in the given context
|
||||
public func renderNodes(nodes:[NodeType], _ context:Context) throws -> String {
|
||||
return try nodes.map { try $0.render(context) }.joinWithSeparator("")
|
||||
}
|
||||
|
||||
public class SimpleNode : NodeType {
|
||||
let handler:Context throws -> String
|
||||
|
||||
public init(handler:Context throws -> String) {
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
return try handler(context)
|
||||
}
|
||||
}
|
||||
|
||||
public class TextNode : NodeType {
|
||||
public let text:String
|
||||
|
||||
public init(text:String) {
|
||||
self.text = text
|
||||
}
|
||||
|
||||
public func render(context:Context) throws -> String {
|
||||
return self.text
|
||||
}
|
||||
}
|
||||
|
||||
public protocol Resolvable {
|
||||
func resolve(context: Context) throws -> Any?
|
||||
}
|
||||
|
||||
public class VariableNode : NodeType {
|
||||
public let variable: Resolvable
|
||||
|
||||
public init(variable: Resolvable) {
|
||||
self.variable = variable
|
||||
}
|
||||
|
||||
public init(variable: String) {
|
||||
self.variable = Variable(variable)
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
let result = try variable.resolve(context)
|
||||
|
||||
if let result = result as? String {
|
||||
return result
|
||||
} else if let result = result as? CustomStringConvertible {
|
||||
return result.description
|
||||
} else if let result = result as? NSObject {
|
||||
return result.description
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
public class NowNode : NodeType {
|
||||
public let format:Variable
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
var format:Variable?
|
||||
|
||||
let components = token.components()
|
||||
guard components.count <= 2 else {
|
||||
throw TemplateSyntaxError("'now' tags may only have one argument: the format string `\(token.contents)`.")
|
||||
}
|
||||
if components.count == 2 {
|
||||
format = Variable(components[1])
|
||||
}
|
||||
|
||||
return NowNode(format:format)
|
||||
}
|
||||
|
||||
public init(format:Variable?) {
|
||||
self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"")
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
let date = NSDate()
|
||||
let format = try self.format.resolve(context)
|
||||
var formatter:NSDateFormatter?
|
||||
|
||||
if let format = format as? NSDateFormatter {
|
||||
formatter = format
|
||||
} else if let format = format as? String {
|
||||
formatter = NSDateFormatter()
|
||||
formatter!.dateFormat = format
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
|
||||
return formatter!.stringFromDate(date)
|
||||
}
|
||||
}
|
||||
|
||||
public class ForNode : NodeType {
|
||||
let variable:Variable
|
||||
let loopVariable:String
|
||||
let nodes:[NodeType]
|
||||
let emptyNodes: [NodeType]
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
|
||||
guard components.count == 4 && components[2] == "in" else {
|
||||
throw TemplateSyntaxError("'for' statements should use the following 'for x in y' `\(token.contents)`.")
|
||||
}
|
||||
|
||||
let loopVariable = components[1]
|
||||
let variable = components[3]
|
||||
|
||||
var emptyNodes = [NodeType]()
|
||||
|
||||
let forNodes = try parser.parse(until(["endfor", "empty"]))
|
||||
|
||||
guard let token = parser.nextToken() else {
|
||||
throw TemplateSyntaxError("`endfor` was not found.")
|
||||
}
|
||||
|
||||
if token.contents == "empty" {
|
||||
emptyNodes = try parser.parse(until(["endfor"]))
|
||||
parser.nextToken()
|
||||
}
|
||||
|
||||
return ForNode(variable: variable, loopVariable: loopVariable, nodes: forNodes, emptyNodes:emptyNodes)
|
||||
}
|
||||
|
||||
public init(variable:String, loopVariable:String, nodes:[NodeType], emptyNodes:[NodeType]) {
|
||||
self.variable = Variable(variable)
|
||||
self.loopVariable = loopVariable
|
||||
self.nodes = nodes
|
||||
self.emptyNodes = emptyNodes
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
let values = try variable.resolve(context)
|
||||
|
||||
if let values = values as? NSArray where values.count > 0 {
|
||||
return try values.map { item in
|
||||
try context.push([loopVariable: item]) {
|
||||
try renderNodes(nodes, context)
|
||||
}
|
||||
}.joinWithSeparator("")
|
||||
}
|
||||
|
||||
return try context.push {
|
||||
try renderNodes(emptyNodes, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class IfNode : NodeType {
|
||||
public let variable:Variable
|
||||
public let trueNodes:[NodeType]
|
||||
public let falseNodes:[NodeType]
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
guard components.count == 2 else {
|
||||
throw TemplateSyntaxError("'if' statements should use the following 'if condition' `\(token.contents)`.")
|
||||
}
|
||||
let variable = components[1]
|
||||
var trueNodes = [NodeType]()
|
||||
var falseNodes = [NodeType]()
|
||||
|
||||
trueNodes = try parser.parse(until(["endif", "else"]))
|
||||
|
||||
guard let token = parser.nextToken() else {
|
||||
throw TemplateSyntaxError("`endif` was not found.")
|
||||
}
|
||||
|
||||
if token.contents == "else" {
|
||||
falseNodes = try parser.parse(until(["endif"]))
|
||||
parser.nextToken()
|
||||
}
|
||||
|
||||
return IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)
|
||||
}
|
||||
|
||||
public class func parse_ifnot(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
guard components.count == 2 else {
|
||||
throw TemplateSyntaxError("'ifnot' statements should use the following 'if condition' `\(token.contents)`.")
|
||||
}
|
||||
let variable = components[1]
|
||||
var trueNodes = [NodeType]()
|
||||
var falseNodes = [NodeType]()
|
||||
|
||||
falseNodes = try parser.parse(until(["endif", "else"]))
|
||||
|
||||
guard let token = parser.nextToken() else {
|
||||
throw TemplateSyntaxError("`endif` was not found.")
|
||||
}
|
||||
|
||||
if token.contents == "else" {
|
||||
trueNodes = try parser.parse(until(["endif"]))
|
||||
parser.nextToken()
|
||||
}
|
||||
|
||||
return IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)
|
||||
}
|
||||
|
||||
public init(variable:String, trueNodes:[NodeType], falseNodes:[NodeType]) {
|
||||
self.variable = Variable(variable)
|
||||
self.trueNodes = trueNodes
|
||||
self.falseNodes = falseNodes
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
let result = try variable.resolve(context)
|
||||
var truthy = false
|
||||
|
||||
if let result = result as? NSArray {
|
||||
if result.count > 0 {
|
||||
truthy = true
|
||||
}
|
||||
} else if result != nil {
|
||||
truthy = true
|
||||
}
|
||||
|
||||
context.push()
|
||||
let output:String
|
||||
if truthy {
|
||||
output = try renderNodes(trueNodes, context)
|
||||
} else {
|
||||
output = try renderNodes(falseNodes, context)
|
||||
}
|
||||
context.pop()
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// Stencil.h
|
||||
// Stencil
|
||||
//
|
||||
// Created by Kyle Fuller on 23/10/2014.
|
||||
// Copyright (c) 2014 Cocode. All rights reserved.
|
||||
// See LICENSE for more details.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for Stencil.
|
||||
FOUNDATION_EXPORT double StencilVersionNumber;
|
||||
|
||||
//! Project version string for Stencil.
|
||||
FOUNDATION_EXPORT const unsigned char StencilVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <Stencil/PublicHeader.h>
|
||||
@@ -1,65 +0,0 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
describe("Context") {
|
||||
var context: Context!
|
||||
|
||||
$0.before {
|
||||
context = Context(dictionary: ["name": "Kyle"])
|
||||
}
|
||||
|
||||
$0.it("allows you to get a value via subscripting") {
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to set a value via subscripting") {
|
||||
context["name"] = "Katie"
|
||||
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
|
||||
$0.it("allows you to remove a value via subscripting") {
|
||||
context["name"] = nil
|
||||
|
||||
try expect(context["name"]).to.beNil()
|
||||
}
|
||||
|
||||
$0.it("allows you to retrieve a value from a parent") {
|
||||
context.push()
|
||||
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to override a parent's value") {
|
||||
context.push()
|
||||
context["name"] = "Katie"
|
||||
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
|
||||
$0.it("allows you to pop to restore previous state") {
|
||||
context.push()
|
||||
context["name"] = "Katie"
|
||||
context.pop()
|
||||
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to push a dictionary onto the stack") {
|
||||
context.push(["name": "Katie"])
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
|
||||
$0.it("allows you to push a dictionary and run a closure then restoring previous state") {
|
||||
var didRun = false
|
||||
|
||||
try context.push(["name": "Katie"]) {
|
||||
didRun = true
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
|
||||
try expect(didRun).to.beTrue()
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
describe("template filters") {
|
||||
let context = Context(dictionary: ["name": "Kyle"])
|
||||
|
||||
$0.it("allows you to register a custom filter") {
|
||||
let template = Template(templateString: "{{ name|repeat }}")
|
||||
template.parser.registerFilter("repeat") { value in
|
||||
if let value = value as? String {
|
||||
return "\(value) \(value)"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
let result = try template.render(context)
|
||||
try expect(result) == "Kyle Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to register a custom filter") {
|
||||
let template = Template(templateString: "{{ name|repeat }}")
|
||||
template.parser.registerFilter("repeat") { value in
|
||||
throw TemplateSyntaxError("No Repeat")
|
||||
}
|
||||
|
||||
try expect(try template.render(context)).toThrow(TemplateSyntaxError("No Repeat"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe("capitalize filter") {
|
||||
let template = Template(templateString: "{{ name|capitalize }}")
|
||||
|
||||
$0.it("capitalizes a string") {
|
||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe("uppercase filter") {
|
||||
let template = Template(templateString: "{{ name|uppercase }}")
|
||||
|
||||
$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"
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
describe("Lexer") {
|
||||
$0.it("can tokenize text") {
|
||||
let lexer = Lexer(templateString: "Hello World")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == Token.Text(value: "Hello World")
|
||||
}
|
||||
|
||||
$0.it("can tokenize a comment") {
|
||||
let lexer = Lexer(templateString: "{# Comment #}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == (1)
|
||||
try expect(tokens.first) == Token.Comment(value: "Comment")
|
||||
}
|
||||
|
||||
$0.it("can tokenize a variable") {
|
||||
let lexer = Lexer(templateString: "{{ Variable }}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == Token.Variable(value: "Variable")
|
||||
}
|
||||
|
||||
$0.it("can tokenize a mixture of content") {
|
||||
let lexer = Lexer(templateString: "My name is {{ name }}.")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 3
|
||||
try expect(tokens[0]) == Token.Text(value: "My name is ")
|
||||
try expect(tokens[1]) == Token.Variable(value: "name")
|
||||
try expect(tokens[2]) == Token.Text(value: ".")
|
||||
}
|
||||
|
||||
$0.it("can tokenize two variables without being greedy") {
|
||||
let lexer = Lexer(templateString: "{{ thing }}{{ name }}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 2
|
||||
try expect(tokens[0]) == Token.Variable(value: "thing")
|
||||
try expect(tokens[1]) == Token.Variable(value: "name")
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
describe("ForNode") {
|
||||
let context = Context(dictionary: [
|
||||
"items": [1, 2, 3],
|
||||
"emptyItems": [Int](),
|
||||
])
|
||||
|
||||
$0.it("renders the given nodes for each item") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "123"
|
||||
}
|
||||
|
||||
$0.it("renders the given empty nodes when no items found item") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||
let node = ForNode(variable: "emptyItems", loopVariable: "item", nodes: nodes, emptyNodes: emptyNodes)
|
||||
try expect(try node.render(context)) == "empty"
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
describe("IfNode") {
|
||||
$0.describe("parsing") {
|
||||
$0.it("can parse an if block") {
|
||||
let tokens = [
|
||||
Token.Block(value: "if value"),
|
||||
Token.Text(value: "true"),
|
||||
Token.Block(value: "else"),
|
||||
Token.Text(value: "false"),
|
||||
Token.Block(value: "endif")
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
let trueNode = node?.trueNodes.first as? TextNode
|
||||
let falseNode = node?.falseNodes.first as? TextNode
|
||||
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.variable.variable) == "value"
|
||||
try expect(node?.trueNodes.count) == 1
|
||||
try expect(trueNode?.text) == "true"
|
||||
try expect(node?.falseNodes.count) == 1
|
||||
try expect(falseNode?.text) == "false"
|
||||
}
|
||||
|
||||
$0.it("can parse an ifnot block") {
|
||||
let tokens = [
|
||||
Token.Block(value: "ifnot value"),
|
||||
Token.Text(value: "false"),
|
||||
Token.Block(value: "else"),
|
||||
Token.Text(value: "true"),
|
||||
Token.Block(value: "endif")
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
let trueNode = node?.trueNodes.first as? TextNode
|
||||
let falseNode = node?.falseNodes.first as? TextNode
|
||||
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.variable.variable) == "value"
|
||||
try expect(node?.trueNodes.count) == 1
|
||||
try expect(trueNode?.text) == "true"
|
||||
try expect(node?.falseNodes.count) == 1
|
||||
try expect(falseNode?.text) == "false"
|
||||
}
|
||||
|
||||
$0.it("throws an error when parsing an if block without an endif") {
|
||||
let tokens = [
|
||||
Token.Block(value: "if value"),
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
let error = TemplateSyntaxError("`endif` was not found.")
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
|
||||
$0.it("throws an error when parsing an ifnot without an endif") {
|
||||
let tokens = [
|
||||
Token.Block(value: "ifnot value"),
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
let error = TemplateSyntaxError("`endif` was not found.")
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("rendering") {
|
||||
let context = Context(dictionary: ["items": true])
|
||||
|
||||
$0.it("renders the truth when expression evaluates to true") {
|
||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
try expect(try node.render(context)) == "true"
|
||||
}
|
||||
|
||||
$0.it("renders the false when expression evaluates to false") {
|
||||
let node = IfNode(variable: "unknown", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
try expect(try node.render(context)) == "false"
|
||||
}
|
||||
|
||||
$0.it("renders the truth when array expression is not empty") {
|
||||
let items: Array<[String:AnyObject]> = [["key":"key1","value":42],["key":"key2","value":1337]]
|
||||
let arrayContext = Context(dictionary: ["items": [items]])
|
||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
try expect(try node.render(arrayContext)) == "true"
|
||||
}
|
||||
|
||||
$0.it("renders the false when array expression is empty") {
|
||||
let emptyItems = Array<[String:AnyObject]>()
|
||||
let arrayContext = Context(dictionary: ["items": emptyItems])
|
||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
try expect(try node.render(arrayContext)) == "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
class ErrorNode : NodeType {
|
||||
func render(context: Context) throws -> String {
|
||||
throw TemplateSyntaxError("Custom Error")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe("Node") {
|
||||
let context = Context(dictionary: [
|
||||
"name": "Kyle",
|
||||
"age": 27,
|
||||
"items": [1, 2, 3],
|
||||
])
|
||||
|
||||
$0.describe("TextNode") {
|
||||
$0.it("renders the given text") {
|
||||
let node = TextNode(text: "Hello World")
|
||||
try expect(try node.render(context)) == "Hello World"
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("VariableNode") {
|
||||
$0.it("resolves and renders the variable") {
|
||||
let node = VariableNode(variable: Variable("name"))
|
||||
try expect(try node.render(context)) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("resolves and renders a non string variable") {
|
||||
let node = VariableNode(variable: Variable("age"))
|
||||
try expect(try node.render(context)) == "27"
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("rendering nodes") {
|
||||
$0.it("renders the nodes") {
|
||||
let nodes: [NodeType] = [
|
||||
TextNode(text:"Hello "),
|
||||
VariableNode(variable: "name"),
|
||||
]
|
||||
|
||||
try expect(try renderNodes(nodes, context)) == "Hello Kyle"
|
||||
}
|
||||
|
||||
$0.it("correctly throws a nodes failure") {
|
||||
let nodes: [NodeType] = [
|
||||
TextNode(text:"Hello "),
|
||||
VariableNode(variable: "name"),
|
||||
ErrorNode(),
|
||||
]
|
||||
|
||||
try expect(try renderNodes(nodes, context)).toThrow(TemplateSyntaxError("Custom Error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import Foundation
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
describe("NowNode") {
|
||||
$0.describe("parsing") {
|
||||
$0.it("parses default format without any now arguments") {
|
||||
let tokens = [ Token.Block(value: "now") ]
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? NowNode
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.format.variable) == "\"yyyy-MM-dd 'at' HH:mm\""
|
||||
}
|
||||
|
||||
$0.it("parses now with a format") {
|
||||
let tokens = [ Token.Block(value: "now \"HH:mm\"") ]
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? NowNode
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.format.variable) == "\"HH:mm\""
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("rendering") {
|
||||
$0.it("renders the date") {
|
||||
let node = NowNode(format: Variable("\"yyyy-MM-dd\""))
|
||||
|
||||
let formatter = NSDateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
let date = formatter.stringFromDate(NSDate())
|
||||
|
||||
try expect(try node.render(Context())) == date
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
describe("TokenParser") {
|
||||
$0.it("can parse a text token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Text(value: "Hello World")
|
||||
])
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? TextNode
|
||||
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.text) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can parse a variable token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Variable(value: "'name'")
|
||||
])
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? VariableNode
|
||||
try expect(nodes.count) == 1
|
||||
let result = try node?.render(Context())
|
||||
try expect(result) == "name"
|
||||
}
|
||||
|
||||
$0.it("can parse a comment token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Comment(value: "Secret stuff!")
|
||||
])
|
||||
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.count) == 0
|
||||
}
|
||||
|
||||
$0.it("can parse a tag token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Block(value: "now"),
|
||||
])
|
||||
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.count) == 1
|
||||
}
|
||||
|
||||
$0.it("errors when parsing an unknown tag") {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Block(value: "unknown"),
|
||||
])
|
||||
|
||||
try expect(try parser.parse()).toThrow(TemplateSyntaxError("Unknown template tag 'unknown'"))
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
class CustomNode : NodeType {
|
||||
func render(context:Context) throws -> String {
|
||||
return "Hello World"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe("Stencil") {
|
||||
$0.it("can render the README example") {
|
||||
let templateString = "There are {{ articles.count }} articles.\n" +
|
||||
"\n" +
|
||||
"{% for article in articles %}" +
|
||||
" - {{ article.title }} by {{ article.author }}.\n" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let context = Context(dictionary: [
|
||||
"articles": [
|
||||
[ "title": "Migrating from OCUnit to XCTest", "author": "Kyle Fuller" ],
|
||||
[ "title": "Memory Management with ARC", "author": "Kyle Fuller" ],
|
||||
]
|
||||
])
|
||||
|
||||
let template = Template(templateString:templateString)
|
||||
let result = try template.render(context)
|
||||
|
||||
let fixture = "There are 2 articles.\n" +
|
||||
"\n" +
|
||||
" - Migrating from OCUnit to XCTest by Kyle Fuller.\n" +
|
||||
" - Memory Management with ARC by Kyle Fuller.\n" +
|
||||
"\n"
|
||||
|
||||
try expect(result) == fixture
|
||||
}
|
||||
|
||||
$0.it("can render a custom template tag") {
|
||||
let templateString = "{% custom %}"
|
||||
let template = Template(templateString:templateString)
|
||||
|
||||
template.parser.registerTag("custom") { parser, token in
|
||||
return CustomNode()
|
||||
}
|
||||
|
||||
let result = try template.render()
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can render a simple custom tag") {
|
||||
let templateString = "{% custom %}"
|
||||
let template = Template(templateString:templateString)
|
||||
|
||||
template.parser.registerSimpleTag("custom") { context in
|
||||
return "Hello World"
|
||||
}
|
||||
|
||||
try expect(try template.render()) == "Hello World"
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import PathKit
|
||||
|
||||
|
||||
describe("Include") {
|
||||
let path = Path(__FILE__) + ".." + ".." + "StencilSpecs" + "fixtures"
|
||||
let loader = TemplateLoader(paths: [path])
|
||||
|
||||
$0.describe("parsing") {
|
||||
$0.it("throws an error when no template is given") {
|
||||
let tokens = [ Token.Block(value: "include") ]
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
|
||||
let error = TemplateSyntaxError("'include' tag takes one argument, the template file to be included")
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
|
||||
$0.it("can parse a valid include block") {
|
||||
let tokens = [ Token.Block(value: "include \"test.html\"") ]
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IncludeNode
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.templateName) == "test.html"
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("rendering") {
|
||||
$0.it("throws an error when rendering without a loader") {
|
||||
let node = IncludeNode(templateName: "test.html")
|
||||
|
||||
do {
|
||||
try node.render(Context())
|
||||
} catch {
|
||||
try expect("\(error)") == "Template loader not in context"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("throws an error when it cannot find the included template") {
|
||||
let node = IncludeNode(templateName: "unknown.html")
|
||||
|
||||
do {
|
||||
try node.render(Context(dictionary: ["loader": loader]))
|
||||
} catch {
|
||||
try expect("\(error)".hasPrefix("'unknown.html' template not found")).to.beTrue()
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("successfully renders a found included template") {
|
||||
let node = IncludeNode(templateName: "test.html")
|
||||
let context = Context(dictionary: ["loader":loader, "target": "World"])
|
||||
let value = try node.render(context)
|
||||
try expect(value) == "Hello World!"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import PathKit
|
||||
|
||||
|
||||
describe("Inheritence") {
|
||||
let path = Path(__FILE__) + ".." + ".." + "StencilSpecs" + "fixtures"
|
||||
let loader = TemplateLoader(paths: [path])
|
||||
|
||||
$0.it("can inherit from another template") {
|
||||
let context = Context(dictionary: ["loader": loader])
|
||||
let template = loader.loadTemplate("child.html")
|
||||
try expect(try template?.render(context)) == "Header\nChild"
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import PathKit
|
||||
|
||||
|
||||
describe("TemplateLoader") {
|
||||
let path = Path(__FILE__) + ".." + ".." + "StencilSpecs" + "fixtures"
|
||||
let loader = TemplateLoader(paths: [path])
|
||||
|
||||
$0.it("returns nil when a template cannot be found") {
|
||||
try expect(loader.loadTemplate("unknown.html")).to.beNil()
|
||||
}
|
||||
|
||||
$0.it("returns nil when an array of templates cannot be found") {
|
||||
try expect(loader.loadTemplate(["unknown.html", "unknown2.html"])).to.beNil()
|
||||
}
|
||||
|
||||
$0.it("can load a template from a file") {
|
||||
if loader.loadTemplate("test.html") == nil {
|
||||
throw failure("didn't find the template")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
describe("Template") {
|
||||
$0.it("can render a template from a string") {
|
||||
let context = Context(dictionary: [ "name": "Kyle" ])
|
||||
let template = Template(templateString: "Hello World")
|
||||
let result = try template.render(context)
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import Foundation
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
@objc class Object : NSObject {
|
||||
let title = "Hello World"
|
||||
}
|
||||
|
||||
|
||||
describe("Variable") {
|
||||
let context = Context(dictionary: [
|
||||
"name": "Kyle",
|
||||
"contacts": ["Katie", "Carlton"],
|
||||
"profiles": [
|
||||
"github": "kylef",
|
||||
],
|
||||
"object": Object(),
|
||||
])
|
||||
|
||||
$0.it("can resolve a string literal with double quotes") {
|
||||
let variable = Variable("\"name\"")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "name"
|
||||
}
|
||||
|
||||
$0.it("can resolve a string literal with single quotes") {
|
||||
let variable = Variable("'name'")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "name"
|
||||
}
|
||||
|
||||
$0.it("can resolve a string variable") {
|
||||
let variable = Variable("name")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("can resolve an item from a dictionary") {
|
||||
let variable = Variable("profiles.github")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "kylef"
|
||||
}
|
||||
|
||||
$0.it("can resolve an item from an array via it's index") {
|
||||
let variable = Variable("contacts.0")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Katie"
|
||||
}
|
||||
|
||||
$0.it("can resolve the first item from an array") {
|
||||
let variable = Variable("contacts.first")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Katie"
|
||||
}
|
||||
|
||||
$0.it("can resolve the last item from an array") {
|
||||
let variable = Variable("contacts.last")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Carlton"
|
||||
}
|
||||
|
||||
$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"
|
||||
}
|
||||
}
|
||||
62
Tests/ContextSpec.swift
Normal file
62
Tests/ContextSpec.swift
Normal file
@@ -0,0 +1,62 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
func testContext() {
|
||||
describe("Context") {
|
||||
var context: Context!
|
||||
|
||||
$0.before {
|
||||
context = Context(dictionary: ["name": "Kyle"])
|
||||
}
|
||||
|
||||
$0.it("allows you to get a value via subscripting") {
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to set a value via subscripting") {
|
||||
context["name"] = "Katie"
|
||||
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
|
||||
$0.it("allows you to remove a value via subscripting") {
|
||||
context["name"] = nil
|
||||
|
||||
try expect(context["name"]).to.beNil()
|
||||
}
|
||||
|
||||
$0.it("allows you to retrieve a value from a parent") {
|
||||
try context.push {
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("allows you to override a parent's value") {
|
||||
try context.push {
|
||||
context["name"] = "Katie"
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("allows you to pop to restore previous state") {
|
||||
context.push {
|
||||
context["name"] = "Katie"
|
||||
}
|
||||
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to push a dictionary and run a closure then restoring previous state") {
|
||||
var didRun = false
|
||||
|
||||
try context.push(["name": "Katie"]) {
|
||||
didRun = true
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
|
||||
try expect(didRun).to.beTrue()
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
}
|
||||
}
|
||||
70
Tests/FilterSpec.swift
Normal file
70
Tests/FilterSpec.swift
Normal file
@@ -0,0 +1,70 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
func testFilter() {
|
||||
describe("template filters") {
|
||||
let context: [String: Any] = ["name": "Kyle"]
|
||||
|
||||
$0.it("allows you to register a custom filter") {
|
||||
let template = Template(templateString: "{{ name|repeat }}")
|
||||
|
||||
let namespace = Namespace()
|
||||
namespace.registerFilter("repeat") { value in
|
||||
if let value = value as? String {
|
||||
return "\(value) \(value)"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
let result = try template.render(Context(dictionary: context, namespace: namespace))
|
||||
try expect(result) == "Kyle Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to register a custom which throws") {
|
||||
let template = Template(templateString: "{{ name|repeat }}")
|
||||
let namespace = Namespace()
|
||||
namespace.registerFilter("repeat") { value in
|
||||
throw TemplateSyntaxError("No Repeat")
|
||||
}
|
||||
|
||||
try expect(try template.render(Context(dictionary: context, namespace: namespace))).toThrow(TemplateSyntaxError("No Repeat"))
|
||||
}
|
||||
|
||||
$0.it("allows whitespace in expression") {
|
||||
let template = Template(templateString: "{{ name | uppercase }}")
|
||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
||||
try expect(result) == "KYLE"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe("capitalize filter") {
|
||||
let template = Template(templateString: "{{ name|capitalize }}")
|
||||
|
||||
$0.it("capitalizes a string") {
|
||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe("uppercase filter") {
|
||||
let template = Template(templateString: "{{ name|uppercase }}")
|
||||
|
||||
$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"
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Tests/LexerSpec.swift
Normal file
50
Tests/LexerSpec.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
func testLexer() {
|
||||
describe("Lexer") {
|
||||
$0.it("can tokenize text") {
|
||||
let lexer = Lexer(templateString: "Hello World")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == Token.Text(value: "Hello World")
|
||||
}
|
||||
|
||||
$0.it("can tokenize a comment") {
|
||||
let lexer = Lexer(templateString: "{# Comment #}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == (1)
|
||||
try expect(tokens.first) == Token.Comment(value: "Comment")
|
||||
}
|
||||
|
||||
$0.it("can tokenize a variable") {
|
||||
let lexer = Lexer(templateString: "{{ Variable }}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == Token.Variable(value: "Variable")
|
||||
}
|
||||
|
||||
$0.it("can tokenize a mixture of content") {
|
||||
let lexer = Lexer(templateString: "My name is {{ name }}.")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 3
|
||||
try expect(tokens[0]) == Token.Text(value: "My name is ")
|
||||
try expect(tokens[1]) == Token.Variable(value: "name")
|
||||
try expect(tokens[2]) == Token.Text(value: ".")
|
||||
}
|
||||
|
||||
$0.it("can tokenize two variables without being greedy") {
|
||||
let lexer = Lexer(templateString: "{{ thing }}{{ name }}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 2
|
||||
try expect(tokens[0]) == Token.Variable(value: "thing")
|
||||
try expect(tokens[1]) == Token.Variable(value: "name")
|
||||
}
|
||||
}
|
||||
}
|
||||
66
Tests/Nodes/ForNodeSpec.swift
Normal file
66
Tests/Nodes/ForNodeSpec.swift
Normal file
@@ -0,0 +1,66 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import Foundation
|
||||
|
||||
|
||||
func testForNode() {
|
||||
describe("ForNode") {
|
||||
let context = Context(dictionary: [
|
||||
"items": [1, 2, 3],
|
||||
"emptyItems": [Int](),
|
||||
])
|
||||
|
||||
$0.it("renders the given nodes for each item") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "123"
|
||||
}
|
||||
|
||||
$0.it("renders the given empty nodes when no items found item") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||
let node = ForNode(variable: "emptyItems", loopVariable: "item", nodes: nodes, emptyNodes: emptyNodes)
|
||||
try expect(try node.render(context)) == "empty"
|
||||
}
|
||||
|
||||
$0.it("renders a context variable of type Array<Any>") {
|
||||
let any_context = Context(dictionary: [
|
||||
"items": ([1, 2, 3] as [Any])
|
||||
])
|
||||
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(any_context)) == "123"
|
||||
}
|
||||
|
||||
#if os(OSX)
|
||||
$0.it("renders a context variable of type NSArray") {
|
||||
let nsarray_context = Context(dictionary: [
|
||||
"items": NSArray(array: [1, 2, 3])
|
||||
])
|
||||
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(nsarray_context)) == "123"
|
||||
}
|
||||
#endif
|
||||
|
||||
$0.it("renders the given nodes while providing if the item is first in the context") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.first")]
|
||||
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "1true2false3false"
|
||||
}
|
||||
|
||||
$0.it("renders the given nodes while providing if the item is last in the context") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.last")]
|
||||
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "1false2false3true"
|
||||
}
|
||||
|
||||
$0.it("renders the given nodes while providing item counter") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
|
||||
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "112233"
|
||||
}
|
||||
}
|
||||
}
|
||||
116
Tests/Nodes/IfNodeSpec.swift
Normal file
116
Tests/Nodes/IfNodeSpec.swift
Normal file
@@ -0,0 +1,116 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
func testIfNode() {
|
||||
describe("IfNode") {
|
||||
$0.describe("parsing") {
|
||||
$0.it("can parse an if block") {
|
||||
let tokens = [
|
||||
Token.Block(value: "if value"),
|
||||
Token.Text(value: "true"),
|
||||
Token.Block(value: "else"),
|
||||
Token.Text(value: "false"),
|
||||
Token.Block(value: "endif")
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, namespace: Namespace())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
let trueNode = node?.trueNodes.first as? TextNode
|
||||
let falseNode = node?.falseNodes.first as? TextNode
|
||||
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.variable.variable) == "value"
|
||||
try expect(node?.trueNodes.count) == 1
|
||||
try expect(trueNode?.text) == "true"
|
||||
try expect(node?.falseNodes.count) == 1
|
||||
try expect(falseNode?.text) == "false"
|
||||
}
|
||||
|
||||
$0.it("can parse an ifnot block") {
|
||||
let tokens = [
|
||||
Token.Block(value: "ifnot value"),
|
||||
Token.Text(value: "false"),
|
||||
Token.Block(value: "else"),
|
||||
Token.Text(value: "true"),
|
||||
Token.Block(value: "endif")
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, namespace: Namespace())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
let trueNode = node?.trueNodes.first as? TextNode
|
||||
let falseNode = node?.falseNodes.first as? TextNode
|
||||
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.variable.variable) == "value"
|
||||
try expect(node?.trueNodes.count) == 1
|
||||
try expect(trueNode?.text) == "true"
|
||||
try expect(node?.falseNodes.count) == 1
|
||||
try expect(falseNode?.text) == "false"
|
||||
}
|
||||
|
||||
$0.it("throws an error when parsing an if block without an endif") {
|
||||
let tokens = [
|
||||
Token.Block(value: "if value"),
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, namespace: Namespace())
|
||||
let error = TemplateSyntaxError("`endif` was not found.")
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
|
||||
$0.it("throws an error when parsing an ifnot without an endif") {
|
||||
let tokens = [
|
||||
Token.Block(value: "ifnot value"),
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, namespace: Namespace())
|
||||
let error = TemplateSyntaxError("`endif` was not found.")
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("rendering") {
|
||||
let context = Context(dictionary: ["items": true])
|
||||
|
||||
$0.it("renders the truth when expression evaluates to true") {
|
||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
try expect(try node.render(context)) == "true"
|
||||
}
|
||||
|
||||
$0.it("renders the false when expression evaluates to false") {
|
||||
let node = IfNode(variable: "unknown", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
try expect(try node.render(context)) == "false"
|
||||
}
|
||||
|
||||
$0.it("renders the truth when array expression is not empty") {
|
||||
let items: [[String: Any]] = [["key":"key1","value":42],["key":"key2","value":1337]]
|
||||
let arrayContext = Context(dictionary: ["items": [items]])
|
||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
try expect(try node.render(arrayContext)) == "true"
|
||||
}
|
||||
|
||||
$0.it("renders the false when array expression is empty") {
|
||||
let emptyItems = [[String: Any]]()
|
||||
let arrayContext = Context(dictionary: ["items": emptyItems])
|
||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
try expect(try node.render(arrayContext)) == "false"
|
||||
}
|
||||
|
||||
$0.it("renders the false when dictionary expression is empty") {
|
||||
let emptyItems = [String:Any]()
|
||||
let arrayContext = Context(dictionary: ["items": emptyItems])
|
||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
try expect(try node.render(arrayContext)) == "false"
|
||||
}
|
||||
|
||||
$0.it("renders the false when Array<Any> variable is empty") {
|
||||
let arrayContext = Context(dictionary: ["items": ([] as [Any])])
|
||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
try expect(try node.render(arrayContext)) == "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
Tests/Nodes/NodeSpec.swift
Normal file
60
Tests/Nodes/NodeSpec.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
class ErrorNode : NodeType {
|
||||
func render(context: Context) throws -> String {
|
||||
throw TemplateSyntaxError("Custom Error")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func testNode() {
|
||||
describe("Node") {
|
||||
let context = Context(dictionary: [
|
||||
"name": "Kyle",
|
||||
"age": 27,
|
||||
"items": [1, 2, 3],
|
||||
])
|
||||
|
||||
$0.describe("TextNode") {
|
||||
$0.it("renders the given text") {
|
||||
let node = TextNode(text: "Hello World")
|
||||
try expect(try node.render(context)) == "Hello World"
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("VariableNode") {
|
||||
$0.it("resolves and renders the variable") {
|
||||
let node = VariableNode(variable: Variable("name"))
|
||||
try expect(try node.render(context)) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("resolves and renders a non string variable") {
|
||||
let node = VariableNode(variable: Variable("age"))
|
||||
try expect(try node.render(context)) == "27"
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("rendering nodes") {
|
||||
$0.it("renders the nodes") {
|
||||
let nodes: [NodeType] = [
|
||||
TextNode(text:"Hello "),
|
||||
VariableNode(variable: "name"),
|
||||
]
|
||||
|
||||
try expect(try renderNodes(nodes, context)) == "Hello Kyle"
|
||||
}
|
||||
|
||||
$0.it("correctly throws a nodes failure") {
|
||||
let nodes: [NodeType] = [
|
||||
TextNode(text:"Hello "),
|
||||
VariableNode(variable: "name"),
|
||||
ErrorNode(),
|
||||
]
|
||||
|
||||
try expect(try renderNodes(nodes, context)).toThrow(TemplateSyntaxError("Custom Error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Tests/Nodes/NowNodeSpec.swift
Normal file
43
Tests/Nodes/NowNodeSpec.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
import Foundation
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
func testNowNode() {
|
||||
#if !os(Linux)
|
||||
describe("NowNode") {
|
||||
$0.describe("parsing") {
|
||||
$0.it("parses default format without any now arguments") {
|
||||
let tokens = [ Token.Block(value: "now") ]
|
||||
let parser = TokenParser(tokens: tokens, namespace: Namespace())
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? NowNode
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.format.variable) == "\"yyyy-MM-dd 'at' HH:mm\""
|
||||
}
|
||||
|
||||
$0.it("parses now with a format") {
|
||||
let tokens = [ Token.Block(value: "now \"HH:mm\"") ]
|
||||
let parser = TokenParser(tokens: tokens, namespace: Namespace())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? NowNode
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.format.variable) == "\"HH:mm\""
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("rendering") {
|
||||
$0.it("renders the date") {
|
||||
let node = NowNode(format: Variable("\"yyyy-MM-dd\""))
|
||||
|
||||
let formatter = NSDateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
let date = formatter.stringFromDate(NSDate())
|
||||
|
||||
try expect(try node.render(Context())) == date
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
62
Tests/ParserSpec.swift
Normal file
62
Tests/ParserSpec.swift
Normal file
@@ -0,0 +1,62 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
func testTokenParser() {
|
||||
describe("TokenParser") {
|
||||
$0.it("can parse a text token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Text(value: "Hello World")
|
||||
], namespace: Namespace())
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? TextNode
|
||||
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.text) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can parse a variable token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Variable(value: "'name'")
|
||||
], namespace: Namespace())
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? VariableNode
|
||||
try expect(nodes.count) == 1
|
||||
let result = try node?.render(Context())
|
||||
try expect(result) == "name"
|
||||
}
|
||||
|
||||
$0.it("can parse a comment token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Comment(value: "Secret stuff!")
|
||||
], namespace: Namespace())
|
||||
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.count) == 0
|
||||
}
|
||||
|
||||
$0.it("can parse a tag token") {
|
||||
let namespace = Namespace()
|
||||
namespace.registerSimpleTag("known") { _ in
|
||||
return ""
|
||||
}
|
||||
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Block(value: "known"),
|
||||
], namespace: namespace)
|
||||
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.count) == 1
|
||||
}
|
||||
|
||||
$0.it("errors when parsing an unknown tag") {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Block(value: "unknown"),
|
||||
], namespace: Namespace())
|
||||
|
||||
try expect(try parser.parse()).toThrow(TemplateSyntaxError("Unknown template tag 'unknown'"))
|
||||
}
|
||||
}
|
||||
}
|
||||
65
Tests/StencilSpec.swift
Normal file
65
Tests/StencilSpec.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
class CustomNode : NodeType {
|
||||
func render(context:Context) throws -> String {
|
||||
return "Hello World"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func testStencil() {
|
||||
describe("Stencil") {
|
||||
$0.it("can render the README example") {
|
||||
let templateString = "There are {{ articles.count }} articles.\n" +
|
||||
"\n" +
|
||||
"{% for article in articles %}" +
|
||||
" - {{ article.title }} by {{ article.author }}.\n" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let context = Context(dictionary: [
|
||||
"articles": [
|
||||
[ "title": "Migrating from OCUnit to XCTest", "author": "Kyle Fuller" ],
|
||||
[ "title": "Memory Management with ARC", "author": "Kyle Fuller" ],
|
||||
]
|
||||
])
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
let result = try template.render(context)
|
||||
|
||||
let fixture = "There are 2 articles.\n" +
|
||||
"\n" +
|
||||
" - Migrating from OCUnit to XCTest by Kyle Fuller.\n" +
|
||||
" - Memory Management with ARC by Kyle Fuller.\n" +
|
||||
"\n"
|
||||
|
||||
try expect(result) == fixture
|
||||
}
|
||||
|
||||
$0.it("can render a custom template tag") {
|
||||
let templateString = "{% custom %}"
|
||||
let template = Template(templateString: templateString)
|
||||
|
||||
let namespace = Namespace()
|
||||
namespace.registerTag("custom") { parser, token in
|
||||
return CustomNode()
|
||||
}
|
||||
|
||||
let result = try template.render(Context(namespace: namespace))
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can render a simple custom tag") {
|
||||
let templateString = "{% custom %}"
|
||||
let template = Template(templateString: templateString)
|
||||
|
||||
let namespace = Namespace()
|
||||
namespace.registerSimpleTag("custom") { context in
|
||||
return "Hello World"
|
||||
}
|
||||
|
||||
try expect(try template.render(Context(namespace: namespace))) == "Hello World"
|
||||
}
|
||||
}
|
||||
}
|
||||
60
Tests/TemplateLoader/IncludeSpec.swift
Normal file
60
Tests/TemplateLoader/IncludeSpec.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import PathKit
|
||||
|
||||
|
||||
func testInclude() {
|
||||
describe("Include") {
|
||||
let path = Path(__FILE__) + ".." + ".." + "fixtures"
|
||||
let loader = TemplateLoader(paths: [path])
|
||||
|
||||
$0.describe("parsing") {
|
||||
$0.it("throws an error when no template is given") {
|
||||
let tokens = [ Token.Block(value: "include") ]
|
||||
let parser = TokenParser(tokens: tokens, namespace: Namespace())
|
||||
|
||||
let error = TemplateSyntaxError("'include' tag takes one argument, the template file to be included")
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
|
||||
$0.it("can parse a valid include block") {
|
||||
let tokens = [ Token.Block(value: "include \"test.html\"") ]
|
||||
let parser = TokenParser(tokens: tokens, namespace: Namespace())
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IncludeNode
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.templateName) == Variable("\"test.html\"")
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("rendering") {
|
||||
$0.it("throws an error when rendering without a loader") {
|
||||
let node = IncludeNode(templateName: Variable("\"test.html\""))
|
||||
|
||||
do {
|
||||
try node.render(Context())
|
||||
} catch {
|
||||
try expect("\(error)") == "Template loader not in context"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("throws an error when it cannot find the included template") {
|
||||
let node = IncludeNode(templateName: Variable("\"unknown.html\""))
|
||||
|
||||
do {
|
||||
try node.render(Context(dictionary: ["loader": loader]))
|
||||
} catch {
|
||||
try expect("\(error)".hasPrefix("'unknown.html' template not found")).to.beTrue()
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("successfully renders a found included template") {
|
||||
let node = IncludeNode(templateName: Variable("\"test.html\""))
|
||||
let context = Context(dictionary: ["loader":loader, "target": "World"])
|
||||
let value = try node.render(context)
|
||||
try expect(value) == "Hello World!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Tests/TemplateLoader/InheritenceSpec.swift
Normal file
17
Tests/TemplateLoader/InheritenceSpec.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import PathKit
|
||||
|
||||
|
||||
func testInheritence() {
|
||||
describe("Inheritence") {
|
||||
let path = Path(__FILE__) + ".." + ".." + "fixtures"
|
||||
let loader = TemplateLoader(paths: [path])
|
||||
|
||||
$0.it("can inherit from another template") {
|
||||
let context = Context(dictionary: ["loader": loader])
|
||||
let template = loader.loadTemplate("child.html")
|
||||
try expect(try template?.render(context)) == "Header\nChild"
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Tests/TemplateLoaderSpec.swift
Normal file
25
Tests/TemplateLoaderSpec.swift
Normal file
@@ -0,0 +1,25 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import PathKit
|
||||
|
||||
|
||||
func testTemplateLoader() {
|
||||
describe("TemplateLoader") {
|
||||
let path = Path(__FILE__) + ".." + ".." + "Tests" + "fixtures"
|
||||
let loader = TemplateLoader(paths: [path])
|
||||
|
||||
$0.it("returns nil when a template cannot be found") {
|
||||
try expect(loader.loadTemplate("unknown.html")).to.beNil()
|
||||
}
|
||||
|
||||
$0.it("returns nil when an array of templates cannot be found") {
|
||||
try expect(loader.loadTemplate(["unknown.html", "unknown2.html"])).to.beNil()
|
||||
}
|
||||
|
||||
$0.it("can load a template from a file") {
|
||||
if loader.loadTemplate("test.html") == nil {
|
||||
throw failure("didn't find the template")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
Tests/TemplateSpec.swift
Normal file
14
Tests/TemplateSpec.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
func testTemplate() {
|
||||
describe("Template") {
|
||||
$0.it("can render a template from a string") {
|
||||
let context = Context(dictionary: [ "name": "Kyle" ])
|
||||
let template = Template(templateString: "Hello World")
|
||||
let result = try template.render(context)
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Tests/TokenSpec.swift
Normal file
34
Tests/TokenSpec.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
func testToken() {
|
||||
describe("Token") {
|
||||
$0.it("can split the contents into components") {
|
||||
let token = Token.Text(value: "hello world")
|
||||
let components = token.components()
|
||||
|
||||
try expect(components.count) == 2
|
||||
try expect(components[0]) == "hello"
|
||||
try expect(components[1]) == "world"
|
||||
}
|
||||
|
||||
$0.it("can split the contents into components with single quoted strings") {
|
||||
let token = Token.Text(value: "hello 'kyle fuller'")
|
||||
let components = token.components()
|
||||
|
||||
try expect(components.count) == 2
|
||||
try expect(components[0]) == "hello"
|
||||
try expect(components[1]) == "'kyle fuller'"
|
||||
}
|
||||
|
||||
$0.it("can split the contents into components with double quoted strings") {
|
||||
let token = Token.Text(value: "hello \"kyle fuller\"")
|
||||
let components = token.components()
|
||||
|
||||
try expect(components.count) == 2
|
||||
try expect(components[0]) == "hello"
|
||||
try expect(components[1]) == "\"kyle fuller\""
|
||||
}
|
||||
}
|
||||
}
|
||||
77
Tests/VariableSpec.swift
Normal file
77
Tests/VariableSpec.swift
Normal file
@@ -0,0 +1,77 @@
|
||||
import Foundation
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
#if os(OSX)
|
||||
@objc class Object : NSObject {
|
||||
let title = "Hello World"
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
func testVariable() {
|
||||
describe("Variable") {
|
||||
let context = Context(dictionary: [
|
||||
"name": "Kyle",
|
||||
"contacts": ["Katie", "Carlton"],
|
||||
"profiles": [
|
||||
"github": "kylef",
|
||||
],
|
||||
])
|
||||
|
||||
#if os(OSX)
|
||||
context["object"] = Object()
|
||||
#endif
|
||||
|
||||
$0.it("can resolve a string literal with double quotes") {
|
||||
let variable = Variable("\"name\"")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "name"
|
||||
}
|
||||
|
||||
$0.it("can resolve a string literal with single quotes") {
|
||||
let variable = Variable("'name'")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "name"
|
||||
}
|
||||
|
||||
$0.it("can resolve a string variable") {
|
||||
let variable = Variable("name")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("can resolve an item from a dictionary") {
|
||||
let variable = Variable("profiles.github")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "kylef"
|
||||
}
|
||||
|
||||
$0.it("can resolve an item from an array via it's index") {
|
||||
let variable = Variable("contacts.0")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Katie"
|
||||
}
|
||||
|
||||
$0.it("can resolve the first item from an array") {
|
||||
let variable = Variable("contacts.first")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Katie"
|
||||
}
|
||||
|
||||
$0.it("can resolve the last item from an array") {
|
||||
let variable = Variable("contacts.last")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Carlton"
|
||||
}
|
||||
|
||||
#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"
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
15
Tests/main.swift
Normal file
15
Tests/main.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
testContext()
|
||||
testFilter()
|
||||
testLexer()
|
||||
testToken()
|
||||
testTokenParser()
|
||||
testTemplateLoader()
|
||||
testTemplate()
|
||||
testVariable()
|
||||
testNode()
|
||||
testForNode()
|
||||
testIfNode()
|
||||
testNowNode()
|
||||
testInclude()
|
||||
testInheritence()
|
||||
testStencil()
|
||||
21
circle.yml
21
circle.yml
@@ -1,21 +0,0 @@
|
||||
machine:
|
||||
xcode:
|
||||
version: "7.0"
|
||||
environment:
|
||||
XCODE_SCHEME: NONE
|
||||
XCODE_PROJECT: NONE
|
||||
|
||||
dependencies:
|
||||
post:
|
||||
- brew install --HEAD kylef/formulae/conche
|
||||
|
||||
test:
|
||||
override:
|
||||
- conche test
|
||||
|
||||
deployment:
|
||||
release:
|
||||
tag: /.*/
|
||||
commands:
|
||||
- pod trunk push
|
||||
|
||||
Reference in New Issue
Block a user