75 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
70 changed files with 1975 additions and 2257 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

View File

@@ -96,7 +96,7 @@ When the `ForNode` is rendered in a context, it will look up the variable `artic
There are two ways to register custom template tags. A simple way which allows you to map 1:1 a block token to a Node. You can also register a more advanced template tag which has 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 onto the `TokenParser` which you can access from your `Template`.
The tags are registered with a `Namespace` passed when rendering your `Template`.
#### Simple Tags
@@ -105,7 +105,7 @@ A simple tag is registered with a string for the tag name and a block of code wh
Heres an example. Registering a template tag called `custom` which just renders `Hello World` in the rendered template:
```swift
parser.registerSimpleTag("custom") { context in
namespace.registerSimpleTag("custom") { context in
return "Hello World"
}
```
@@ -120,7 +120,7 @@ You would use it as such in a template:
If you need more control or functionality than the simple 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 `TokenParser` which accepts a name for the tag and a block of code to handle parsing. The block of code is invoked with the parser and the current token as an argument. This allows you to use the API on `TokenParser` to parse nodes further in the token array.
You would register a template tag using the `registerTag` API inside a `Namespace` which accepts a name for the tag and a block of code to handle parsing. The block of code is invoked with the parser and the current token as an argument. This allows you to use the API on `TokenParser` to parse 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.
@@ -163,7 +163,7 @@ class DebugNode : Node {
We will need to write a parser to parse up until the `enddebug` template block and create a `DebugNode` with the nodes in-between. If there was another error form another Node inside, then we will return that error.
```swift
parser.registerTag("debug") { parser, token in
namespace.registerTag("debug") { parser, token in
// Use the parser to parse every token up until the `enddebug` block.
let nodes = try until(["enddebug"]))
return DebugNode(nodes)

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

10
Podfile
View File

@@ -1,10 +0,0 @@
use_frameworks!
target 'Stencil' do
podspec
end
target 'StencilTests' do
podspec
end

View File

@@ -1,10 +0,0 @@
PODS:
- PathKit (0.5.0)
DEPENDENCIES:
- PathKit (~> 0.5.0)
SPEC CHECKSUMS:
PathKit: 64e70cb924d6faed9251049f5f28662855524baf
COCOAPODS: 0.38.2

View File

@@ -1,13 +1,12 @@
Stencil
=======
# Stencil
[![Build Status](http://img.shields.io/circleci/project/kylef/Stencil/master.svg)](https://circleci.com/gh/kylef/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.
@@ -26,8 +25,8 @@ let context = Context(dictionary: [
])
do {
let template = Template(named: "template.stencil")
let rendered = template.render(context)
let template = try Template(named: "template.stencil")
let rendered = try template.render(context)
print(rendered)
} catch {
print("Failed to render template \(error)")
@@ -74,8 +73,43 @@ 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
@@ -108,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 %}
```
@@ -148,13 +182,37 @@ let context = Context(dictionary: [
])
```
### Customisation
You can build your own custom filters and tags and pass them down while
rendering your template. Any custom filters or tags must be registered
with a namespace which contains all filters and tags available to the template.
```swift
let namespace = Namespace()
// Register your filters and tags with the namespace
let rendered = try template.render(context, namespace: namespace)
```
#### Registering custom filters
```swift
namespace.registerFilter("double") { value in
if let value = value as? Int {
return value * 2
}
return value
}
```
#### Building custom tags
You can build a custom template tag. There are a couple of APIs to allow
you to write your own custom tags. The following is the simplest form:
```swift
template.parser.registerSimpleTag("custom") { context in
namespace.registerSimpleTag("custom") { context in
return "Hello World"
}
```
@@ -168,7 +226,8 @@ of template tags. You will need to call the `registerTag` API which accepts a
closure to handle the parsing. You can find examples of the `now`, `if` and
`for` tags found inside `Node.swift`.
The architecture of Stencil along with how to build advanced plugins can be found in the [architecture](ARCHITECTURE.md) document.
The architecture of Stencil along with how to build advanced plugins can be
found in the [architecture](ARCHITECTURE.md) document.
### Comments
@@ -182,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)
}
}

View File

@@ -1,23 +1,19 @@
import Foundation
import PathKit
#if os(Linux)
let NSFileNoSuchFileError = 4
#endif
/// A class representing a template
public class Template {
public let parser:TokenParser
/// Create a template with the given name inside the main bundle
public convenience init(named:String) throws {
try self.init(named:named, inBundle:nil)
}
let tokens: [Token]
/// Create a template with the given name inside the given bundle
public convenience init(named:String, inBundle bundle:NSBundle?) throws {
let url:NSURL
if let bundle = bundle {
url = bundle.URLForResource(named, withExtension: nil)!
} else {
url = NSBundle.mainBundle().URLForResource(named, withExtension: nil)!
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)
@@ -25,7 +21,7 @@ public class Template {
/// Create a template with a file found at the given URL
public convenience init(URL:NSURL) throws {
try self.init(path: Path(URL.absoluteString))
try self.init(path: Path(URL.path!))
}
/// Create a template with a file found at the given path
@@ -36,13 +32,14 @@ public class Template {
/// Create a template with a template string
public init(templateString:String) {
let lexer = Lexer(templateString: templateString)
let tokens = lexer.tokenize()
parser = TokenParser(tokens: tokens)
tokens = lexer.tokenize()
}
/// Render the given template
public func render(context:Context? = nil) throws -> String {
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 ?? Context())
return try renderNodes(nodes, context)
}
}

View File

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

View File

@@ -1,38 +1,68 @@
public enum Token : Equatable {
/// A token representing a piece of text.
case Text(value:String)
import Foundation
/// A token representing a variable.
case Variable(value:String)
/// A token representing a comment.
case Comment(value:String)
/// Split a string by spaces leaving quoted phrases together
func smartSplit(value: String) -> [String] {
var word = ""
var separator: Character = " "
var components: [String] = []
/// A token representing a template block.
case Block(value:String)
for character in value.characters {
if character == separator {
if separator != " " {
word.append(separator)
}
/// 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()
if !word.isEmpty {
components.append(word)
word = ""
}
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)
separator = " "
} else {
if separator == " " && (character == "'" || character == "\"") {
separator = character
}
word.append(character)
}
}
var contents:String {
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
@@ -46,7 +76,8 @@ public enum Token : Equatable {
}
}
public func ==(lhs:Token, rhs:Token) -> Bool {
public func == (lhs: Token, rhs: Token) -> Bool {
switch (lhs, rhs) {
case (.Text(let lhsValue), .Text(let rhsValue)):
return lhsValue == rhsValue

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,16 +0,0 @@
Pod::Spec.new do |spec|
spec.name = 'Stencil'
spec.version = '0.3.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/*.swift', 'Stencil/TemplateLoader/*.swift'
spec.ios.deployment_target = '8.0'
spec.osx.deployment_target = '10.9'
spec.requires_arc = true
spec.dependency 'PathKit', '~> 0.5.0'
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,648 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
27A848E41B42240E004ACA13 /* Inheritence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A848E31B42240E004ACA13 /* Inheritence.swift */; };
27A848E91B42242C004ACA13 /* base.html in Resources */ = {isa = PBXBuildFile; fileRef = 27A848E71B42242C004ACA13 /* base.html */; };
27A848EA1B42242C004ACA13 /* child.html in Resources */ = {isa = PBXBuildFile; fileRef = 27A848E81B42242C004ACA13 /* child.html */; };
27A848EC1B42247D004ACA13 /* InheritenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A848EB1B42247D004ACA13 /* InheritenceTests.swift */; };
27CE0ADE1A50BEC3004A105B /* TemplateLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0ADD1A50BEC3004A105B /* TemplateLoader.swift */; };
27CE0AE01A50BF05004A105B /* TemplateLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0ADF1A50BF05004A105B /* TemplateLoaderTests.swift */; };
27CE0AFA1A50C963004A105B /* test.html in Resources */ = {isa = PBXBuildFile; fileRef = 27CE0AF91A50C963004A105B /* test.html */; };
27CE0B011A50CBD1004A105B /* Include.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0B001A50CBD1004A105B /* Include.swift */; };
27CE0B041A50CBEA004A105B /* IncludeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0B031A50CBEA004A105B /* IncludeTests.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 */; };
B47F77A0E91D074331ECA4BA /* Pods_StencilTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 993D1013A77A01814D8E4A04 /* Pods_StencilTests.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
EE07225918EA237F21D8B902 /* Pods_Stencil.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 19D2CE595625A4F7A865B0BD /* Pods_Stencil.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
27CE0AF51A50C292004A105B /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 77FAAE4919F91E480029DC5E /* Project object */;
proxyType = 1;
remoteGlobalIDString = 77FAAE5119F91E480029DC5E;
remoteInfo = Stencil;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
19D2CE595625A4F7A865B0BD /* Pods_Stencil.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Stencil.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1F9D5A26B78DADAECC18CA94 /* Pods-Stencil.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Stencil.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Stencil/Pods-Stencil.debug.xcconfig"; sourceTree = "<group>"; };
27A848E31B42240E004ACA13 /* Inheritence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Inheritence.swift; sourceTree = "<group>"; };
27A848E71B42242C004ACA13 /* base.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = base.html; sourceTree = "<group>"; };
27A848E81B42242C004ACA13 /* child.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = child.html; sourceTree = "<group>"; };
27A848EB1B42247D004ACA13 /* InheritenceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InheritenceTests.swift; sourceTree = "<group>"; };
27CE0ADD1A50BEC3004A105B /* TemplateLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TemplateLoader.swift; sourceTree = "<group>"; };
27CE0ADF1A50BF05004A105B /* TemplateLoaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TemplateLoaderTests.swift; sourceTree = "<group>"; };
27CE0AF91A50C963004A105B /* test.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = test.html; sourceTree = "<group>"; };
27CE0B001A50CBD1004A105B /* Include.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Include.swift; sourceTree = "<group>"; };
27CE0B031A50CBEA004A105B /* IncludeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IncludeTests.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>"; };
8E90EE157CBB4F8E96884DED /* Pods-Stencil.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Stencil.release.xcconfig"; path = "Pods/Target Support Files/Pods-Stencil/Pods-Stencil.release.xcconfig"; sourceTree = "<group>"; };
993D1013A77A01814D8E4A04 /* Pods_StencilTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_StencilTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D504530B9669DDF40C6890DF /* Pods-StencilTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StencilTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-StencilTests/Pods-StencilTests.release.xcconfig"; sourceTree = "<group>"; };
D7B326C5BB2955F522787D9B /* Pods-StencilTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StencilTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-StencilTests/Pods-StencilTests.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
77FAAE4E19F91E480029DC5E /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
EE07225918EA237F21D8B902 /* Pods_Stencil.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
77FAAE5A19F91E480029DC5E /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
77FAAE5E19F91E480029DC5E /* Stencil.framework in Frameworks */,
B47F77A0E91D074331ECA4BA /* Pods_StencilTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
27CE0AFF1A50CBBF004A105B /* TemplateLoader */ = {
isa = PBXGroup;
children = (
27CE0B001A50CBD1004A105B /* Include.swift */,
27A848E31B42240E004ACA13 /* Inheritence.swift */,
);
path = TemplateLoader;
sourceTree = "<group>";
};
27CE0B021A50CBEA004A105B /* TemplateLoader */ = {
isa = PBXGroup;
children = (
27CE0B031A50CBEA004A105B /* IncludeTests.swift */,
27A848EB1B42247D004ACA13 /* InheritenceTests.swift */,
);
path = TemplateLoader;
sourceTree = "<group>";
};
77FAAE4819F91E480029DC5E = {
isa = PBXGroup;
children = (
77FAAE5419F91E480029DC5E /* Stencil */,
77FAAE6119F91E480029DC5E /* StencilTests */,
77FAAE5319F91E480029DC5E /* Products */,
C06534353970612CB8F9960D /* Pods */,
A82CB85756DE155E072D4047 /* Frameworks */,
);
indentWidth = 2;
sourceTree = "<group>";
tabWidth = 2;
};
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 */,
77EB082419F96E88001870F1 /* Template.swift */,
27CE0ADD1A50BEC3004A105B /* TemplateLoader.swift */,
7725B3CE19F94214002CF74B /* Tokenizer.swift */,
7725B3CC19F92B61002CF74B /* Variable.swift */,
27CE0AFF1A50CBBF004A105B /* TemplateLoader */,
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 */,
27CE0ADF1A50BF05004A105B /* TemplateLoaderTests.swift */,
27CE0B021A50CBEA004A105B /* TemplateLoader */,
27CE0AF91A50C963004A105B /* test.html */,
27A848E71B42242C004ACA13 /* base.html */,
27A848E81B42242C004ACA13 /* child.html */,
77FAAE6219F91E480029DC5E /* Supporting Files */,
);
path = StencilTests;
sourceTree = "<group>";
};
77FAAE6219F91E480029DC5E /* Supporting Files */ = {
isa = PBXGroup;
children = (
77FAAE6319F91E480029DC5E /* Info.plist */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
A82CB85756DE155E072D4047 /* Frameworks */ = {
isa = PBXGroup;
children = (
19D2CE595625A4F7A865B0BD /* Pods_Stencil.framework */,
993D1013A77A01814D8E4A04 /* Pods_StencilTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
C06534353970612CB8F9960D /* Pods */ = {
isa = PBXGroup;
children = (
1F9D5A26B78DADAECC18CA94 /* Pods-Stencil.debug.xcconfig */,
8E90EE157CBB4F8E96884DED /* Pods-Stencil.release.xcconfig */,
D7B326C5BB2955F522787D9B /* Pods-StencilTests.debug.xcconfig */,
D504530B9669DDF40C6890DF /* Pods-StencilTests.release.xcconfig */,
);
name = Pods;
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 = (
AFE0827734C5388F8E996172 /* Check Pods Manifest.lock */,
77FAAE4D19F91E480029DC5E /* Sources */,
77FAAE4E19F91E480029DC5E /* Frameworks */,
77FAAE4F19F91E480029DC5E /* Headers */,
77FAAE5019F91E480029DC5E /* Resources */,
5374160581EF83B2213B72F1 /* Copy Pods 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 = (
C50FF01A96AD1CEACBF2E02A /* Check Pods Manifest.lock */,
77FAAE5919F91E480029DC5E /* Sources */,
77FAAE5A19F91E480029DC5E /* Frameworks */,
77FAAE5B19F91E480029DC5E /* Resources */,
138C1DDF1FB55A83668EE384 /* Embed Pods Frameworks */,
36A8E92EBDE6D917075028D4 /* Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
27CE0AF61A50C292004A105B /* 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 = {
LastSwiftUpdateCheck = 0700;
LastUpgradeCheck = 0700;
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 = (
27CE0AFA1A50C963004A105B /* test.html in Resources */,
27A848EA1B42242C004ACA13 /* child.html in Resources */,
27A848E91B42242C004ACA13 /* base.html in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
138C1DDF1FB55A83668EE384 /* Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Embed Pods Frameworks";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-StencilTests/Pods-StencilTests-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
36A8E92EBDE6D917075028D4 /* Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Copy Pods Resources";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-StencilTests/Pods-StencilTests-resources.sh\"\n";
showEnvVarsInLog = 0;
};
5374160581EF83B2213B72F1 /* Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Copy Pods Resources";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Stencil/Pods-Stencil-resources.sh\"\n";
showEnvVarsInLog = 0;
};
AFE0827734C5388F8E996172 /* Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Check Pods Manifest.lock";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n";
showEnvVarsInLog = 0;
};
C50FF01A96AD1CEACBF2E02A /* Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Check Pods Manifest.lock";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
77FAAE4D19F91E480029DC5E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
27CE0B011A50CBD1004A105B /* Include.swift in Sources */,
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 */,
7725B3D519F9438F002CF74B /* Node.swift in Sources */,
27CE0ADE1A50BEC3004A105B /* TemplateLoader.swift in Sources */,
27A848E41B42240E004ACA13 /* Inheritence.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
77FAAE5919F91E480029DC5E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
77FAAE6519F91E480029DC5E /* StencilTests.swift in Sources */,
7725B3D319F9437F002CF74B /* NodeTests.swift in Sources */,
27A848EC1B42247D004ACA13 /* InheritenceTests.swift in Sources */,
27CE0AE01A50BF05004A105B /* TemplateLoaderTests.swift in Sources */,
7725B3D919F94A61002CF74B /* ParserTests.swift in Sources */,
77EB082719F96E9C001870F1 /* TemplateTests.swift in Sources */,
7725B3CB19F92B4F002CF74B /* VariableTests.swift in Sources */,
27CE0B041A50CBEA004A105B /* IncludeTests.swift in Sources */,
77EB082919FA85F2001870F1 /* LexerTests.swift in Sources */,
77FAAE7119F9208C0029DC5E /* ContextTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
27CE0AF61A50C292004A105B /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 77FAAE5119F91E480029DC5E /* Stencil */;
targetProxy = 27CE0AF51A50C292004A105B /* 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;
ENABLE_TESTABILITY = 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.9;
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.9;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Release;
};
77FAAE6919F91E480029DC5E /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 1F9D5A26B78DADAECC18CA94 /* Pods-Stencil.debug.xcconfig */;
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_BUNDLE_IDENTIFIER = "org.cocode.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
77FAAE6A19F91E480029DC5E /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 8E90EE157CBB4F8E96884DED /* Pods-Stencil.release.xcconfig */;
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_BUNDLE_IDENTIFIER = "org.cocode.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Release;
};
77FAAE6C19F91E480029DC5E /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D7B326C5BB2955F522787D9B /* Pods-StencilTests.debug.xcconfig */;
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_BUNDLE_IDENTIFIER = "org.cocode.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
};
77FAAE6D19F91E480029DC5E /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D504530B9669DDF40C6890DF /* Pods-StencilTests.release.xcconfig */;
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_BUNDLE_IDENTIFIER = "org.cocode.$(PRODUCT_NAME:rfc1034identifier)";
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,113 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0700"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "77FAAE5119F91E480029DC5E"
BuildableName = "Stencil.framework"
BlueprintName = "Stencil"
ReferencedContainer = "container:Stencil.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "77FAAE5C19F91E480029DC5E"
BuildableName = "StencilTests.xctest"
BlueprintName = "StencilTests"
ReferencedContainer = "container:Stencil.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "77FAAE5C19F91E480029DC5E"
BuildableName = "StencilTests.xctest"
BlueprintName = "StencilTests"
ReferencedContainer = "container:Stencil.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "77FAAE5119F91E480029DC5E"
BuildableName = "Stencil.framework"
BlueprintName = "Stencil"
ReferencedContainer = "container:Stencil.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "77FAAE5119F91E480029DC5E"
BuildableName = "Stencil.framework"
BlueprintName = "Stencil"
ReferencedContainer = "container:Stencil.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "77FAAE5119F91E480029DC5E"
BuildableName = "Stencil.framework"
BlueprintName = "Stencil"
ReferencedContainer = "container:Stencil.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

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

View File

@@ -1,49 +0,0 @@
/// A container for template variables.
public class Context : Equatable {
var dictionaries:[[String:AnyObject]]
/// Initialise a Context with a dictionary
public init(dictionary:[String:AnyObject]) {
dictionaries = [dictionary]
}
/// Initialise an empty Context
public init() {
dictionaries = []
}
public subscript(key: String) -> AnyObject? {
/// 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 var dictionary = dictionaries.popLast() {
dictionary[key] = value
dictionaries.append(dictionary)
}
}
}
/// Push a new level into the Context
public func push(dictionary:[String:AnyObject]? = nil) {
dictionaries.append(dictionary ?? [:])
}
/// Pop the last level off of the Context
public func pop() -> [String:AnyObject]? {
return dictionaries.popLast()
}
}
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>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2014 Cocode. All rights reserved.</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>

View File

@@ -1,59 +0,0 @@
import Foundation
public struct Lexer {
public let templateString:String
let regex = try! NSRegularExpression(pattern: "(\\{\\{.*?\\}\\}|\\{%.*?%\\}|\\{#.*?#\\})", options: [])
public init(templateString:String) {
self.templateString = templateString
}
func createToken(string:String) -> Token {
func strip() -> String {
return string[string.startIndex.successor().successor()..<string.endIndex.predecessor().predecessor()].stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
}
if string.hasPrefix("{{") {
return Token.Variable(value: strip())
} else if string.hasPrefix("{%") {
return Token.Block(value: strip())
} else if string.hasPrefix("{#") {
return Token.Comment(value: strip())
}
return Token.Text(value: string)
}
/// Returns an array of tokens from a given template string.
public func tokenize() -> [Token] {
// Unfortunately NSRegularExpression doesn't have a split.
// So here's a really terrible implementation
var tokens = [Token]()
let range = NSMakeRange(0, templateString.characters.count)
var lastIndex = 0
let nsTemplateString = templateString as NSString
let options = NSMatchingOptions(rawValue: 0)
regex.enumerateMatchesInString(templateString, options: options, range: range) { (result, flags, b) in
if let result = result {
if result.range.location != lastIndex {
let previousMatch = nsTemplateString.substringWithRange(NSMakeRange(lastIndex, result.range.location - lastIndex))
tokens.append(self.createToken(previousMatch))
}
let match = nsTemplateString.substringWithRange(result.range)
tokens.append(self.createToken(match))
lastIndex = result.range.location + result.range.length
}
}
if lastIndex < templateString.characters.count {
let substring = (templateString as NSString).substringFromIndex(lastIndex)
tokens.append(Token.Text(value: substring))
}
return tokens
}
}

View File

@@ -1,237 +0,0 @@
import Foundation
public struct TemplateSyntaxError : ErrorType, Equatable, CustomStringConvertible {
public let description:String
public init(_ description:String) {
self.description = description
}
}
public func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool {
return lhs.description == rhs.description
}
public protocol NodeType {
/// Render the node in the given context
func render(context:Context) throws -> String
}
/// Render the collection of nodes in the given context
public func renderNodes(nodes:[NodeType], _ context:Context) throws -> String {
return try nodes.map { try $0.render(context) }.joinWithSeparator("")
}
public class SimpleNode : NodeType {
let handler:Context throws -> String
public init(handler:Context throws -> String) {
self.handler = handler
}
public func render(context: Context) throws -> String {
return try handler(context)
}
}
public class TextNode : NodeType {
public let text:String
public init(text:String) {
self.text = text
}
public func render(context:Context) throws -> String {
return self.text
}
}
public class VariableNode : NodeType {
public let variable:Variable
public init(variable:Variable) {
self.variable = variable
}
public init(variable:String) {
self.variable = Variable(variable)
}
public func render(context:Context) throws -> String {
let result:AnyObject? = variable.resolve(context)
if let result = result as? String {
return result
} else if let result = result as? NSObject {
return result.description
}
return ""
}
}
public class NowNode : NodeType {
public let format:Variable
public class func parse(parser:TokenParser, token:Token) -> NodeType {
var format:Variable?
let components = token.components()
if components.count == 2 {
format = Variable(components[1])
}
return 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) throws -> String {
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 ""
}
return formatter!.stringFromDate(date)
}
}
public class ForNode : NodeType {
let variable:Variable
let loopVariable:String
let nodes:[NodeType]
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
let components = token.components()
if components.count == 4 && components[2] == "in" {
let loopVariable = components[1]
let variable = components[3]
var emptyNodes = [NodeType]()
let forNodes = try parser.parse(until(["endfor", "empty"]))
if let token = parser.nextToken() {
if token.contents == "empty" {
emptyNodes = try parser.parse(until(["endfor"]))
parser.nextToken()
}
} else {
throw TemplateSyntaxError("`endfor` was not found.")
}
return ForNode(variable: variable, loopVariable: loopVariable, nodes: forNodes, emptyNodes:emptyNodes)
}
throw TemplateSyntaxError("'for' statements should use the following 'for x in y' `\(token.contents)`.")
}
public init(variable:String, loopVariable:String, nodes:[NodeType], emptyNodes:[NodeType]) {
self.variable = Variable(variable)
self.loopVariable = loopVariable
self.nodes = nodes
}
public func render(context: Context) throws -> String {
if let values = variable.resolve(context) as? [AnyObject] {
return try values.map { item in
context.push()
context[loopVariable] = item
let result = try renderNodes(nodes, context)
context.pop()
return result
}.joinWithSeparator("")
}
return ""
}
}
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 variable = token.components()[1]
var trueNodes = [NodeType]()
var falseNodes = [NodeType]()
trueNodes = try parser.parse(until(["endif", "else"]))
if let token = parser.nextToken() {
if token.contents == "else" {
falseNodes = try parser.parse(until(["endif"]))
parser.nextToken()
}
} else {
throw TemplateSyntaxError("`endif` was not found.")
}
return IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)
}
public class func parse_ifnot(parser:TokenParser, token:Token) throws -> NodeType {
let variable = token.components()[1]
var trueNodes = [NodeType]()
var falseNodes = [NodeType]()
falseNodes = try parser.parse(until(["endif", "else"]))
if let token = parser.nextToken() {
if token.contents == "else" {
trueNodes = try parser.parse(until(["endif"]))
parser.nextToken()
}
} else {
throw TemplateSyntaxError("`endif` was not found.")
}
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: AnyObject? = variable.resolve(context)
var truthy = false
if let result = result as? [AnyObject] {
if result.count > 0 {
truthy = true
}
} else if result != nil {
truthy = true
}
context.push()
let output:String
if truthy {
output = try renderNodes(trueNodes, context)
} else {
output = try renderNodes(falseNodes, context)
}
context.pop()
return output
}
}

View File

@@ -1,91 +0,0 @@
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
}
/// 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 var tags = [String:TagParser]()
public init(tokens:[Token]) {
self.tokens = tokens
registerTag("for", parser: ForNode.parse)
registerTag("if", parser: IfNode.parse)
registerTag("ifnot", parser: IfNode.parse_ifnot)
registerTag("now", parser: NowNode.parse)
registerTag("include", parser: IncludeNode.parse)
registerTag("extends", parser: ExtendsNode.parse)
registerTag("block", parser: BlockNode.parse)
}
/// 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)
})
}
/// 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(let variable):
nodes.append(VariableNode(variable: variable))
case .Block:
let tag = token.components().first
if let parse_until = parse_until {
if parse_until(parser: self, token: token) {
prependToken(token)
return nodes
}
}
if let tag = tag, let parser = self.tags[tag] {
nodes.append(try parser(self, token))
}
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)
}
}

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 <Foundation/Foundation.h>
//! Project version number for Stencil.
FOUNDATION_EXPORT double StencilVersionNumber;
//! Project version string for Stencil.
FOUNDATION_EXPORT const unsigned char StencilVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <Stencil/PublicHeader.h>

View File

@@ -1,37 +0,0 @@
import Foundation
import PathKit
public class IncludeNode : NodeType {
public let templateName:String
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
let bits = token.contents.componentsSeparatedByString("\"")
if bits.count != 3 {
throw TemplateSyntaxError("'include' tag takes one argument, the template file to be included")
}
return IncludeNode(templateName: bits[1])
}
public init(templateName:String) {
self.templateName = templateName
}
public func render(context: Context) throws -> String {
if let loader = context["loader"] as? TemplateLoader {
if let template = loader.loadTemplate(templateName) {
return try template.render(context)
}
let paths:String = loader.paths.map { path in
return path.description
}.joinWithSeparator(", ")
throw TemplateSyntaxError("'\(templateName)' template not found in \(paths)")
}
throw TemplateSyntaxError("Template loader not in context")
}
}

View File

@@ -1,108 +0,0 @@
import Foundation
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)
}
}
func any<Element>(elements:[Element], closure:(Element -> Bool)) -> Element? {
for element in elements {
if closure(element) {
return element
}
}
return nil
}
class ExtendsNode : NodeType {
let templateName:String
let blocks:[String:BlockNode]
class func parse(parser:TokenParser, token:Token) throws -> NodeType {
let bits = token.contents.componentsSeparatedByString("\"")
if bits.count != 3 {
throw TemplateSyntaxError("'extends' takes one argument, the template file to be extended")
}
let parsedNodes = try parser.parse()
if (any(parsedNodes) { ($0 as? ExtendsNode) != nil }) != nil {
throw TemplateSyntaxError("'extends' cannot appear more than once in the same template")
}
let blockNodes = parsedNodes.filter { node in node is BlockNode }
let nodes = blockNodes.reduce([String:BlockNode](), combine: { (accumulator, node:NodeType) -> [String:BlockNode] in
let node = (node as! BlockNode)
var dict = accumulator
dict[node.name] = node
return dict
})
return ExtendsNode(templateName: bits[1], blocks: nodes)
}
init(templateName:String, blocks:[String:BlockNode]) {
self.templateName = templateName
self.blocks = blocks
}
func render(context: Context) throws -> String {
if let loader = context["loader"] as? TemplateLoader {
if let template = loader.loadTemplate(templateName) {
let blockContext = BlockContext(blocks: blocks)
context.push([BlockContext.contextKey: blockContext])
let result = try template.render(context)
context.pop()
return result
}
let paths:String = loader.paths.map { path in
return path.description
}.joinWithSeparator(", ")
throw TemplateSyntaxError("'\(templateName)' template not found in \(paths)")
}
throw TemplateSyntaxError("Template loader not in context")
}
}
class BlockNode : NodeType {
let name:String
let nodes:[NodeType]
class func parse(parser:TokenParser, token:Token) throws -> NodeType {
let bits = token.components()
if bits.count != 2 {
throw TemplateSyntaxError("'block' tag takes one argument, the template file to be included")
}
let blockName = bits[1]
let nodes = try parser.parse(until(["endblock"]))
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)
}
}

View File

@@ -1,53 +0,0 @@
import Foundation
/// A structure used to represent a template variable, and to resolve it in a given context.
public struct Variable : Equatable {
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.componentsSeparatedByString(".")
}
/// Resolve the variable in the given context
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? [String:AnyObject] {
current = dictionary[bit]
} else if let array = current as? [AnyObject] {
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 {
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 Foundation
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>$(PRODUCT_BUNDLE_IDENTIFIER)</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 Foundation
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,184 +0,0 @@
import Foundation
import XCTest
import Stencil
class ErrorNode : NodeType {
func render(context: Context) throws -> String {
throw TemplateSyntaxError("Custom Error")
}
}
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")
XCTAssertEqual(try? node.render(context), "Hello World")
}
}
class VariableNodeTests: NodeTests {
func testVariableNodeResolvesVariable() {
let node = VariableNode(variable:Variable("name"))
XCTAssertEqual(try? node.render(context), "Kyle")
}
func testVariableNodeResolvesNonStringVariable() {
let node = VariableNode(variable:Variable("age"))
XCTAssertEqual(try? node.render(context), "27")
}
}
class RenderNodeTests: NodeTests {
func testRenderingNodes() {
let nodes = [TextNode(text:"Hello "), VariableNode(variable: "name")] as [NodeType]
XCTAssertEqual(try? renderNodes(nodes, context), "Hello Kyle")
}
func testRenderingNodesWithFailure() {
let nodes = [TextNode(text:"Hello "), VariableNode(variable: "name"), ErrorNode()] as [NodeType]
assertFailure(try renderNodes(nodes, context), TemplateSyntaxError("Custom Error"))
}
}
class ForNodeTests: NodeTests {
func testForNodeRender() {
let node = ForNode(variable: "items", loopVariable: "item", nodes: [VariableNode(variable: "item")], emptyNodes:[])
XCTAssertEqual(try? node.render(context), "123")
}
}
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(try 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(try 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(try parser.parse(), TemplateSyntaxError("`endif` was not found."))
}
func testParseIfNotWithoutEndIfError() {
let tokens = [
Token.Block(value: "ifnot value"),
]
let parser = TokenParser(tokens: tokens)
assertFailure(try parser.parse(), TemplateSyntaxError("`endif` was not found."))
}
// MARK: Rendering
func testIfNodeRenderTruth() {
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
XCTAssertEqual(try? node.render(context), "true")
}
func testIfNodeRenderFalse() {
let node = IfNode(variable: "unknown", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
XCTAssertEqual(try? node.render(context), "false")
}
}
class NowNodeTests: NodeTests {
// MARK: Parsing
func testParseDefaultNow() {
let tokens = [ Token.Block(value: "now") ]
let parser = TokenParser(tokens: tokens)
assertSuccess(try 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(try 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 formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let date = formatter.stringFromDate(NSDate())
XCTAssertEqual(try? node.render(context), date)
}
}

View File

@@ -1,49 +0,0 @@
import Foundation
import XCTest
import Stencil
class TokenParserTests: XCTestCase {
func testParsingTextToken() {
let parser = TokenParser(tokens: [
Token.Text(value: "Hello World")
])
assertSuccess(try 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(try 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(try parser.parse()) { nodes in
XCTAssertEqual(nodes.count, 0)
}
}
func testParsingTagToken() {
let parser = TokenParser(tokens: [
Token.Block(value: "now"),
])
assertSuccess(try parser.parse()) { nodes in
XCTAssertEqual(nodes.count, 1)
}
}
}

View File

@@ -1,77 +0,0 @@
import Foundation
import XCTest
import Stencil
func assertSuccess<T>(@autoclosure closure:() throws -> (T), block:(T -> ())) {
do {
block(try closure())
} catch {
XCTFail("Unexpected error \(error)")
}
}
func assertFailure<T, U : Equatable>(@autoclosure closure:() throws -> (T), _ error:U) {
do {
try closure()
} catch let e as U {
XCTAssertEqual(e, error)
} catch {
XCTFail()
}
}
class CustomNode : NodeType {
func render(context:Context) throws -> String {
return "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 = 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"
XCTAssertEqual(result, fixture)
}
func testCustomTag() {
let templateString = "{% custom %}"
let template = Template(templateString:templateString)
template.parser.registerTag("custom") { parser, token in
return CustomNode()
}
XCTAssertEqual(try? template.render(), "Hello World")
}
func testSimpleCustomTag() {
let templateString = "{% custom %}"
let template = Template(templateString:templateString)
template.parser.registerSimpleTag("custom") { context in
return "Hello World"
}
XCTAssertEqual(try? template.render(), "Hello World")
}
}

View File

@@ -1,64 +0,0 @@
import Foundation
import XCTest
import Stencil
import PathKit
class IncludeTests: NodeTests {
var loader:TemplateLoader!
override func setUp() {
super.setUp()
let path = (Path(__FILE__) + Path("../..")).absolute()
loader = TemplateLoader(paths: [path])
}
// MARK: Parsing
func testParseMissingTemplate() {
let tokens = [ Token.Block(value: "include") ]
let parser = TokenParser(tokens: tokens)
assertFailure(try parser.parse(), TemplateSyntaxError("'include' tag takes one argument, the template file to be included"))
}
func testParse() {
let tokens = [ Token.Block(value: "include \"test.html\"") ]
let parser = TokenParser(tokens: tokens)
assertSuccess(try parser.parse()) { nodes in
let node = nodes.first as! IncludeNode
XCTAssertEqual(nodes.count, 1)
XCTAssertEqual(node.templateName, "test.html")
}
}
// MARK: Render
func testRenderWithoutLoader() {
let node = IncludeNode(templateName: "test.html")
do {
try node.render(Context())
} catch {
XCTAssertEqual("\(error)", "Template loader not in context")
}
}
func testRenderWithoutTemplateNamed() {
let node = IncludeNode(templateName: "unknown.html")
do {
try node.render(Context(dictionary:["loader":loader]))
} catch {
XCTAssertTrue("\(error)".hasPrefix("'unknown.html' template not found"))
}
}
func testRender() {
let node = IncludeNode(templateName: "test.html")
let value = try? node.render(Context(dictionary:["loader":loader, "target": "World"]))
XCTAssertEqual(value, "Hello World!")
}
}

View File

@@ -1,44 +0,0 @@
import Foundation
import XCTest
import Stencil
import PathKit
class InheritenceTests: NodeTests {
var loader:TemplateLoader!
override func setUp() {
super.setUp()
let path = (Path(__FILE__) + Path("../..")).absolute()
loader = TemplateLoader(paths: [path])
}
func testInheritence() {
context = Context(dictionary: ["loader": loader])
let template = loader.loadTemplate("child.html")!
XCTAssertEqual(try? template.render(context), "Header\nChild")
}
}
//class BlockNodeTests: NodeTests {
// func testBlockNodeWithoutChildren() {
// let context = Context()
// let block = BlockNode(name:"header", nodes:[TextNode(text: "contents")])
// let result = block.render(context)
//
// assertSuccess(result) { rendered in
// XCTAssertEqual(rendered, "contents")
// }
// }
//
// func testBlockNodeWithChild() {
// let context = Context()
// let node = BlockNode(name:"header", nodes:[TextNode(text: "contents")])
// let childBlock = BlockNode(name: "header", nodes: [TextNode(text: "child contents")])
// let result = node.render(context)
//
// assertSuccess(result) { rendered in
// XCTAssertEqual(rendered, "child contents")
// }
// }
//}

View File

@@ -1,24 +0,0 @@
import Foundation
import XCTest
import Stencil
import PathKit
class TemplateLoaderTests: XCTestCase {
func testLoadingUnknownTemplate() {
let loader = TemplateLoader(paths:[])
XCTAssertNil(loader.loadTemplate("unknown.html"))
}
func testLoadingUnknownTemplates() {
let loader = TemplateLoader(paths:[])
XCTAssertNil(loader.loadTemplate(["unknown.html", "unknown2.html"]))
}
func testLoadingTemplate() {
let path = (Path(__FILE__) + Path("..")).absolute()
let loader = TemplateLoader(paths: [path])
XCTAssertTrue(loader.loadTemplate("test.html") != nil)
}
}

View File

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

View File

@@ -1,62 +0,0 @@
import Foundation
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
}
}

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()

View File

@@ -1,15 +0,0 @@
machine:
xcode:
version: "7.0"
test:
override:
- set -o pipefail && xcodebuild -workspace Stencil.xcworkspace -scheme Stencil test | tee $CIRCLE_ARTIFACTS/xcode_raw_ios.log | xcpretty -c
- pod lib lint
deployment:
release:
tag: /.*/
commands:
- pod trunk push