116 Commits

Author SHA1 Message Date
Kyle Fuller
4ffc888ba4 Release 0.6.0-beta.1 2016-04-04 22:39:19 +02:00
Kyle Fuller
3c21975b97 Merge pull request #61 from GregKaleka/patch-1
Minor grammatical fixes to README.md
2016-03-16 13:53:25 +00:00
Greg Kaleka
df9065f5a8 Minor grammatical fixes to README.md 2016-03-13 21:30:48 -07:00
Kyle Fuller
05b71736aa Merge pull request #59 from shnhrrsn/namespace-fix
Added namespace to Context
2016-03-09 23:41:43 +00:00
shnhrrsn
aa1399be55 Fixed tests for namespace changes 2016-03-06 00:39:10 -05:00
shnhrrsn
bdc14ab1e1 Added namespace to Context 2016-03-05 23:57:15 -05:00
Kyle Fuller
67d4c52535 [Context] Ensure pop happens when an error is thrown 2016-03-05 00:37:12 +00:00
Kyle Fuller
48026cde2c Split tags into separate files 2016-03-05 00:15:18 +00:00
Kyle Fuller
dc4b965aaa Merge pull request #55 from dtrenz/dtrenz-readme-usage
Added `try` to throwing expressions in example.
2016-02-27 15:46:05 -05:00
Dan Trenz
2190afee0d Added try to throwing expressions in example.
Both `Template(named: "template.stencil")` and `Template(named: "template.stencil")` throw but were not preceded by `try`. This usage example, in it's current form, triggers compiler errors.
2016-02-27 15:20:55 -05:00
Kyle Fuller
9b7e6ba7ed Release 0.5.3 2016-02-26 16:37:08 -05:00
Kyle Fuller
bf0989d329 Merge pull request #54 from kylef/kylef/linux
Make tests pass and run on Linux
2016-02-26 16:34:11 -05:00
Kyle Fuller
affd56ec99 [travis] Test on Linux and OS X 2016-02-26 16:18:00 -05:00
Kyle Fuller
070a82cb2d Change how we normalize values to be linux compatible
Closes #51
2016-02-26 16:16:36 -05:00
Kyle Fuller
3ec009381d Disable the now tag on Linux
NSDate is unavailable
2016-02-11 19:34:46 -05:00
Kyle Fuller
6deb93ac19 Disable NSObject based tests on Linux 2016-02-11 19:25:26 -05:00
Kyle Fuller
b4ba12bbde Use spectre-build for tests 2016-02-08 13:47:52 +00:00
Kyle Fuller
19d712b4a4 Release 0.5.2 2016-01-30 14:57:26 +01:00
Kyle Fuller
201b8e263c Fix failing for node tests
These we're broken in commit 0783506
2015-12-14 16:03:48 +00:00
Kyle Fuller
03928721c4 Move away from deprecated curry syntax 2015-12-14 09:04:18 -05:00
Kyle Fuller
07835063ed Fix an ambiguous array literal warning 2015-12-14 09:04:01 -05:00
Kyle Fuller
3c13d81b21 Whoops, Tests not Specs 2015-12-09 19:20:05 +00:00
Kyle Fuller
1668830d9b [for] Provide forloop context, first, last and counter 2015-12-09 19:18:16 +00:00
Kyle Fuller
14195b3199 Include missing specs 2015-12-09 19:18:08 +00:00
Kyle Fuller
ae75ea5911 [podspec] Correct PathKit dependency 2015-12-09 19:16:47 +00:00
Kyle Fuller
9c9ebbe559 Release 0.5.1 2015-12-08 18:08:44 +00:00
Kyle Fuller
5cdf1d326b Merge pull request #47 from neonichu/fix-manifest
Needs to depend on PathKit 0.6.x
2015-12-08 16:32:39 +00:00
Boris Bügling
f78562a1fd Needs to depend on PathKit 0.6.x
Seems like this is a bug in the current "stable" SPM which has been
fixed upstream in the meantime.
2015-12-08 17:31:12 +01:00
Kyle Fuller
0ccd8809e0 Merge pull request #46 from neonichu/linux-support
Support for Linux
2015-12-08 15:59:36 +00:00
Boris Bügling
356393088b Support for Linux 2015-12-08 16:54:58 +01:00
Kyle Fuller
b792cd09b9 Merge pull request #45 from neonichu/spm-support
Support for SPM
2015-12-08 12:28:55 +00:00
Boris Bügling
372b2e7576 Add Package.swift and move files around 2015-12-08 11:45:03 +01:00
Kyle Fuller
0bfd4134f9 Merge pull request #43 from njdehoog/filter_whitespace
Allow whitespace in filter expression
2015-11-24 14:33:17 +00:00
Niels de Hoog
aca0a3181d Allow whitespace in filter expression 2015-11-23 15:27:51 +01:00
Kyle Fuller
a1a268d5ac Merge pull request #41 from kylef/scanner
Replace NSRegularExpression with string scanning
2015-11-23 11:02:54 +00:00
Kyle Fuller
465834d89c Merge pull request #40 from njdehoog/array_any
Cast ForNode values to Array<Any>
2015-11-23 11:02:29 +00:00
Niels de Hoog
0af879ba8a Use switch syntax in resolve functions 2015-11-23 11:44:32 +01:00
Niels de Hoog
a516de51ff Update spec name to conform to style 2015-11-23 11:30:30 +01:00
Niels de Hoog
1f4aae1859 Added IfNode spec for Array<Any> value 2015-11-23 11:26:10 +01:00
Niels de Hoog
cba1cbe388 Updated specs for ForNode 2015-11-23 11:24:13 +01:00
Kyle Fuller
3722998c35 Replace NSRegularExpressions with string scanning 2015-11-21 16:27:24 +00:00
Kyle Fuller
22919dc5ce [Variable] Normalize resolved types into Swift types 2015-11-21 15:25:55 +00:00
Kyle Fuller
89b7da2e10 [Variable] Use Swift split over Foundation 2015-11-21 14:42:51 +00:00
Kyle Fuller
3bd3aec296 Resolve extends and include arguments as variables 2015-11-21 14:42:23 +00:00
Kyle Fuller
48a9a65bd5 [Token] Correctly split quoted components 2015-11-21 14:27:23 +00:00
Kyle Fuller
c86ab9c5b9 Remove unnessecary uses of Foundation 2015-11-21 14:06:15 +00:00
Kyle Fuller
dc774fe43b Add 'Namespace' a container for tags and filters 2015-11-18 16:10:27 +03:00
Kyle Fuller
226becb258 Release 0.4.0 2015-10-30 12:35:33 -07:00
Kyle Fuller
507cc5c661 [for block] Handle empty nodes
Closes #35
2015-10-26 08:26:16 -07:00
Kyle Fuller
9b26b7d71a [Context] Convenience push with block function 2015-10-26 08:26:16 -07:00
Kyle Fuller
19366ec71b Merge pull request #36 from AliSoftware/fix/if-node
Fix IfNode when using Array of arbitrary types
2015-10-26 08:23:37 -07:00
Olivier Halligon
ba65ab5fbe Fix IfNode when using Array of arbitrary types (which made the cast to [AnyObject] fail) 2015-10-26 14:26:41 +01:00
Kyle Fuller
8ac6e26876 Allow template filters to throw errors 2015-10-24 14:41:37 -07:00
Kyle Fuller
f35be4b701 Add test around custom template filters 2015-10-24 14:35:25 -07:00
Kyle Fuller
033ae61e42 Restore code style 2015-10-24 14:26:32 -07:00
Kyle Fuller
1ea58b70f3 Error for unknown blocks 2015-10-24 14:24:34 -07:00
Kyle Fuller
5883775f37 [Extends] Make sure we don't leave endblock behind 2015-10-24 14:24:06 -07:00
Kyle Fuller
d1891038f8 Merge pull request #33 from AliSoftware/guards
Adding Guards 👮
2015-10-24 13:28:47 -07:00
Kyle Fuller
d5acc7298c Switch to Conche and Spectre 2015-10-22 21:13:26 -07:00
Kyle Fuller
6464b3170a Improve support for native Swift types 2015-10-22 11:42:50 -07:00
Kyle Fuller
d03df12cba Move to JSON podspec 2015-10-22 10:05:46 -07:00
Kyle Fuller
62f6016e94 [README] Syntax highlight 2015-10-22 09:49:26 -07:00
Kyle Fuller
16da9ac034 Introduce variable filters 2015-10-22 09:47:45 -07:00
Kyle Fuller
7d5d226017 Include missing import to Foundation 2015-10-21 21:59:40 -07:00
Olivier Halligon
05dc420808 Add more safeguards 🚓🛂 2015-10-18 22:30:33 +02:00
Olivier Halligon
f4ed872a45 guard all the things! 👮 2015-10-18 22:30:25 +02:00
Kyle Fuller
f0abd34c32 [Template] Throw when initialising with non-existant file 2015-10-18 10:53:10 -07:00
Kyle Fuller
4d76fb4e60 [Template] Include tests for bundle/url initialiser 2015-10-18 09:41:16 -07:00
Kyle Fuller
9bdef5fee0 Merge pull request #34 from AliSoftware/fix/public
Made Token's `components()` & `contents` functions public
2015-10-18 09:12:30 -07:00
Olivier Halligon
20cc95fb87 Made Token's components & contents functions public so we can use it to implement custom tags with registerTag() 2015-10-18 15:48:03 +02:00
Kyle Fuller
1136ca8fca [Template] Set default value for bundle initialiser 2015-10-02 12:20:42 -07:00
Kyle Fuller
8f334563bf Merge pull request #30 from chunkerchunker/master
Allow Template.render() to be called multiple times
2015-10-02 12:15:49 -07:00
Andy Choi
b03ec50a42 Allow Template.render() to be called multiple times
Allow Template.render() to be called multiple times, for the use case where a single template is rendered against multiple Contexts.
2015-09-30 20:56:54 -07:00
Kyle Fuller
2ab9b85305 Merge pull request #29 from chunkerchunker/template-url-init
bugfix for loading Template from URL
2015-09-29 17:08:18 -07:00
chunkerchunker
a297b4ec42 bugfix for loading Template from URL
NSURL.absoluteString includes "file://", which Path() doesn't expect.
2015-09-29 16:54:07 -07:00
Kyle Fuller
9de84d5ca4 [Circle] Deploy on all tags 2015-09-25 15:13:32 -07:00
Kyle Fuller
e5378b7603 [circleci] Update tag regex 2015-09-25 15:09:02 -07:00
Kyle Fuller
29dc14855c Release 0.3.0 2015-09-25 15:06:18 -07:00
Kyle Fuller
3935dac021 Publish releases to Circle CI 2015-09-25 15:03:31 -07:00
Kyle Fuller
9c335caeb6 Remove custom Result type and throw errors 2015-09-25 12:53:45 -07:00
Kyle Fuller
25f5583542 [Template] Initialisers to throw on failure 2015-09-25 11:17:04 -07:00
Kyle Fuller
878c5cfde8 Use modern Swift 2.0 2015-09-25 10:40:58 -07:00
Kyle Fuller
a0bde992c2 Upgrade to PathKit ~> 0.4.0 2015-09-25 10:26:55 -07:00
Kyle Fuller
554d2ee07f [circleci] Use Xcode 7 2015-09-14 16:05:33 -07:00
Kyle Fuller
4dc8bf3d1f Merge pull request #26 from kylef/swift-2.0
Switch to Swift 2.0
2015-09-09 18:55:23 -07:00
Kyle Fuller
dcf2611ac2 Switch to Swift 2.0 2015-09-08 18:44:01 -07:00
Kyle Fuller
c1a485c429 Merge pull request #24 from kattrali/patch-1
Fix title typo
2015-08-24 02:23:22 -07:00
Delisa Mason
f9006d515a Fix title typo 2015-08-24 00:10:18 -07:00
Kyle Fuller
6d01792cd6 Release 0.2.0 2015-06-29 21:45:28 -07:00
Kyle Fuller
620154e721 Include template inheritence
Closes #15
2015-06-29 18:36:27 -07:00
Kyle Fuller
53d5a4f8c3 [Project] Use 2 spaces for indentation 2015-06-29 16:40:15 -07:00
Kyle Fuller
59bab00c97 [README] Fix broken badge URI 2015-06-29 16:37:20 -07:00
Kyle Fuller
3839bc4147 Merge pull request #23 from kylef/kylef/swift-1.2
Support Swift 1.2
2015-06-29 16:36:41 -07:00
Kyle Fuller
84f117b40c Revert "Implement universal framework"
This reverts commit 5a080f92cc.

Conflicts:
	.travis.yml
	Stencil.xcodeproj/project.pbxproj
2015-06-29 16:33:30 -07:00
Kyle Fuller
d1d8e6e17f [Podspec] Include missing TemplateLoader source 2015-06-29 16:20:12 -07:00
Kyle Fuller
44810a82e7 [Podspec] Target iOS > 8.0 2015-06-29 16:16:50 -07:00
Kyle Fuller
028b340b54 Switch to Circle CI 2015-06-29 16:11:54 -07:00
Kyle Fuller
19a7abce4c Support Swift 1.2 2015-06-29 15:47:33 -07:00
Kyle Fuller
0212e8781c [travis] Quickly lint podspec 2014-12-29 03:31:35 +00:00
Kyle Fuller
5aac08cabf [travis] Lint podspec 2014-12-29 03:22:18 +00:00
Kyle Fuller
45fcebec57 [README] Add installation instructions
Closes #21
2014-12-29 00:54:52 +00:00
Kyle Fuller
fa34c2a98e Add an include tag 2014-12-29 00:19:06 +00:00
Kyle Fuller
1989c20932 Add a Template Loader 2014-12-29 00:19:06 +00:00
Kyle Fuller
2374786963 Release 0.1.1
Closes #18
2014-12-25 23:38:57 +00:00
Kyle Fuller
5a080f92cc Implement universal framework 2014-12-25 23:35:55 +00:00
Kyle Fuller
5d61043f8c Add architecture document and docs 2014-11-30 00:04:37 +00:00
Kyle Fuller
017692a0dc Merge pull request #19 from pyanfield/master
update readme file
2014-11-22 02:02:35 +01:00
pyanfield
0710c404b5 update readme file 2014-11-22 08:59:08 +08:00
Kyle Fuller
529e357334 Merge pull request #17 from segiddins/seg-fix-ios-objc
[Stencil.h] Allow using stencil from ObjC on iOS
2014-11-18 00:31:57 +01:00
Samuel E. Giddins
c70510b3a5 [Stencil.h] Allow using stencil from ObjC on iOS 2014-11-17 15:30:20 -08:00
Kyle Fuller
63f6b3a950 Merge pull request #13 from kylef/travis
Enable travis-ci
2014-11-06 00:45:38 +00:00
Kyle Fuller
b3d551962a [travis] Use Xcode 6.1 2014-11-06 00:39:00 +00:00
Kyle Fuller
2495d1e781 Merge pull request #14 from jpsim/readme-proofread
Minor README edits (typo's, grammar)
2014-10-26 20:09:55 +00:00
JP Simard
068b4ffe13 Minor README edits (typo's, grammar) 2014-10-26 12:13:52 -07:00
Kyle Fuller
914d45827f [README] Add a note about Xcode 6.1 2014-10-26 18:39:26 +00:00
Kyle Fuller
3a0e4ce17e Enable travis-ci 2014-10-26 18:34:46 +00:00
63 changed files with 2289 additions and 1884 deletions

29
.gitignore vendored
View File

@@ -1,26 +1,3 @@
# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
#
# Pods/
.conche/
.build/
Packages/

1
.swift-version Normal file
View File

@@ -0,0 +1 @@
DEVELOPMENT-SNAPSHOT-2016-01-25-a

11
.travis.yml Normal file
View 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

175
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,175 @@
Stencil Architecture
====================
This document outlines the architecture of Stencil and how it works internally.
Stencil uses a three-step process for rendering templates. The first step is tokenising the template into an array of Tokens. Afterwards, the array of tokens are transformed into a collection of Nodes. Once we have a collection of Nodes (objects conforming to the `Node` protocol), we then call `render(context)` on each Node instructing it to render itself inside the given context.
## Token
Token is an enum which has four members. These represent a piece of text, a variable, a comment or a template block. They are parsed using the `TokenParser` which takes the template as a string as input and returns an array of Tokens.
### Values
#### Text
A text token represents a string which will be rendered in the template. For example, a text token with the string `Hello World` will be rendered as such in the output.
#### Variable
A variable token represents a variable inside a context. It will be evaluated and rendered in the output. It is created from the template using `{{ string }}`.
#### Comment
The comment token represents a comment inside the source. It is created using `{# This is a comment #}`.
#### Block
A block represents a template tag. It is created using `{% this is a template block %}` inside a template. The template tag in this case would be called `this`. See “Block Token” below for more information.
### Parsing
A template is parsed using the TokenParser into an array of Tokens. For example:
```html+django
Hello {{ name }}
```
Would be parsed into two tokens. A token representing the string, `Hello ` and a token representing the variable called `name`. So, in Swift it would be represented as follows:
```swift
let tokens = [
Token.Text("Hello "),
Token.Variable("name"),
]
```
## Node
Node is a protocol with a single method, to render it inside a context. When rendering a node, it is converted into the output string, or an error if there is a failure. Tokens are converted to Nodes using the `TokenParser` class.
For some Tokens, there is a direct mapping from a Token to a Node. However block nodes do not have a 1:1 mapping.
### Token Parsing
#### Text Token
A text token is converted directly to a `TextNode` which simply returns the text when rendered.
#### Variable Token
Variable Tokens are transformed directly to a `VariableNode`, which will evaluate a variable in the given template when rendered.
#### Comment Token
A comment token is simply omitted, a comment token will be dropped when it is converted to a Node.
#### Block Token
Block tokens are slightly different from the other tokens, there is no direct mapping. A block token is made up of a string representing the token. For example `now` or `for article in articles`. The `TokenParser` will pull out the first word inside the string representation and use that to look-up a parser for the block. So, in this example, the template tag names will be `now` or `for`.
The template tags are registered with a block of code which deals with the parsing for the given tag. This allows the parser to parse a set of tokens ahead of the block tone. This is useful for control flow, such as the `for` template tag will want to parse any following tokens up until the `endblock` block token.
For example:
```html+django
{% for article in articles %}
An Article
{% endfor %}
```
Or as a set of tokens:
```swift
let tokens = [
Token.Block("for article in articles"),
Token.Text(" An Article")
Token.Block("endfor")
]
```
Will result in a single Node (a `ForNode`) which contains the sub-node containing the text. The `ForNode` class has a property called `forNodes` which contains the text node representing the text token (` An Article`).
When the `ForNode` is rendered in a context, it will look up the variable `articles` and if its an array it will loop over it. Inserting the variable `article` into the context while rendered the `forNodes` for each article.
### Custom Nodes
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 its 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 with a `Namespace` passed when rendering your `Template`.
#### Simple Tags
A simple tag is registered with a string for the tag name and a block of code which is evaluated when the block is rendered in a given context.
Heres an example. Registering a template tag called `custom` which just renders `Hello World` in the rendered template:
```swift
namespace.registerSimpleTag("custom") { context in
return "Hello World"
}
```
You would use it as such in a template:
```html+django
{% custom %}
```
#### Advanced Tags
If you need more control or functionality than the simple tags 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 `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 nodes further in the token array.
As an example, were 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.
```html+django
{% debug %}
Debugging is enabled!
{% enddebug %}
```
This will be represented by a `DebugNode` which will have a property containing all of the Nodes inside the `debug`/`enddebug` block. In the above example, this will just be a TextNode containing ` Debugging is enabled!`.
When the `DebugNode` is rendered, it will determine if debug is enabled by introspecting the context and if it is enabled. We will render the nodes, otherwise just return an empty string to hide the debug output.
So, our `DebugNode` would look like as following:
```swift
class DebugNode : Node {
let nodes:[Node]
init(nodes:[Node]) {
self.nodes = nodes
}
func render(context: Context) throws -> String {
// Is there a debug variable inside the context?
if let debug = context["debug"] as? Bool {
// Is debug set to true?
if debug {
// Let's render the given nodes in the context since debug is enabled.
return renderNodes(nodes, context)
}
}
// Debug is turned off, so let's not render anything
return ""
}
}
```
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
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)
}
```
## Context
A Context is a structure containing any templates you would like to use in a template. Its somewhat like a dictionary, however you can push and pop to scope variables. So that means that when iterating over a for loop, you can push a new scope into the context to store any variables local to the scope.

7
Makefile Normal file
View 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
View 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),
]
)

140
README.md
View File

@@ -1,11 +1,12 @@
Stencil
=======
# Stencil
[![Build Status](https://travis-ci.org/kylef/Stencil.svg?branch=master)](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
feel right at home with Stencil.
### Example
## Example
```html+django
There are {{ articles.count }} articles.
@@ -17,23 +18,29 @@ There are {{ articles.count }} articles.
```swift
let context = Context(dictionary: [
"articles": [
[ "title": "Migrating from OCUnit to XCTest", "author": "Kyle Fuller" ],
[ "title": "Memory Management with ARC", "author": "Kyle Fuller" ],
]
"articles": [
[ "title": "Migrating from OCUnit to XCTest", "author": "Kyle Fuller" ],
[ "title": "Memory Management with ARC", "author": "Kyle Fuller" ],
]
])
let template = Template(named: "template.stencil")
let result = template.render(context)
switch result {
case .Error(let error):
println("There was an error rendering your template (\(error)).")
case .Success(let string):
println("\(string)")
do {
let template = try Template(named: "template.stencil")
let rendered = try template.render(context)
print(rendered)
} catch {
print("Failed to render template \(error)")
}
```
## Installation
Installation with CocoaPods is recommended.
```ruby
pod 'Stencil'
```
## Philosophy
Stencil follows the same philosophy of Django:
@@ -66,13 +73,48 @@ 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
Filters allow you to transform the values of variables. For example, they look like:
```html+django
{{ variable|uppercase }}
```
##### Capitalize
The capitalize filter allows you to capitalize a string.
For example, `stencil` to `Stencil`.
```html+django
{{ "stencil"|capitalize }}
```
##### Uppercase
The uppercase filter allows you to transform a string to uppercase.
For example, `Stencil` to `STENCIL`.
```html+django
{{ "Stencil"|uppercase }}
```
##### Lowercase
The uppercase filter allows you to transform a string to lowercase.
For example, `Stencil` to `stencil`.
```html+django
{{ "Stencil"|lowercase }}
```
### Tags
Tags are a mechanism to execute a piece of code which can allows you to have
Tags are a mechanism to execute a piece of code, allowing you to have
control flow within your template.
```html+django
@@ -81,7 +123,7 @@ control flow within your template.
{% endif %}
```
A tag can also effect the context and define variables as follows:
A tag can also affect the context and define variables as follows:
```html+django
{% for item in items %}
@@ -89,8 +131,8 @@ A tag can also effect the context and define variables as follows:
{% endfor %}
```
Stencil has a couple of built in tags which are listed below. You can also
extend Stencil and provide your own tags.
Stencil has a couple of built-in tags which are listed below. You can also
extend Stencil by providing your own tags.
#### for
@@ -100,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 %}
```
@@ -124,29 +166,72 @@ A for loop allows you to iterate over an array found by variable lookup.
{% endif %}
```
#### include
You can include another template using the `include` tag.
```html+django
{% include "comment.html" %}
```
The `include` tag requires a TemplateLoader to be found inside your context with the paths, or bundles used to lookup the template.
```swift
let context = Context(dictionary: [
"loader": TemplateLoader(bundle:[NSBundle.mainBundle()])
])
```
### 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
return .Success("Hello World")
namespace.registerSimpleTag("custom") { context in
return "Hello World"
}
```
When your tag is used via `{% custom %}` it will execute the registered block
of code allowing you to modify or retrieve a value from the context. Then
return either a string rendered in your template, or an error.
return either a string rendered in your template, or throw an error.
If you want to accept arguments or to capture different tokens between two sets
of template tags. You will need to the `registerTag` API which accepts a
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`.
The architecture of Stencil along with how to build advanced plugins can be
found in the [architecture](ARCHITECTURE.md) document.
### Comments
To comment out part of your template you can use the following syntax:
To comment out part of your template, you can use the following syntax:
```html+django
{# My comment is completely hidden #}
@@ -156,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.

55
Sources/Context.swift Normal file
View File

@@ -0,0 +1,55 @@
/// A container for template variables.
public class Context {
var dictionaries: [[String: Any]]
let namespace: Namespace
/// 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 = []
}
self.namespace = namespace
}
public subscript(key: String) -> Any? {
/// Retrieves a variable's value, starting at the current context and going upwards
get {
for dictionary in Array(dictionaries.reverse()) {
if let value = dictionary[key] {
return value
}
}
return nil
}
/// Set a variable in the current context, deleting the variable if it's nil
set(value) {
if let dictionary = dictionaries.popLast() {
var mutable_dictionary = dictionary
mutable_dictionary[key] = value
dictionaries.append(mutable_dictionary)
}
}
}
/// Push a new level into the Context
private func push(dictionary: [String: Any]? = nil) {
dictionaries.append(dictionary ?? [:])
}
/// Pop the last level off of the Context
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)
defer { pop() }
return try closure()
}
}

33
Sources/Filters.swift Normal file
View File

@@ -0,0 +1,33 @@
func toString(value: Any?) -> String? {
if let value = value as? String {
return value
} else if let value = value as? CustomStringConvertible {
return value.description
}
return nil
}
func capitalise(value: Any?) -> Any? {
if let value = toString(value) {
return value.capitalizedString
}
return value
}
func uppercase(value: Any?) -> Any? {
if let value = toString(value) {
return value.uppercaseString
}
return value
}
func lowercase(value: Any?) -> Any? {
if let value = toString(value) {
return value.lowercaseString
}
return value
}

62
Sources/ForTag.swift Normal file
View 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
View 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)
}
}
}
}

38
Sources/Include.swift Normal file
View File

@@ -0,0 +1,38 @@
import PathKit
public class IncludeNode : NodeType {
public let templateName: Variable
public class func parse(parser: TokenParser, token: Token) throws -> NodeType {
let bits = token.components()
guard bits.count == 2 else {
throw TemplateSyntaxError("'include' tag takes one argument, the template file to be included")
}
return IncludeNode(templateName: Variable(bits[1]))
}
public init(templateName: Variable) {
self.templateName = templateName
}
public func render(context: Context) throws -> String {
guard let loader = context["loader"] as? TemplateLoader else {
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)")
}
return try template.render(context)
}
}

113
Sources/Inheritence.swift Normal file
View File

@@ -0,0 +1,113 @@
class BlockContext {
class var contextKey: String { return "block_context" }
var blocks: [String:BlockNode]
init(blocks: [String:BlockNode]) {
self.blocks = blocks
}
func pop(blockName: String) -> BlockNode? {
return blocks.removeValueForKey(blockName)
}
}
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: Variable
let blocks: [String:BlockNode]
class func parse(parser: TokenParser, token: Token) throws -> NodeType {
let bits = token.components()
guard bits.count == 2 else {
throw TemplateSyntaxError("'extends' takes one argument, the template file to be extended")
}
let parsedNodes = try parser.parse()
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]()) { (accumulator, node:NodeType) -> [String:BlockNode] in
let node = (node as! BlockNode)
var dict = accumulator
dict[node.name] = node
return dict
}
return ExtendsNode(templateName: Variable(bits[1]), blocks: nodes)
}
init(templateName: Variable, blocks: [String: BlockNode]) {
self.templateName = templateName
self.blocks = blocks
}
func render(context: Context) throws -> String {
guard let loader = context["loader"] as? TemplateLoader else {
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)
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 {
let bits = token.components()
guard bits.count == 2 else {
throw TemplateSyntaxError("'block' tag takes one argument, the template file to be included")
}
let blockName = bits[1]
let nodes = try parser.parse(until(["endblock"]))
parser.nextToken()
return BlockNode(name:blockName, nodes:nodes)
}
init(name: String, nodes: [NodeType]) {
self.name = name
self.nodes = nodes
}
func render(context: Context) throws -> String {
if let blockContext = context[BlockContext.contextKey] as? BlockContext, node = blockContext.pop(name) {
return try node.render(context)
}
return try renderNodes(nodes, context)
}
}

147
Sources/Lexer.swift Normal file
View 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
View 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
View 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
View 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

91
Sources/Parser.swift Normal file
View File

@@ -0,0 +1,91 @@
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
}
}
public typealias Filter = Any? throws -> Any?
/// A class for parsing an array of tokens and converts them into a collection of Node's
public class TokenParser {
public typealias TagParser = (TokenParser, Token) throws -> NodeType
private var tokens: [Token]
private let namespace: Namespace
public init(tokens: [Token], namespace: Namespace) {
self.tokens = tokens
self.namespace = namespace
}
/// Parse the given tokens into nodes
public func parse() throws -> [NodeType] {
return try parse(nil)
}
public func parse(parse_until:((parser:TokenParser, token:Token) -> (Bool))?) throws -> [NodeType] {
var nodes = [NodeType]()
while tokens.count > 0 {
let token = nextToken()!
switch token {
case .Text(let text):
nodes.append(TextNode(text: text))
case .Variable:
nodes.append(VariableNode(variable: try compileFilter(token.contents)))
case .Block:
let tag = token.components().first
if let parse_until = parse_until where parse_until(parser: self, token: token) {
prependToken(token)
return nodes
}
if let tag = tag {
if let parser = namespace.tags[tag] {
nodes.append(try parser(self, token))
} else {
throw TemplateSyntaxError("Unknown template tag '\(tag)'")
}
}
case .Comment:
continue
}
}
return nodes
}
public func nextToken() -> Token? {
if tokens.count > 0 {
return tokens.removeAtIndex(0)
}
return nil
}
public func prependToken(token:Token) {
tokens.insert(token, atIndex: 0)
}
public func findFilter(name: String) throws -> Filter {
if let filter = namespace.filters[name] {
return filter
}
throw TemplateSyntaxError("Invalid filter '\(name)'")
}
func compileFilter(token: String) throws -> Resolvable {
return try FilterExpression(token: token, parser: self)
}
}

45
Sources/Template.swift Normal file
View File

@@ -0,0 +1,45 @@
import Foundation
import PathKit
#if os(Linux)
let NSFileNoSuchFileError = 4
#endif
/// A class representing a template
public class Template {
let tokens: [Token]
/// Create a template with the given name inside the given bundle
public convenience init(named:String, inBundle bundle:NSBundle? = nil) throws {
let useBundle = bundle ?? NSBundle.mainBundle()
guard let url = useBundle.URLForResource(named, withExtension: nil) else {
throw NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError, userInfo: nil)
}
try self.init(URL:url)
}
/// Create a template with a file found at the given URL
public convenience init(URL:NSURL) throws {
try self.init(path: Path(URL.path!))
}
/// Create a template with a file found at the given path
public convenience init(path:Path) throws {
self.init(templateString: try path.read())
}
/// Create a template with a template string
public init(templateString:String) {
let lexer = Lexer(templateString: templateString)
tokens = lexer.tokenize()
}
/// Render the given template
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)
}
}

View File

@@ -0,0 +1,38 @@
import Foundation
import PathKit
// A class for loading a template from disk
public class TemplateLoader {
public let paths: [Path]
public init(paths: [Path]) {
self.paths = paths
}
public init(bundle: [NSBundle]) {
self.paths = bundle.map {
return Path($0.bundlePath)
}
}
public func loadTemplate(templateName: String) -> Template? {
return loadTemplate([templateName])
}
public func loadTemplate(templateNames: [String]) -> Template? {
for path in paths {
for templateName in templateNames {
let templatePath = path + Path(templateName)
if templatePath.exists {
if let template = try? Template(path: templatePath) {
return template
}
}
}
}
return nil
}
}

93
Sources/Tokenizer.swift Normal file
View File

@@ -0,0 +1,93 @@
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)
/// A token representing a variable.
case Variable(value: String)
/// A token representing a comment.
case Comment(value: String)
/// A token representing a template block.
case Block(value: String)
/// Returns the underlying value as an array seperated by spaces
public func components() -> [String] {
switch self {
case .Block(let value):
return smartSplit(value)
case .Variable(let value):
return smartSplit(value)
case .Text(let value):
return smartSplit(value)
case .Comment(let value):
return smartSplit(value)
}
}
public var contents: String {
switch self {
case .Block(let value):
return value
case .Variable(let value):
return value
case .Text(let value):
return value
case .Comment(let value):
return value
}
}
}
public func == (lhs: Token, rhs: Token) -> Bool {
switch (lhs, rhs) {
case (.Text(let lhsValue), .Text(let rhsValue)):
return lhsValue == rhsValue
case (.Variable(let lhsValue), .Variable(let rhsValue)):
return lhsValue == rhsValue
case (.Block(let lhsValue), .Block(let rhsValue)):
return lhsValue == rhsValue
case (.Comment(let lhsValue), .Comment(let rhsValue)):
return lhsValue == rhsValue
default:
return false
}
}

133
Sources/Variable.swift Normal file
View File

@@ -0,0 +1,133 @@
import Foundation
class FilterExpression : Resolvable {
let filters: [Filter]
let variable: Variable
init(token: String, parser: TokenParser) throws {
let bits = token.characters.split("|").map({ String($0).trim(" ") })
if bits.isEmpty {
filters = []
variable = Variable("")
throw TemplateSyntaxError("Variable tags must include at least 1 argument")
}
variable = Variable(bits[0])
let filterBits = bits[1 ..< bits.endIndex]
do {
filters = try filterBits.map { try parser.findFilter($0) }
} catch {
filters = []
throw error
}
}
func resolve(context: Context) throws -> Any? {
let result = try variable.resolve(context)
return try filters.reduce(result) { x, y in
return try y(x)
}
}
}
/// 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
/// Create a variable with a string representing the variable
public init(_ variable: String) {
self.variable = variable
}
private func lookup() -> [String] {
return variable.characters.split(".").map(String.init)
}
/// Resolve the variable in the given context
public func resolve(context: Context) throws -> Any? {
var current: Any? = context
if (variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\"")) {
// String literal
return variable[variable.startIndex.successor() ..< variable.endIndex.predecessor()]
}
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 array = current as? [Any] {
if let index = Int(bit) {
current = array[index]
} else if bit == "first" {
current = array.first
} else if bit == "last" {
current = array.last
} else if bit == "count" {
current = array.count
}
} else if let object = current as? NSObject { // NSKeyValueCoding
#if os(Linux)
return nil
#else
current = object.valueForKey(bit)
#endif
} else {
return nil
}
}
return normalize(current)
}
}
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
}
}

View File

@@ -1,13 +0,0 @@
Pod::Spec.new do |spec|
spec.name = 'Stencil'
spec.version = '0.1.0'
spec.summary = 'Stencil is a simple and powerful template language for Swift.'
spec.homepage = 'https://github.com/kylef/Stencil'
spec.license = { :type => 'BSD', :file => 'LICENSE' }
spec.author = { 'Kyle Fuller' => 'inbox@kylefuller.co.uk' }
spec.social_media_url = 'http://twitter.com/kylefuller'
spec.source = { :git => 'https://github.com/kylef/Stencil.git', :tag => "#{spec.version}" }
spec.source_files = 'Stencil/*.{h,swift}'
spec.requires_arc = true
end

29
Stencil.podspec.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "Stencil",
"version": "0.6.0-beta.1",
"summary": "Stencil is a simple and powerful template language for Swift.",
"homepage": "https://github.com/kylef/Stencil",
"license": {
"type": "BSD",
"file": "LICENSE"
},
"authors": {
"Kyle Fuller": "kyle@fuller.li"
},
"social_media_url": "http://twitter.com/kylefuller",
"source": {
"git": "https://github.com/kylef/Stencil.git",
"tag": "0.6.0-beta.1"
},
"source_files": [
"Sources/*.swift"
],
"platforms": {
"ios": "8.0",
"osx": "10.9"
},
"requires_arc": true,
"dependencies": {
"PathKit": [ "~> 0.6.0" ]
}
}

View File

@@ -1,473 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
71CE4C0A19FD29D000B9E0C5 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE4C0919FD29D000B9E0C5 /* Result.swift */; };
7725B3CB19F92B4F002CF74B /* VariableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3CA19F92B4F002CF74B /* VariableTests.swift */; };
7725B3CD19F92B61002CF74B /* Variable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3CC19F92B61002CF74B /* Variable.swift */; };
7725B3CF19F94214002CF74B /* Tokenizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3CE19F94214002CF74B /* Tokenizer.swift */; };
7725B3D319F9437F002CF74B /* NodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3D219F9437F002CF74B /* NodeTests.swift */; };
7725B3D519F9438F002CF74B /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3D419F9438F002CF74B /* Node.swift */; };
7725B3D719F94A43002CF74B /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3D619F94A43002CF74B /* Parser.swift */; };
7725B3D919F94A61002CF74B /* ParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3D819F94A61002CF74B /* ParserTests.swift */; };
77EB082519F96E88001870F1 /* Template.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77EB082419F96E88001870F1 /* Template.swift */; };
77EB082719F96E9C001870F1 /* TemplateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77EB082619F96E9C001870F1 /* TemplateTests.swift */; };
77EB082919FA85F2001870F1 /* LexerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77EB082819FA85F2001870F1 /* LexerTests.swift */; };
77EB082B19FA8600001870F1 /* Lexer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77EB082A19FA8600001870F1 /* Lexer.swift */; };
77FAAE5819F91E480029DC5E /* Stencil.h in Headers */ = {isa = PBXBuildFile; fileRef = 77FAAE5719F91E480029DC5E /* Stencil.h */; settings = {ATTRIBUTES = (Public, ); }; };
77FAAE5E19F91E480029DC5E /* Stencil.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77FAAE5219F91E480029DC5E /* Stencil.framework */; };
77FAAE6519F91E480029DC5E /* StencilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77FAAE6419F91E480029DC5E /* StencilTests.swift */; };
77FAAE6F19F920750029DC5E /* Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77FAAE6E19F920750029DC5E /* Context.swift */; };
77FAAE7119F9208C0029DC5E /* ContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77FAAE7019F9208C0029DC5E /* ContextTests.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
77FAAE5F19F91E480029DC5E /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 77FAAE4919F91E480029DC5E /* Project object */;
proxyType = 1;
remoteGlobalIDString = 77FAAE5119F91E480029DC5E;
remoteInfo = Stencil;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
71CE4C0919FD29D000B9E0C5 /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = "<group>"; };
7725B3CA19F92B4F002CF74B /* VariableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VariableTests.swift; sourceTree = "<group>"; };
7725B3CC19F92B61002CF74B /* Variable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Variable.swift; sourceTree = "<group>"; };
7725B3CE19F94214002CF74B /* Tokenizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tokenizer.swift; sourceTree = "<group>"; };
7725B3D219F9437F002CF74B /* NodeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeTests.swift; sourceTree = "<group>"; };
7725B3D419F9438F002CF74B /* Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = "<group>"; };
7725B3D619F94A43002CF74B /* Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
7725B3D819F94A61002CF74B /* ParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParserTests.swift; sourceTree = "<group>"; };
77EB082419F96E88001870F1 /* Template.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Template.swift; sourceTree = "<group>"; };
77EB082619F96E9C001870F1 /* TemplateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TemplateTests.swift; sourceTree = "<group>"; };
77EB082819FA85F2001870F1 /* LexerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LexerTests.swift; sourceTree = "<group>"; };
77EB082A19FA8600001870F1 /* Lexer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Lexer.swift; sourceTree = "<group>"; };
77FAAE5219F91E480029DC5E /* Stencil.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Stencil.framework; sourceTree = BUILT_PRODUCTS_DIR; };
77FAAE5619F91E480029DC5E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
77FAAE5719F91E480029DC5E /* Stencil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Stencil.h; sourceTree = "<group>"; };
77FAAE5D19F91E480029DC5E /* StencilTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StencilTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
77FAAE6319F91E480029DC5E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
77FAAE6419F91E480029DC5E /* StencilTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StencilTests.swift; sourceTree = "<group>"; };
77FAAE6E19F920750029DC5E /* Context.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Context.swift; sourceTree = "<group>"; };
77FAAE7019F9208C0029DC5E /* ContextTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
77FAAE4E19F91E480029DC5E /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
77FAAE5A19F91E480029DC5E /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
77FAAE5E19F91E480029DC5E /* Stencil.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
77FAAE4819F91E480029DC5E = {
isa = PBXGroup;
children = (
77FAAE5419F91E480029DC5E /* Stencil */,
77FAAE6119F91E480029DC5E /* StencilTests */,
77FAAE5319F91E480029DC5E /* Products */,
);
sourceTree = "<group>";
};
77FAAE5319F91E480029DC5E /* Products */ = {
isa = PBXGroup;
children = (
77FAAE5219F91E480029DC5E /* Stencil.framework */,
77FAAE5D19F91E480029DC5E /* StencilTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
77FAAE5419F91E480029DC5E /* Stencil */ = {
isa = PBXGroup;
children = (
77FAAE5719F91E480029DC5E /* Stencil.h */,
77FAAE6E19F920750029DC5E /* Context.swift */,
77EB082A19FA8600001870F1 /* Lexer.swift */,
7725B3D419F9438F002CF74B /* Node.swift */,
7725B3D619F94A43002CF74B /* Parser.swift */,
71CE4C0919FD29D000B9E0C5 /* Result.swift */,
77EB082419F96E88001870F1 /* Template.swift */,
7725B3CE19F94214002CF74B /* Tokenizer.swift */,
7725B3CC19F92B61002CF74B /* Variable.swift */,
77FAAE5519F91E480029DC5E /* Supporting Files */,
);
path = Stencil;
sourceTree = "<group>";
};
77FAAE5519F91E480029DC5E /* Supporting Files */ = {
isa = PBXGroup;
children = (
77FAAE5619F91E480029DC5E /* Info.plist */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
77FAAE6119F91E480029DC5E /* StencilTests */ = {
isa = PBXGroup;
children = (
77FAAE6419F91E480029DC5E /* StencilTests.swift */,
77FAAE7019F9208C0029DC5E /* ContextTests.swift */,
7725B3CA19F92B4F002CF74B /* VariableTests.swift */,
7725B3D219F9437F002CF74B /* NodeTests.swift */,
7725B3D819F94A61002CF74B /* ParserTests.swift */,
77EB082819FA85F2001870F1 /* LexerTests.swift */,
77EB082619F96E9C001870F1 /* TemplateTests.swift */,
77FAAE6219F91E480029DC5E /* Supporting Files */,
);
path = StencilTests;
sourceTree = "<group>";
};
77FAAE6219F91E480029DC5E /* Supporting Files */ = {
isa = PBXGroup;
children = (
77FAAE6319F91E480029DC5E /* Info.plist */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
77FAAE4F19F91E480029DC5E /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
77FAAE5819F91E480029DC5E /* Stencil.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
77FAAE5119F91E480029DC5E /* Stencil */ = {
isa = PBXNativeTarget;
buildConfigurationList = 77FAAE6819F91E480029DC5E /* Build configuration list for PBXNativeTarget "Stencil" */;
buildPhases = (
77FAAE4D19F91E480029DC5E /* Sources */,
77FAAE4E19F91E480029DC5E /* Frameworks */,
77FAAE4F19F91E480029DC5E /* Headers */,
77FAAE5019F91E480029DC5E /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = Stencil;
productName = Stencil;
productReference = 77FAAE5219F91E480029DC5E /* Stencil.framework */;
productType = "com.apple.product-type.framework";
};
77FAAE5C19F91E480029DC5E /* StencilTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 77FAAE6B19F91E480029DC5E /* Build configuration list for PBXNativeTarget "StencilTests" */;
buildPhases = (
77FAAE5919F91E480029DC5E /* Sources */,
77FAAE5A19F91E480029DC5E /* Frameworks */,
77FAAE5B19F91E480029DC5E /* Resources */,
);
buildRules = (
);
dependencies = (
77FAAE6019F91E480029DC5E /* PBXTargetDependency */,
);
name = StencilTests;
productName = StencilTests;
productReference = 77FAAE5D19F91E480029DC5E /* StencilTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
77FAAE4919F91E480029DC5E /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0600;
ORGANIZATIONNAME = Cocode;
TargetAttributes = {
77FAAE5119F91E480029DC5E = {
CreatedOnToolsVersion = 6.1;
};
77FAAE5C19F91E480029DC5E = {
CreatedOnToolsVersion = 6.1;
};
};
};
buildConfigurationList = 77FAAE4C19F91E480029DC5E /* Build configuration list for PBXProject "Stencil" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
);
mainGroup = 77FAAE4819F91E480029DC5E;
productRefGroup = 77FAAE5319F91E480029DC5E /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
77FAAE5119F91E480029DC5E /* Stencil */,
77FAAE5C19F91E480029DC5E /* StencilTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
77FAAE5019F91E480029DC5E /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
77FAAE5B19F91E480029DC5E /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
77FAAE4D19F91E480029DC5E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
77FAAE6F19F920750029DC5E /* Context.swift in Sources */,
77EB082B19FA8600001870F1 /* Lexer.swift in Sources */,
7725B3CF19F94214002CF74B /* Tokenizer.swift in Sources */,
7725B3D719F94A43002CF74B /* Parser.swift in Sources */,
77EB082519F96E88001870F1 /* Template.swift in Sources */,
7725B3CD19F92B61002CF74B /* Variable.swift in Sources */,
71CE4C0A19FD29D000B9E0C5 /* Result.swift in Sources */,
7725B3D519F9438F002CF74B /* Node.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
77FAAE5919F91E480029DC5E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
77FAAE6519F91E480029DC5E /* StencilTests.swift in Sources */,
7725B3D319F9437F002CF74B /* NodeTests.swift in Sources */,
7725B3D919F94A61002CF74B /* ParserTests.swift in Sources */,
77EB082719F96E9C001870F1 /* TemplateTests.swift in Sources */,
7725B3CB19F92B4F002CF74B /* VariableTests.swift in Sources */,
77EB082919FA85F2001870F1 /* LexerTests.swift in Sources */,
77FAAE7119F9208C0029DC5E /* ContextTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
77FAAE6019F91E480029DC5E /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 77FAAE5119F91E480029DC5E /* Stencil */;
targetProxy = 77FAAE5F19F91E480029DC5E /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
77FAAE6619F91E480029DC5E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.10;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Debug;
};
77FAAE6719F91E480029DC5E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = YES;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.10;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Release;
};
77FAAE6919F91E480029DC5E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
COMBINE_HIDPI_IMAGES = YES;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
FRAMEWORK_VERSION = A;
INFOPLIST_FILE = Stencil/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
77FAAE6A19F91E480029DC5E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
COMBINE_HIDPI_IMAGES = YES;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
FRAMEWORK_VERSION = A;
INFOPLIST_FILE = Stencil/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Release;
};
77FAAE6C19F91E480029DC5E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
COMBINE_HIDPI_IMAGES = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(DEVELOPER_FRAMEWORKS_DIR)",
"$(inherited)",
);
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
INFOPLIST_FILE = StencilTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
};
77FAAE6D19F91E480029DC5E /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
COMBINE_HIDPI_IMAGES = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(DEVELOPER_FRAMEWORKS_DIR)",
"$(inherited)",
);
INFOPLIST_FILE = StencilTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
77FAAE4C19F91E480029DC5E /* Build configuration list for PBXProject "Stencil" */ = {
isa = XCConfigurationList;
buildConfigurations = (
77FAAE6619F91E480029DC5E /* Debug */,
77FAAE6719F91E480029DC5E /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
77FAAE6819F91E480029DC5E /* Build configuration list for PBXNativeTarget "Stencil" */ = {
isa = XCConfigurationList;
buildConfigurations = (
77FAAE6919F91E480029DC5E /* Debug */,
77FAAE6A19F91E480029DC5E /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
77FAAE6B19F91E480029DC5E /* Build configuration list for PBXNativeTarget "StencilTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
77FAAE6C19F91E480029DC5E /* Debug */,
77FAAE6D19F91E480029DC5E /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 77FAAE4919F91E480029DC5E /* Project object */;
}

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:Stencil.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -1,50 +0,0 @@
import Foundation
/// A container for template variables.
public class Context : Equatable {
var dictionaries:[Dictionary<String, AnyObject>]
public init(dictionary:Dictionary<String, AnyObject>) {
dictionaries = [dictionary]
}
public init() {
dictionaries = []
}
public subscript(key: String) -> AnyObject? {
get {
for dictionary in reverse(dictionaries) {
if let value:AnyObject = dictionary[key] {
return value
}
}
return nil
}
set(value) {
if dictionaries.count > 0 {
var dictionary = dictionaries.removeLast()
dictionary[key] = value
dictionaries.append(dictionary)
}
}
}
public func push() {
push(Dictionary<String, String>())
}
public func push(dictionary:Dictionary<String, String>) {
dictionaries.append(dictionary)
}
public func pop() {
dictionaries.removeLast()
}
}
public func ==(lhs:Context, rhs:Context) -> Bool {
return lhs.dictionaries == rhs.dictionaries
}

View File

@@ -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>org.cocode.$(PRODUCT_NAME:rfc1034identifier)</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>

View File

@@ -1,56 +0,0 @@
import Foundation
public struct Lexer {
public let templateString:String
let regex = NSRegularExpression(pattern: "(\\{\\{.*?\\}\\}|\\{%.*?%\\}|\\{#.*?#\\})", options: nil, error: nil)!
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)
}
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, countElements(templateString))
var lastIndex = 0
let nsTemplateString = templateString as NSString
let options = NSMatchingOptions(0)
regex.enumerateMatchesInString(templateString, options: options, range: range) { (result, flags, b) in
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 < countElements(templateString) {
let substring = (templateString as NSString).substringFromIndex(lastIndex)
tokens.append(Token.Text(value: substring))
}
return tokens
}
}

View File

@@ -1,304 +0,0 @@
import Foundation
struct NodeError : Error {
let token:Token
let message:String
init(token:Token, message:String) {
self.token = token
self.message = message
}
var description:String {
return "\(token.components().first!): \(message)"
}
}
public protocol Node {
func render(context:Context) -> Result
}
extension Array {
func map<U>(block:((Element) -> (U?, Error?))) -> ([U]?, Error?) {
var results = [U]()
for item in self {
let (result, error) = block(item)
if let error = error {
return (nil, error)
} else if (result != nil) {
// let result = result exposing a bug in the Swift compier :(
results.append(result!)
}
}
return (results, nil)
}
}
public func renderNodes(nodes:[Node], context:Context) -> Result {
var result = ""
for item in nodes {
switch item.render(context) {
case .Success(let string):
result += string
case .Error(let error):
return .Error(error)
}
}
return .Success(result)
}
public class SimpleNode : Node {
let handler:(Context) -> (Result)
public init(handler:((Context) -> (Result))) {
self.handler = handler
}
public func render(context:Context) -> Result {
return handler(context)
}
}
public class TextNode : Node {
public let text:String
public init(text:String) {
self.text = text
}
public func render(context:Context) -> Result {
return .Success(self.text)
}
}
public class VariableNode : Node {
public let variable:Variable
public init(variable:Variable) {
self.variable = variable
}
public init(variable:String) {
self.variable = Variable(variable)
}
public func render(context:Context) -> Result {
let result:AnyObject? = variable.resolve(context)
if let result = result as? String {
return .Success(result)
} else if let result = result as? NSObject {
return .Success(result.description)
}
return .Success("")
}
}
public class NowNode : Node {
public let format:Variable
public class func parse(parser:TokenParser, token:Token) -> TokenParser.Result {
var format:Variable?
let components = token.components()
if components.count == 2 {
format = Variable(components[1])
}
return .Success(node:NowNode(format:format))
}
public init(format:Variable?) {
if let format = format {
self.format = format
} else {
self.format = Variable("\"yyyy-MM-dd 'at' HH:mm\"")
}
}
public func render(context: Context) -> Result {
let date = NSDate()
let format: AnyObject? = 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 .Success("")
}
return .Success(formatter!.stringFromDate(date))
}
}
public class ForNode : Node {
let variable:Variable
let loopVariable:String
let nodes:[Node]
public class func parse(parser:TokenParser, token:Token) -> TokenParser.Result {
let components = token.components()
let count = countElements(components)
if count == 4 && components[2] == "in" {
let loopVariable = components[1]
let variable = components[3]
var forNodes:[Node]!
var emptyNodes = [Node]()
switch parser.parse(until(["endfor", "empty"])) {
case .Success(let nodes):
forNodes = nodes
case .Error(let error):
return .Error(error)
}
if let token = parser.nextToken() {
if token.contents == "empty" {
switch parser.parse(until(["endfor"])) {
case .Success(let nodes):
emptyNodes = nodes
case .Error(let error):
return .Error(error)
}
parser.nextToken()
}
} else {
return .Error(error: NodeError(token: token, message: "`endfor` was not found."))
}
return .Success(node:ForNode(variable: variable, loopVariable: loopVariable, nodes: forNodes, emptyNodes:emptyNodes))
}
return .Error(error: NodeError(token: token, message: "Invalid syntax. Expected `for x in y`."))
}
public init(variable:String, loopVariable:String, nodes:[Node], emptyNodes:[Node]) {
self.variable = Variable(variable)
self.loopVariable = loopVariable
self.nodes = nodes
}
public func render(context: Context) -> Result {
let values = variable.resolve(context) as? [AnyObject]
var output = ""
if let values = values {
for item in values {
context.push()
context[loopVariable] = item
let result = renderNodes(nodes, context)
context.pop()
switch result {
case .Success(let string):
output += string
case .Error(let error):
return .Error(error)
}
}
}
return .Success(output)
}
}
public class IfNode : Node {
public let variable:Variable
public let trueNodes:[Node]
public let falseNodes:[Node]
public class func parse(parser:TokenParser, token:Token) -> TokenParser.Result {
let variable = token.components()[1]
var trueNodes = [Node]()
var falseNodes = [Node]()
switch parser.parse(until(["endif", "else"])) {
case .Success(let nodes):
trueNodes = nodes
case .Error(let error):
return .Error(error)
}
if let token = parser.nextToken() {
if token.contents == "else" {
switch parser.parse(until(["endif"])) {
case .Success(let nodes):
falseNodes = nodes
case .Error(let error):
return .Error(error)
}
parser.nextToken()
}
} else {
return .Error(error:NodeError(token: token, message: "`endif` was not found."))
}
return .Success(node:IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes))
}
public class func parse_ifnot(parser:TokenParser, token:Token) -> TokenParser.Result {
let variable = token.components()[1]
var trueNodes = [Node]()
var falseNodes = [Node]()
switch parser.parse(until(["endif", "else"])) {
case .Success(let nodes):
falseNodes = nodes
case .Error(let error):
return .Error(error)
}
if let token = parser.nextToken() {
if token.contents == "else" {
switch parser.parse(until(["endif"])) {
case .Success(let nodes):
trueNodes = nodes
case .Error(let error):
return .Error(error)
}
parser.nextToken()
}
} else {
return .Error(error:NodeError(token: token, message: "`endif` was not found."))
}
return .Success(node:IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes))
}
public init(variable:String, trueNodes:[Node], falseNodes:[Node]) {
self.variable = Variable(variable)
self.trueNodes = trueNodes
self.falseNodes = falseNodes
}
public func render(context: Context) -> Result {
let result: AnyObject? = variable.resolve(context)
var truthy = false
if let result = result as? [AnyObject] {
if result.count > 0 {
truthy = true
}
} else if let result: AnyObject = result {
truthy = true
}
context.push()
let output = renderNodes(truthy ? trueNodes : falseNodes, context)
context.pop()
return output
}
}

View File

@@ -1,104 +0,0 @@
import Foundation
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
}
}
}
return false
}
public class TokenParser {
public typealias TagParser = (TokenParser, Token) -> Result
public typealias NodeList = [Node]
public enum Result {
case Success(node: Node)
case Error(error: Stencil.Error)
}
public enum Results {
case Success(nodes: NodeList)
case Error(error: Stencil.Error)
}
private var tokens:[Token]
private var tags = Dictionary<String, TagParser>()
public init(tokens:[Token]) {
self.tokens = tokens
registerTag("for", ForNode.parse)
registerTag("if", IfNode.parse)
registerTag("ifnot", IfNode.parse_ifnot)
registerTag("now", NowNode.parse)
}
public func registerTag(name:String, parser:TagParser) {
tags[name] = parser
}
public func registerSimpleTag(name:String, handler:((Context) -> (Stencil.Result))) {
registerTag(name, parser: { (parser, token) -> TokenParser.Result in
return .Success(node:SimpleNode(handler: handler))
})
}
public func parse() -> Results {
return parse(nil)
}
public func parse(parse_until:((parser:TokenParser, token:Token) -> (Bool))?) -> TokenParser.Results {
var nodes = NodeList()
while tokens.count > 0 {
let token = nextToken()!
switch token {
case .Text(let text):
nodes.append(TextNode(text: text))
case .Variable(let variable):
nodes.append(VariableNode(variable: variable))
case .Block(let value):
let tag = token.components().first
if let parse_until = parse_until {
if parse_until(parser: self, token: token) {
prependToken(token)
return .Success(nodes:nodes)
}
}
if let tag = tag {
if let parser = self.tags[tag] {
switch parser(self, token) {
case .Success(let node):
nodes.append(node)
case .Error(let error):
return .Error(error:error)
}
}
}
case .Comment(let value):
continue
}
}
return .Success(nodes:nodes)
}
public func nextToken() -> Token? {
if tokens.count > 0 {
return tokens.removeAtIndex(0)
}
return nil
}
public func prependToken(token:Token) {
tokens.insert(token, atIndex: 0)
}
}

View File

@@ -1,25 +0,0 @@
import Foundation
public protocol Error : Printable {
}
public func ==(lhs:Error, rhs:Error) -> Bool {
return lhs.description == rhs.description
}
public enum Result : Equatable {
case Success(String)
case Error(Stencil.Error)
}
public func ==(lhs:Result, rhs:Result) -> Bool {
switch (lhs, rhs) {
case (.Success(let lhsValue), .Success(let rhsValue)):
return lhsValue == rhsValue
case (.Error(let lhsValue), .Error(let rhsValue)):
return lhsValue == rhsValue
default:
return false
}
}

View File

@@ -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 <Cocoa/Cocoa.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>

View File

@@ -1,53 +0,0 @@
import Foundation
public class Template {
public let parser:TokenParser
public convenience init?(named:String) {
self.init(named:named, inBundle:nil)
}
public convenience init?(named:String, inBundle bundle:NSBundle?) {
var url:NSURL?
if let bundle = bundle {
url = bundle.URLForResource(named, withExtension: nil)
} else {
url = NSBundle.mainBundle().URLForResource(named, withExtension: nil)
}
self.init(URL:url!)
}
public convenience init?(URL:NSURL) {
var error:NSError?
let maybeTemplateString = NSString(contentsOfURL: URL, encoding: NSUTF8StringEncoding, error: &error)
if let templateString = maybeTemplateString {
self.init(templateString:templateString)
} else {
self.init(templateString:"")
return nil
}
}
public init(templateString:String) {
let lexer = Lexer(templateString: templateString)
let tokens = lexer.tokenize()
parser = TokenParser(tokens: tokens)
}
public func render(context:Context) -> Result {
switch parser.parse() {
case .Success(let nodes):
return renderNodes(nodes, context)
case .Error(let error):
return .Error(error)
}
}
public func render() -> Result {
let context = Context()
return render(context)
}
}

View File

@@ -1,57 +0,0 @@
import Foundation
public enum Token : Equatable {
case Text(value:String)
case Variable(value:String)
case Comment(value:String)
case Block(value:String)
/// Returns the underlying value as an array seperated by spaces
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)
case .Variable(let value):
return strip(value)
case .Text(let value):
return strip(value)
case .Comment(let value):
return strip(value)
}
}
var contents:String {
switch self {
case .Block(let value):
return value
case .Variable(let value):
return value
case .Text(let value):
return value
case .Comment(let value):
return value
}
}
}
public func ==(lhs:Token, rhs:Token) -> Bool {
switch (lhs, rhs) {
case (.Text(let lhsValue), .Text(let rhsValue)):
return lhsValue == rhsValue
case (.Variable(let lhsValue), .Variable(let rhsValue)):
return lhsValue == rhsValue
case (.Block(let lhsValue), .Block(let rhsValue)):
return lhsValue == rhsValue
case (.Comment(let lhsValue), .Comment(let rhsValue)):
return lhsValue == rhsValue
default:
return false
}
}

View File

@@ -1,49 +0,0 @@
import Foundation
public struct Variable : Equatable {
public let variable:String
public init(_ variable:String) {
self.variable = variable
}
private func lookup() -> [String] {
return variable.componentsSeparatedByString(".")
}
public func resolve(context:Context) -> AnyObject? {
var current:AnyObject? = context
if (variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\"")) {
return variable.substringWithRange(variable.startIndex.successor() ..< variable.endIndex.predecessor())
}
for bit in lookup() {
if let context = current as? Context {
current = context[bit]
} else if let dictionary = current as? Dictionary<String, AnyObject> {
current = dictionary[bit]
} else if let array = current as? [AnyObject] {
if let index = bit.toInt() {
current = array[index]
} else if bit == "first" {
current = array.first
} else if bit == "last" {
current = array.last
} else if bit == "count" {
current = countElements(array)
}
} else if let object = current as? NSObject {
current = object.valueForKey(bit)
} else {
return nil
}
}
return current
}
}
public func ==(lhs:Variable, rhs:Variable) -> Bool {
return lhs.variable == rhs.variable
}

View File

@@ -1,65 +0,0 @@
import Cocoa
import XCTest
import Stencil
class ContextTests: XCTestCase {
var context:Context!
override func setUp() {
context = Context(dictionary: ["name": "Kyle"])
}
func testItAllowsYouToRetrieveAValue() {
let name = context["name"] as String!
XCTAssertEqual(name, "Kyle")
}
func testItAllowsYouToSetValue() {
context["name"] = "Katie"
let name = context["name"] as String!
XCTAssertEqual(name, "Katie")
}
func testItAllowsYouToRemoveAValue() {
context["name"] = nil
XCTAssertNil(context["name"])
}
func testItAllowsYouToRetrieveAValueFromParent() {
context.push()
let name = context["name"] as String!
XCTAssertEqual(name, "Kyle")
}
func testItAllowsYouToOverideAParentVariable() {
context.push()
context["name"] = "Katie"
let name = context["name"] as String!
XCTAssertEqual(name, "Katie")
}
func testShowAllowYouToPopVariablesRestoringPreviousState() {
context.push()
context["name"] = "Katie"
context.pop()
let name = context["name"] as String!
XCTAssertEqual(name, "Kyle")
}
func testItAllowsYouToPushADictionaryToTheStack() {
context.push(["name": "Katie"])
let name = context["name"] as String!
XCTAssertEqual(name, "Katie")
}
func testItAllowsYouToCompareTwoContextsForEquality() {
let otherContext = Context(dictionary: ["name": "Kyle"])
XCTAssertEqual(otherContext, context )
}
}

View File

@@ -1,24 +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>org.cocode.$(PRODUCT_NAME:rfc1034identifier)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View File

@@ -1,50 +0,0 @@
import Cocoa
import XCTest
import Stencil
class LexerTests: XCTestCase {
func testTokenizeText() {
let lexer = Lexer(templateString:"Hello World")
let tokens = lexer.tokenize()
XCTAssertEqual(tokens.count, 1)
XCTAssertEqual(tokens.first!, Token.Text(value: "Hello World"))
}
func testTokenizeComment() {
let lexer = Lexer(templateString:"{# Comment #}")
let tokens = lexer.tokenize()
XCTAssertEqual(tokens.count, 1)
XCTAssertEqual(tokens.first!, Token.Comment(value: "Comment"))
}
func testTokenizeVariable() {
let lexer = Lexer(templateString:"{{ Variable }}")
let tokens = lexer.tokenize()
XCTAssertEqual(tokens.count, 1)
XCTAssertEqual(tokens.first!, Token.Variable(value: "Variable"))
}
func testTokenizeMixture() {
let lexer = Lexer(templateString:"My name is {{ name }}.")
let tokens = lexer.tokenize()
XCTAssertEqual(tokens.count, 3)
XCTAssertEqual(tokens[0], Token.Text(value: "My name is "))
XCTAssertEqual(tokens[1], Token.Variable(value: "name"))
XCTAssertEqual(tokens[2], Token.Text(value: "."))
}
func testTokenizeTwoVariables() { // Don't be greedy
let lexer = Lexer(templateString:"{{ thing }}{{ name }}")
let tokens = lexer.tokenize()
XCTAssertEqual(tokens.count, 2)
XCTAssertEqual(tokens[0], Token.Variable(value: "thing"))
XCTAssertEqual(tokens[1], Token.Variable(value: "name"))
}
}

View File

@@ -1,250 +0,0 @@
import Cocoa
import XCTest
import Stencil
class ErrorNodeError : Error {
var description: String {
return "Node Error"
}
}
class ErrorNode : Node {
func render(context: Context) -> Result {
return .Error(ErrorNodeError())
}
}
class NodeTests: XCTestCase {
var context:Context!
override func setUp() {
context = Context(dictionary: [
"name": "Kyle",
"age": 27,
"items": [1,2,3],
])
}
}
class TextNodeTests: NodeTests {
func testTextNodeResolvesText() {
let node = TextNode(text:"Hello World")
let result = node.render(context)
switch node.render(context) {
case .Success(let string):
XCTAssertEqual(string, "Hello World")
case .Error(let error):
XCTAssert(false, "Unexpected error")
}
}
}
class VariableNodeTests: NodeTests {
func testVariableNodeResolvesVariable() {
let node = VariableNode(variable:Variable("name"))
let result = node.render(context)
switch node.render(context) {
case .Success(let string):
XCTAssertEqual(string, "Kyle")
case .Error(let error):
XCTAssert(false, "Unexpected error")
}
}
func testVariableNodeResolvesNonStringVariable() {
let node = VariableNode(variable:Variable("age"))
let result = node.render(context)
switch node.render(context) {
case .Success(let string):
XCTAssertEqual(string, "27")
case .Error(let error):
XCTAssert(false, "Unexpected error")
}
}
}
class RenderNodeTests: NodeTests {
func testRenderingNodes() {
let nodes = [TextNode(text:"Hello "), VariableNode(variable: "name")] as [Node]
switch renderNodes(nodes, context) {
case .Success(let result):
XCTAssertEqual(result, "Hello Kyle")
case .Error(let error):
XCTAssert(false, "Unexpected error")
}
}
func testRenderingNodesWithFailure() {
let nodes = [TextNode(text:"Hello "), VariableNode(variable: "name"), ErrorNode()] as [Node]
switch renderNodes(nodes, context) {
case .Success(let result):
XCTAssert(false, "Unexpected success")
case .Error(let error):
XCTAssertEqual("\(error)", "Node Error")
}
}
}
class ForNodeTests: NodeTests {
func testForNodeRender() {
let node = ForNode(variable: "items", loopVariable: "item", nodes: [VariableNode(variable: "item")], emptyNodes:[])
let result = node.render(context)
switch node.render(context) {
case .Success(let string):
XCTAssertEqual(string, "123")
case .Error(let error):
XCTAssert(false, "Unexpected error")
}
}
}
class IfNodeTests: NodeTests {
// MARK: Parsing
func testParseIf() {
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)
assertSuccess(parser.parse()) { nodes in
let node = nodes.first! as IfNode
let trueNode = node.trueNodes.first! as TextNode
let falseNode = node.falseNodes.first! as TextNode
XCTAssertEqual(nodes.count, 1)
XCTAssertEqual(node.variable.variable, "value")
XCTAssertEqual(node.trueNodes.count, 1)
XCTAssertEqual(trueNode.text, "true")
XCTAssertEqual(node.falseNodes.count, 1)
XCTAssertEqual(falseNode.text, "false")
}
}
func testParseIfNot() {
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)
assertSuccess(parser.parse()) { nodes in
let node = nodes.first! as IfNode
let trueNode = node.trueNodes.first! as TextNode
let falseNode = node.falseNodes.first! as TextNode
XCTAssertEqual(nodes.count, 1)
XCTAssertEqual(node.variable.variable, "value")
XCTAssertEqual(node.trueNodes.count, 1)
XCTAssertEqual(trueNode.text, "true")
XCTAssertEqual(node.falseNodes.count, 1)
XCTAssertEqual(falseNode.text, "false")
}
}
func testParseIfWithoutEndIfError() {
let tokens = [
Token.Block(value: "if value"),
]
let parser = TokenParser(tokens: tokens)
assertFailure(parser.parse(), "if: `endif` was not found.")
}
func testParseIfNotWithoutEndIfError() {
let tokens = [
Token.Block(value: "ifnot value"),
]
let parser = TokenParser(tokens: tokens)
assertFailure(parser.parse(), "ifnot: `endif` was not found.")
}
// MARK: Rendering
func testIfNodeRenderTruth() {
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
let result = node.render(context)
switch node.render(context) {
case .Success(let string):
XCTAssertEqual(string, "true")
case .Error(let error):
XCTAssert(false, "Unexpected error")
}
}
func testIfNodeRenderFalse() {
let node = IfNode(variable: "unknown", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
let result = node.render(context)
switch node.render(context) {
case .Success(let string):
XCTAssertEqual(string, "false")
case .Error(let error):
XCTAssert(false, "Unexpected error")
}
}
}
class NowNodeTests: NodeTests {
// MARK: Parsing
func testParseDefaultNow() {
let tokens = [ Token.Block(value: "now") ]
let parser = TokenParser(tokens: tokens)
assertSuccess(parser.parse()) { nodes in
let node = nodes.first! as NowNode
XCTAssertEqual(nodes.count, 1)
XCTAssertEqual(node.format.variable, "\"yyyy-MM-dd 'at' HH:mm\"")
}
}
func testParseNowWithFormat() {
let tokens = [ Token.Block(value: "now \"HH:mm\"") ]
let parser = TokenParser(tokens: tokens)
assertSuccess(parser.parse()) { nodes in
let node = nodes.first! as NowNode
XCTAssertEqual(nodes.count, 1)
XCTAssertEqual(node.format.variable, "\"HH:mm\"")
}
}
// MARK: Rendering
func testRenderNowNode() {
let node = NowNode(format: Variable("\"yyyy-MM-dd\""))
let result = node.render(context)
let formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let date = formatter.stringFromDate(NSDate())
switch node.render(context) {
case .Success(let string):
XCTAssertEqual(string, date)
case .Error(let error):
XCTAssert(false, "Unexpected error")
}
}
}

View File

@@ -1,49 +0,0 @@
import Cocoa
import XCTest
import Stencil
class TokenParserTests: XCTestCase {
func testParsingTextToken() {
let parser = TokenParser(tokens: [
Token.Text(value: "Hello World")
])
assertSuccess(parser.parse()) { nodes in
let node = nodes.first as TextNode!
XCTAssertEqual(nodes.count, 1)
XCTAssertEqual(node.text, "Hello World")
}
}
func testParsingVariableToken() {
let parser = TokenParser(tokens: [
Token.Variable(value: "name")
])
assertSuccess(parser.parse()) { nodes in
let node = nodes.first as VariableNode!
XCTAssertEqual(nodes.count, 1)
XCTAssertEqual(node.variable, Variable("name"))
}
}
func testParsingCommentToken() {
let parser = TokenParser(tokens: [
Token.Comment(value: "Secret stuff!")
])
assertSuccess(parser.parse()) { nodes in
XCTAssertEqual(nodes.count, 0)
}
}
func testParsingTagToken() {
let parser = TokenParser(tokens: [
Token.Block(value: "now"),
])
assertSuccess(parser.parse()) { nodes in
XCTAssertEqual(nodes.count, 1)
}
}
}

View File

@@ -1,79 +0,0 @@
import Cocoa
import XCTest
import Stencil
func assertSuccess(result:TokenParser.Results, block:(([Node]) -> ())) {
switch result {
case .Success(let nodes):
block(nodes)
case .Error(let error):
XCTAssert(false, "Unexpected error")
}
}
func assertFailure(result:TokenParser.Results, description:String) {
switch result {
case .Success(let nodes):
XCTAssert(false, "Unexpected error")
case .Error(let error):
XCTAssertEqual("\(error)", description)
}
}
class CustomNode : Node {
func render(context:Context) -> Result {
return .Success("Hello World")
}
}
class StencilTests: XCTestCase {
func testReadmeExample() {
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 = 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"
XCTAssertEqual(result, Result.Success(fixture))
}
func testCustomTag() {
let templateString = "{% custom %}"
let template = Template(templateString:templateString)
template.parser.registerTag("custom") { parser, token in
return .Success(node:CustomNode())
}
let result = template.render()
XCTAssertEqual(result, Result.Success("Hello World"))
}
func testSimpleCustomTag() {
let templateString = "{% custom %}"
let template = Template(templateString:templateString)
template.parser.registerSimpleTag("custom") { context in
return .Success("Hello World")
}
let result = template.render()
XCTAssertEqual(result, Result.Success("Hello World"))
}
}

View File

@@ -1,14 +0,0 @@
import Cocoa
import XCTest
import Stencil
class TemplateTests: XCTestCase {
func testTemplate() {
let context = Context(dictionary: [ "name": "Kyle" ])
let template = Template(templateString: "Hello World")
let result = template.render(context)
XCTAssertEqual(result, Result.Success("Hello World"))
}
}

View File

@@ -1,62 +0,0 @@
import Cocoa
import XCTest
import Stencil
@objc class Object : NSObject {
let title = "Hello World"
}
class VariableTests: XCTestCase {
var context:Context!
override func setUp() {
context = Context(dictionary: [
"name": "Kyle",
"contacts": [ "Katie", "Orta", ],
"profiles": [ "github": "kylef", ],
"object": Object(),
])
}
func testResolvingStringLiteral() {
let variable = Variable("\"name\"")
let result = variable.resolve(context) as String!
XCTAssertEqual(result, "name")
}
func testResolvingVariable() {
let variable = Variable("name")
let result = variable.resolve(context) as String!
XCTAssertEqual(result, "Kyle")
}
func testResolvingItemFromDictionary() {
let variable = Variable("profiles.github")
let result = variable.resolve(context) as String!
XCTAssertEqual(result, "kylef")
}
func testResolvingItemFromArrayWithIndex() {
let variable = Variable("contacts.0")
let result = variable.resolve(context) as String!
XCTAssertEqual(result, "Katie")
}
func testResolvingFirstItemFromArray() {
let variable = Variable("contacts.first")
let result = variable.resolve(context) as String!
XCTAssertEqual(result, "Katie")
}
func testResolvingLastItemFromArray() {
let variable = Variable("contacts.last")
let result = variable.resolve(context) as String!
XCTAssertEqual(result, "Orta")
}
func testResolvingValueViaKVO() {
let variable = Variable("object.title")
let result = variable.resolve(context) as String!
XCTAssertEqual(result, "Hello World")
}
}

62
Tests/ContextSpec.swift Normal file
View 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
View 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
View 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")
}
}
}

View 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"
}
}
}

View 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"
}
}
}
}

View 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"))
}
}
}
}

View 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
View 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
View 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"
}
}
}

View 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!"
}
}
}
}

View 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"
}
}
}

View 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
View 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
View 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
View 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
}
}

2
Tests/fixtures/base.html vendored Normal file
View File

@@ -0,0 +1,2 @@
{% block header %}Header{% endblock %}
{% block body %}Body{% endblock %}

2
Tests/fixtures/child.html vendored Normal file
View File

@@ -0,0 +1,2 @@
{% extends "base.html" %}
{% block body %}Child{% endblock %}

1
Tests/fixtures/test.html vendored Normal file
View File

@@ -0,0 +1 @@
Hello {{ target }}!

15
Tests/main.swift Normal file
View File

@@ -0,0 +1,15 @@
testContext()
testFilter()
testLexer()
testToken()
testTokenParser()
testTemplateLoader()
testTemplate()
testVariable()
testNode()
testForNode()
testIfNode()
testNowNode()
testInclude()
testInheritence()
testStencil()