Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
226becb258 | ||
|
|
507cc5c661 | ||
|
|
9b26b7d71a | ||
|
|
19366ec71b | ||
|
|
ba65ab5fbe | ||
|
|
8ac6e26876 | ||
|
|
f35be4b701 | ||
|
|
033ae61e42 | ||
|
|
1ea58b70f3 | ||
|
|
5883775f37 | ||
|
|
d1891038f8 | ||
|
|
d5acc7298c | ||
|
|
6464b3170a | ||
|
|
d03df12cba | ||
|
|
62f6016e94 | ||
|
|
16da9ac034 | ||
|
|
7d5d226017 | ||
|
|
05dc420808 | ||
|
|
f4ed872a45 | ||
|
|
f0abd34c32 | ||
|
|
4d76fb4e60 | ||
|
|
9bdef5fee0 | ||
|
|
20cc95fb87 | ||
|
|
1136ca8fca | ||
|
|
8f334563bf | ||
|
|
b03ec50a42 | ||
|
|
2ab9b85305 | ||
|
|
a297b4ec42 | ||
|
|
9de84d5ca4 | ||
|
|
e5378b7603 | ||
|
|
29dc14855c | ||
|
|
3935dac021 | ||
|
|
9c335caeb6 | ||
|
|
25f5583542 | ||
|
|
878c5cfde8 | ||
|
|
a0bde992c2 | ||
|
|
554d2ee07f | ||
|
|
4dc8bf3d1f | ||
|
|
dcf2611ac2 | ||
|
|
c1a485c429 | ||
|
|
f9006d515a | ||
|
|
6d01792cd6 | ||
|
|
620154e721 | ||
|
|
53d5a4f8c3 | ||
|
|
59bab00c97 | ||
|
|
3839bc4147 | ||
|
|
84f117b40c | ||
|
|
d1d8e6e17f | ||
|
|
44810a82e7 | ||
|
|
028b340b54 | ||
|
|
19a7abce4c | ||
|
|
0212e8781c | ||
|
|
5aac08cabf | ||
|
|
45fcebec57 | ||
|
|
fa34c2a98e | ||
|
|
1989c20932 |
27
.gitignore
vendored
27
.gitignore
vendored
@@ -1,26 +1 @@
|
||||
# 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/
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
language: objective-c
|
||||
osx_image: xcode61
|
||||
before_install:
|
||||
- gem install cocoapods
|
||||
- gem install xcpretty
|
||||
script:
|
||||
- set -o pipefail
|
||||
- xcodebuild -project Stencil.xcodeproj -scheme Stencil test | xcpretty -c
|
||||
- xcodebuild -project Stencil.xcodeproj -scheme Stencil test -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO | xcpretty -c
|
||||
@@ -92,7 +92,7 @@ Will result in a single Node (a `ForNode`) which contains the sub-node containin
|
||||
|
||||
When the `ForNode` is rendered in a context, it will look up the variable `articles` and if it’s an array it will loop over it. Inserting the variable `article` into the context while rendered the `forNodes` for each article.
|
||||
|
||||
### Custom Node’s
|
||||
### Custom Nodes
|
||||
|
||||
There are two ways to register custom template tags. A simple way which allows you to map 1:1 a block token to a Node. You can also register a more advanced template tag which has it’s own block of code for handling parsing if you want to parse up until another token such as if you are trying to provide flow-control.
|
||||
|
||||
@@ -106,7 +106,7 @@ Here’s an example. Registering a template tag called `custom` which just rende
|
||||
|
||||
```swift
|
||||
parser.registerSimpleTag("custom") { context in
|
||||
return .Success("Hello World")
|
||||
return "Hello World"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -144,7 +144,7 @@ class DebugNode : Node {
|
||||
self.nodes = nodes
|
||||
}
|
||||
|
||||
func render(context: Context) -> Result {
|
||||
func render(context: Context) throws -> String {
|
||||
// Is there a debug variable inside the context?
|
||||
if let debug = context["debug"] as? Bool {
|
||||
// Is debug set to true?
|
||||
@@ -155,7 +155,7 @@ class DebugNode : Node {
|
||||
}
|
||||
|
||||
// Debug is turned off, so let's not render anything
|
||||
return .Success("")
|
||||
return ""
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -163,18 +163,13 @@ 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) -> TokenParser.Result in
|
||||
parser.registerTag("debug") { parser, token in
|
||||
// Use the parser to parse every token up until the `enddebug` block.
|
||||
switch parser.parse(until(["enddebug"]))
|
||||
case .Success(let nodes):
|
||||
nodes
|
||||
case .Error(let error):
|
||||
// There was an error, this is most-likely due to another template block returning an error.
|
||||
return .Error(error)
|
||||
}
|
||||
let nodes = try until(["enddebug"]))
|
||||
return DebugNode(nodes)
|
||||
}
|
||||
```
|
||||
|
||||
## Context
|
||||
|
||||
A Context is a structure containing any templates you would like to use in a template. It’s somewhat like a dictionary, however you can push and pop to scope variables. So that means that when iterating over a for loop, you can push a new scope into the context to store any variables local to the scope.
|
||||
A Context is a structure containing any templates you would like to use in a template. It’s somewhat like a dictionary, however you can push and pop to scope variables. So that means that when iterating over a for loop, you can push a new scope into the context to store any variables local to the scope.
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// UniversalFramework_Base.xcconfig
|
||||
// Stencil
|
||||
//
|
||||
// Created by Marius Rackwitz on 29/11/14.
|
||||
// Copyright (c) 2014 Marius Rackwitz. All rights reserved.
|
||||
//
|
||||
|
||||
// Make it universal
|
||||
SUPPORTED_PLATFORMS = iphonesimulator iphoneos macosx
|
||||
VALID_ARCHS[sdk=iphoneos*] = arm64 armv7 armv7s
|
||||
VALID_ARCHS[sdk=iphonesimulator*] = arm64 armv7 armv7s
|
||||
VALID_ARCHS[sdk=macosx*] = i386 x86_64
|
||||
|
||||
// Dynamic linking uses different default copy paths
|
||||
LD_RUNPATH_SEARCH_PATHS[sdk=iphoneos*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||
LD_RUNPATH_SEARCH_PATHS[sdk=iphonesimulator*] = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||
LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks'
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// UniversalFramework_Framework.xcconfig
|
||||
// Stencil
|
||||
//
|
||||
// Created by Marius Rackwitz on 29/11/14.
|
||||
// Copyright (c) 2014 Marius Rackwitz. All rights reserved.
|
||||
//
|
||||
|
||||
#include "UniversalFramework_Base.xcconfig"
|
||||
|
||||
// iOS-specific default settings
|
||||
CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer
|
||||
TARGETED_DEVICE_FAMILY[sdk=iphonesimulator*] = 1,2
|
||||
TARGETED_DEVICE_FAMILY[sdk=iphone*] = 1,2
|
||||
|
||||
// OSX-specific default settings
|
||||
FRAMEWORK_VERSION[sdk=macosx*] = A
|
||||
COMBINE_HIDPI_IMAGES[sdk=macosx*] = YES
|
||||
@@ -1,16 +0,0 @@
|
||||
//
|
||||
// UniversalFramework_Test.xcconfig
|
||||
// Stencil
|
||||
//
|
||||
// Created by Marius Rackwitz on 29/11/14.
|
||||
// Copyright (c) 2014 Marius Rackwitz. All rights reserved.
|
||||
//
|
||||
|
||||
#include "UniversalFramework_Base.xcconfig"
|
||||
|
||||
FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*] = $(inherited) '$(SDKROOT)/Developer/Library/Frameworks'
|
||||
FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*] = $(inherited) '$(SDKROOT)/Developer/Library/Frameworks'
|
||||
FRAMEWORK_SEARCH_PATHS[sdk=macosx*] = $(inherited) '$(DEVELOPER_FRAMEWORKS_DIR)'
|
||||
|
||||
// Yep.
|
||||
LD_RUNPATH_SEARCH_PATHS[sdk=macosx*] = $(inherited) '@executable_path/../Frameworks' '@loader_path/../Frameworks'
|
||||
108
README.md
108
README.md
@@ -1,15 +1,12 @@
|
||||
Stencil
|
||||
=======
|
||||
# Stencil
|
||||
|
||||
[](https://travis-ci.org/kylef/Stencil)
|
||||
[](https://circleci.com/gh/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.
|
||||
|
||||
*NOTE: Stencil requires Xcode 6.1.*
|
||||
|
||||
### Example
|
||||
## Example
|
||||
|
||||
```html+django
|
||||
There are {{ articles.count }} articles.
|
||||
@@ -21,23 +18,29 @@ There are {{ articles.count }} articles.
|
||||
|
||||
```swift
|
||||
let context = Context(dictionary: [
|
||||
"articles": [
|
||||
[ "title": "Migrating from OCUnit to XCTest", "author": "Kyle Fuller" ],
|
||||
[ "title": "Memory Management with ARC", "author": "Kyle Fuller" ],
|
||||
]
|
||||
"articles": [
|
||||
[ "title": "Migrating from OCUnit to XCTest", "author": "Kyle Fuller" ],
|
||||
[ "title": "Memory Management with ARC", "author": "Kyle Fuller" ],
|
||||
]
|
||||
])
|
||||
|
||||
let template = Template(named: "template.stencil")
|
||||
let result = template!.render(context)
|
||||
|
||||
switch result {
|
||||
case .Error(let error):
|
||||
println("There was an error rendering your template (\(error)).")
|
||||
case .Success(let string):
|
||||
println("\(string)")
|
||||
do {
|
||||
let template = Template(named: "template.stencil")
|
||||
let rendered = template.render(context)
|
||||
print(rendered)
|
||||
} catch {
|
||||
print("Failed to render template \(error)")
|
||||
}
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Installation with CocoaPods is recommended.
|
||||
|
||||
```ruby
|
||||
pod 'Stencil'
|
||||
```
|
||||
|
||||
## Philosophy
|
||||
|
||||
Stencil follows the same philosophy of Django:
|
||||
@@ -74,6 +77,53 @@ There are {{ people.count }} people, {{ people.first }} is 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 }}
|
||||
```
|
||||
|
||||
#### Registering custom filters
|
||||
|
||||
```swift
|
||||
template.parser.registerFilter("double") { value in
|
||||
if let value = value as? Int {
|
||||
return value * 2
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
```
|
||||
|
||||
### Tags
|
||||
|
||||
Tags are a mechanism to execute a piece of code, allowing you to have
|
||||
@@ -128,6 +178,22 @@ A for loop allows you to iterate over an array found by variable lookup.
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
#### include
|
||||
|
||||
You can include another template using the `include` tag.
|
||||
|
||||
```html+django
|
||||
{% include "comment.html" %}
|
||||
```
|
||||
|
||||
The `include` tag requires a TemplateLoader to be found inside your context with the paths, or bundles used to lookup the template.
|
||||
|
||||
```swift
|
||||
let context = Context(dictionary: [
|
||||
"loader": TemplateLoader(bundle:[NSBundle.mainBundle()])
|
||||
])
|
||||
```
|
||||
|
||||
#### Building custom tags
|
||||
|
||||
You can build a custom template tag. There are a couple of APIs to allow
|
||||
@@ -135,19 +201,21 @@ you to write your own custom tags. The following is the simplest form:
|
||||
|
||||
```swift
|
||||
template.parser.registerSimpleTag("custom") { context in
|
||||
return .Success("Hello World")
|
||||
return "Hello World"
|
||||
}
|
||||
```
|
||||
|
||||
When your tag is used via `{% custom %}` it will execute the registered block
|
||||
of code allowing you to modify or retrieve a value from the context. Then
|
||||
return either a string rendered in your template, or an error.
|
||||
return either a string rendered in your template, or throw an error.
|
||||
|
||||
If you want to accept arguments or to capture different tokens between two sets
|
||||
of template tags. You will need to call the `registerTag` API which accepts a
|
||||
closure to handle the parsing. You can find examples of the `now`, `if` and
|
||||
`for` tags found inside `Node.swift`.
|
||||
|
||||
Custom template tags must be registered prior to calling `Template.render` the first time.
|
||||
|
||||
The architecture of Stencil along with how to build advanced plugins can be found in the [architecture](ARCHITECTURE.md) document.
|
||||
|
||||
### Comments
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = 'Stencil'
|
||||
spec.version = '0.1.1'
|
||||
spec.summary = 'Stencil is a simple and powerful template language for Swift.'
|
||||
spec.homepage = 'https://github.com/kylef/Stencil'
|
||||
spec.license = { :type => 'BSD', :file => 'LICENSE' }
|
||||
spec.author = { 'Kyle Fuller' => 'inbox@kylefuller.co.uk' }
|
||||
spec.social_media_url = 'http://twitter.com/kylefuller'
|
||||
spec.source = { :git => 'https://github.com/kylef/Stencil.git', :tag => "#{spec.version}" }
|
||||
spec.source_files = 'Stencil/*.{h,swift}'
|
||||
spec.requires_arc = true
|
||||
end
|
||||
|
||||
41
Stencil.podspec.json
Normal file
41
Stencil.podspec.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "Stencil",
|
||||
"version": "0.4.0",
|
||||
"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": "inbox@kylefuller.co.uk"
|
||||
},
|
||||
"social_media_url": "http://twitter.com/kylefuller",
|
||||
"source": {
|
||||
"git": "https://github.com/kylef/Stencil.git",
|
||||
"tag": "0.4.0"
|
||||
},
|
||||
"source_files": [
|
||||
"Stencil/*.swift",
|
||||
"Stencil/TemplateLoader/*.swift"
|
||||
],
|
||||
"platforms": {
|
||||
"ios": "8.0",
|
||||
"osx": "10.9"
|
||||
},
|
||||
"requires_arc": true,
|
||||
"dependencies": {
|
||||
"PathKit": [ "~> 0.5.0" ]
|
||||
},
|
||||
"test_specification": {
|
||||
"source_files": [
|
||||
"StencilSpecs/*.swift",
|
||||
"StencilSpecs/TemplateLoader/*.swift",
|
||||
"StencilSpecs/Nodes/*.swift"
|
||||
],
|
||||
"dependencies": {
|
||||
"Spectre": [ "~> 0.4.1" ],
|
||||
"PathKit": [ "~> 0.5.0" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,485 +0,0 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
27E2138D1A4CD5F50073E063 /* UniversalFramework_Base.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 27E2138A1A4CD5F50073E063 /* UniversalFramework_Base.xcconfig */; };
|
||||
27E2138E1A4CD5F50073E063 /* UniversalFramework_Framework.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 27E2138B1A4CD5F50073E063 /* UniversalFramework_Framework.xcconfig */; };
|
||||
27E2138F1A4CD5F50073E063 /* UniversalFramework_Test.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 27E2138C1A4CD5F50073E063 /* UniversalFramework_Test.xcconfig */; };
|
||||
71CE4C0A19FD29D000B9E0C5 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE4C0919FD29D000B9E0C5 /* Result.swift */; };
|
||||
7725B3CB19F92B4F002CF74B /* VariableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3CA19F92B4F002CF74B /* VariableTests.swift */; };
|
||||
7725B3CD19F92B61002CF74B /* Variable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3CC19F92B61002CF74B /* Variable.swift */; };
|
||||
7725B3CF19F94214002CF74B /* Tokenizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3CE19F94214002CF74B /* Tokenizer.swift */; };
|
||||
7725B3D319F9437F002CF74B /* NodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3D219F9437F002CF74B /* NodeTests.swift */; };
|
||||
7725B3D519F9438F002CF74B /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3D419F9438F002CF74B /* Node.swift */; };
|
||||
7725B3D719F94A43002CF74B /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3D619F94A43002CF74B /* Parser.swift */; };
|
||||
7725B3D919F94A61002CF74B /* ParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3D819F94A61002CF74B /* ParserTests.swift */; };
|
||||
77EB082519F96E88001870F1 /* Template.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77EB082419F96E88001870F1 /* Template.swift */; };
|
||||
77EB082719F96E9C001870F1 /* TemplateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77EB082619F96E9C001870F1 /* TemplateTests.swift */; };
|
||||
77EB082919FA85F2001870F1 /* LexerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77EB082819FA85F2001870F1 /* LexerTests.swift */; };
|
||||
77EB082B19FA8600001870F1 /* Lexer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77EB082A19FA8600001870F1 /* Lexer.swift */; };
|
||||
77FAAE5819F91E480029DC5E /* Stencil.h in Headers */ = {isa = PBXBuildFile; fileRef = 77FAAE5719F91E480029DC5E /* Stencil.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
77FAAE5E19F91E480029DC5E /* Stencil.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77FAAE5219F91E480029DC5E /* Stencil.framework */; };
|
||||
77FAAE6519F91E480029DC5E /* StencilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77FAAE6419F91E480029DC5E /* StencilTests.swift */; };
|
||||
77FAAE6F19F920750029DC5E /* Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77FAAE6E19F920750029DC5E /* Context.swift */; };
|
||||
77FAAE7119F9208C0029DC5E /* ContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77FAAE7019F9208C0029DC5E /* ContextTests.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
77FAAE5F19F91E480029DC5E /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 77FAAE4919F91E480029DC5E /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 77FAAE5119F91E480029DC5E;
|
||||
remoteInfo = Stencil;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
27E2138A1A4CD5F50073E063 /* UniversalFramework_Base.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Base.xcconfig; sourceTree = "<group>"; };
|
||||
27E2138B1A4CD5F50073E063 /* UniversalFramework_Framework.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Framework.xcconfig; sourceTree = "<group>"; };
|
||||
27E2138C1A4CD5F50073E063 /* UniversalFramework_Test.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Test.xcconfig; sourceTree = "<group>"; };
|
||||
71CE4C0919FD29D000B9E0C5 /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = "<group>"; };
|
||||
7725B3CA19F92B4F002CF74B /* VariableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VariableTests.swift; sourceTree = "<group>"; };
|
||||
7725B3CC19F92B61002CF74B /* Variable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Variable.swift; sourceTree = "<group>"; };
|
||||
7725B3CE19F94214002CF74B /* Tokenizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tokenizer.swift; sourceTree = "<group>"; };
|
||||
7725B3D219F9437F002CF74B /* NodeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeTests.swift; sourceTree = "<group>"; };
|
||||
7725B3D419F9438F002CF74B /* Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = "<group>"; };
|
||||
7725B3D619F94A43002CF74B /* Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
|
||||
7725B3D819F94A61002CF74B /* ParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParserTests.swift; sourceTree = "<group>"; };
|
||||
77EB082419F96E88001870F1 /* Template.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Template.swift; sourceTree = "<group>"; };
|
||||
77EB082619F96E9C001870F1 /* TemplateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TemplateTests.swift; sourceTree = "<group>"; };
|
||||
77EB082819FA85F2001870F1 /* LexerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LexerTests.swift; sourceTree = "<group>"; };
|
||||
77EB082A19FA8600001870F1 /* Lexer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Lexer.swift; sourceTree = "<group>"; };
|
||||
77FAAE5219F91E480029DC5E /* Stencil.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Stencil.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
77FAAE5619F91E480029DC5E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
77FAAE5719F91E480029DC5E /* Stencil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Stencil.h; sourceTree = "<group>"; };
|
||||
77FAAE5D19F91E480029DC5E /* StencilTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StencilTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
77FAAE6319F91E480029DC5E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
77FAAE6419F91E480029DC5E /* StencilTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StencilTests.swift; sourceTree = "<group>"; };
|
||||
77FAAE6E19F920750029DC5E /* Context.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Context.swift; sourceTree = "<group>"; };
|
||||
77FAAE7019F9208C0029DC5E /* ContextTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextTests.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
77FAAE4E19F91E480029DC5E /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
77FAAE5A19F91E480029DC5E /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
77FAAE5E19F91E480029DC5E /* Stencil.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
27E213891A4CD5F50073E063 /* Configurations */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
27E2138A1A4CD5F50073E063 /* UniversalFramework_Base.xcconfig */,
|
||||
27E2138B1A4CD5F50073E063 /* UniversalFramework_Framework.xcconfig */,
|
||||
27E2138C1A4CD5F50073E063 /* UniversalFramework_Test.xcconfig */,
|
||||
);
|
||||
path = Configurations;
|
||||
sourceTree = SOURCE_ROOT;
|
||||
};
|
||||
77FAAE4819F91E480029DC5E = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
77FAAE5419F91E480029DC5E /* Stencil */,
|
||||
77FAAE6119F91E480029DC5E /* StencilTests */,
|
||||
27E213891A4CD5F50073E063 /* Configurations */,
|
||||
77FAAE5319F91E480029DC5E /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
77FAAE5319F91E480029DC5E /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
77FAAE5219F91E480029DC5E /* Stencil.framework */,
|
||||
77FAAE5D19F91E480029DC5E /* StencilTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
77FAAE5419F91E480029DC5E /* Stencil */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
77FAAE5719F91E480029DC5E /* Stencil.h */,
|
||||
77FAAE6E19F920750029DC5E /* Context.swift */,
|
||||
77EB082A19FA8600001870F1 /* Lexer.swift */,
|
||||
7725B3D419F9438F002CF74B /* Node.swift */,
|
||||
7725B3D619F94A43002CF74B /* Parser.swift */,
|
||||
71CE4C0919FD29D000B9E0C5 /* Result.swift */,
|
||||
77EB082419F96E88001870F1 /* Template.swift */,
|
||||
7725B3CE19F94214002CF74B /* Tokenizer.swift */,
|
||||
7725B3CC19F92B61002CF74B /* Variable.swift */,
|
||||
77FAAE5519F91E480029DC5E /* Supporting Files */,
|
||||
);
|
||||
path = Stencil;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
77FAAE5519F91E480029DC5E /* Supporting Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
77FAAE5619F91E480029DC5E /* Info.plist */,
|
||||
);
|
||||
name = "Supporting Files";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
77FAAE6119F91E480029DC5E /* StencilTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
77FAAE6419F91E480029DC5E /* StencilTests.swift */,
|
||||
77FAAE7019F9208C0029DC5E /* ContextTests.swift */,
|
||||
7725B3CA19F92B4F002CF74B /* VariableTests.swift */,
|
||||
7725B3D219F9437F002CF74B /* NodeTests.swift */,
|
||||
7725B3D819F94A61002CF74B /* ParserTests.swift */,
|
||||
77EB082819FA85F2001870F1 /* LexerTests.swift */,
|
||||
77EB082619F96E9C001870F1 /* TemplateTests.swift */,
|
||||
77FAAE6219F91E480029DC5E /* Supporting Files */,
|
||||
);
|
||||
path = StencilTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
77FAAE6219F91E480029DC5E /* Supporting Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
77FAAE6319F91E480029DC5E /* Info.plist */,
|
||||
);
|
||||
name = "Supporting Files";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
77FAAE4F19F91E480029DC5E /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
77FAAE5819F91E480029DC5E /* Stencil.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXHeadersBuildPhase section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
77FAAE5119F91E480029DC5E /* Stencil */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 77FAAE6819F91E480029DC5E /* Build configuration list for PBXNativeTarget "Stencil" */;
|
||||
buildPhases = (
|
||||
77FAAE4D19F91E480029DC5E /* Sources */,
|
||||
77FAAE4E19F91E480029DC5E /* Frameworks */,
|
||||
77FAAE4F19F91E480029DC5E /* Headers */,
|
||||
77FAAE5019F91E480029DC5E /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Stencil;
|
||||
productName = Stencil;
|
||||
productReference = 77FAAE5219F91E480029DC5E /* Stencil.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
77FAAE5C19F91E480029DC5E /* StencilTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 77FAAE6B19F91E480029DC5E /* Build configuration list for PBXNativeTarget "StencilTests" */;
|
||||
buildPhases = (
|
||||
77FAAE5919F91E480029DC5E /* Sources */,
|
||||
77FAAE5A19F91E480029DC5E /* Frameworks */,
|
||||
77FAAE5B19F91E480029DC5E /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
77FAAE6019F91E480029DC5E /* PBXTargetDependency */,
|
||||
);
|
||||
name = StencilTests;
|
||||
productName = StencilTests;
|
||||
productReference = 77FAAE5D19F91E480029DC5E /* StencilTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
77FAAE4919F91E480029DC5E /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 0600;
|
||||
ORGANIZATIONNAME = Cocode;
|
||||
TargetAttributes = {
|
||||
77FAAE5119F91E480029DC5E = {
|
||||
CreatedOnToolsVersion = 6.1;
|
||||
};
|
||||
77FAAE5C19F91E480029DC5E = {
|
||||
CreatedOnToolsVersion = 6.1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 77FAAE4C19F91E480029DC5E /* Build configuration list for PBXProject "Stencil" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
);
|
||||
mainGroup = 77FAAE4819F91E480029DC5E;
|
||||
productRefGroup = 77FAAE5319F91E480029DC5E /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
77FAAE5119F91E480029DC5E /* Stencil */,
|
||||
77FAAE5C19F91E480029DC5E /* StencilTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
77FAAE5019F91E480029DC5E /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
27E2138F1A4CD5F50073E063 /* UniversalFramework_Test.xcconfig in Resources */,
|
||||
27E2138E1A4CD5F50073E063 /* UniversalFramework_Framework.xcconfig in Resources */,
|
||||
27E2138D1A4CD5F50073E063 /* UniversalFramework_Base.xcconfig in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
77FAAE5B19F91E480029DC5E /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
77FAAE4D19F91E480029DC5E /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
77FAAE6F19F920750029DC5E /* Context.swift in Sources */,
|
||||
77EB082B19FA8600001870F1 /* Lexer.swift in Sources */,
|
||||
7725B3CF19F94214002CF74B /* Tokenizer.swift in Sources */,
|
||||
7725B3D719F94A43002CF74B /* Parser.swift in Sources */,
|
||||
77EB082519F96E88001870F1 /* Template.swift in Sources */,
|
||||
7725B3CD19F92B61002CF74B /* Variable.swift in Sources */,
|
||||
71CE4C0A19FD29D000B9E0C5 /* Result.swift in Sources */,
|
||||
7725B3D519F9438F002CF74B /* Node.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
77FAAE5919F91E480029DC5E /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
77FAAE6519F91E480029DC5E /* StencilTests.swift in Sources */,
|
||||
7725B3D319F9437F002CF74B /* NodeTests.swift in Sources */,
|
||||
7725B3D919F94A61002CF74B /* ParserTests.swift in Sources */,
|
||||
77EB082719F96E9C001870F1 /* TemplateTests.swift in Sources */,
|
||||
7725B3CB19F92B4F002CF74B /* VariableTests.swift in Sources */,
|
||||
77EB082919FA85F2001870F1 /* LexerTests.swift in Sources */,
|
||||
77FAAE7119F9208C0029DC5E /* ContextTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
77FAAE6019F91E480029DC5E /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 77FAAE5119F91E480029DC5E /* Stencil */;
|
||||
targetProxy = 77FAAE5F19F91E480029DC5E /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
77FAAE6619F91E480029DC5E /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.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 = 27E2138B1A4CD5F50073E063 /* UniversalFramework_Framework.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";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
77FAAE6A19F91E480029DC5E /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 27E2138B1A4CD5F50073E063 /* UniversalFramework_Framework.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";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
77FAAE6C19F91E480029DC5E /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 27E2138C1A4CD5F50073E063 /* UniversalFramework_Test.xcconfig */;
|
||||
buildSettings = {
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = StencilTests/Info.plist;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
77FAAE6D19F91E480029DC5E /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 27E2138C1A4CD5F50073E063 /* UniversalFramework_Test.xcconfig */;
|
||||
buildSettings = {
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = StencilTests/Info.plist;
|
||||
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 */;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:Stencil.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -1,110 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0610"
|
||||
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
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
buildConfiguration = "Debug">
|
||||
<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>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Debug"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "77FAAE5119F91E480029DC5E"
|
||||
BuildableName = "Stencil.framework"
|
||||
BlueprintName = "Stencil"
|
||||
ReferencedContainer = "container:Stencil.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Release"
|
||||
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>
|
||||
@@ -1,52 +1,53 @@
|
||||
import Foundation
|
||||
|
||||
/// A container for template variables.
|
||||
public class Context : Equatable {
|
||||
var dictionaries:[Dictionary<String, AnyObject>]
|
||||
public class Context {
|
||||
var dictionaries:[[String: Any]]
|
||||
|
||||
public init(dictionary:Dictionary<String, AnyObject>) {
|
||||
dictionaries = [dictionary]
|
||||
}
|
||||
/// Initialise a Context with a dictionary
|
||||
public init(dictionary:[String: Any]) {
|
||||
dictionaries = [dictionary]
|
||||
}
|
||||
|
||||
public init() {
|
||||
dictionaries = []
|
||||
}
|
||||
/// 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 reverse(dictionaries) {
|
||||
if let value:AnyObject = dictionary[key] {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a variable in the current context, deleting the variable if it's nil
|
||||
set(value) {
|
||||
if dictionaries.count > 0 {
|
||||
var dictionary = dictionaries.removeLast()
|
||||
dictionary[key] = value
|
||||
dictionaries.append(dictionary)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func push() {
|
||||
push(Dictionary<String, String>())
|
||||
}
|
||||
|
||||
public func push(dictionary:Dictionary<String, String>) {
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func pop() {
|
||||
dictionaries.removeLast()
|
||||
}
|
||||
}
|
||||
/// Push a new level into the Context
|
||||
public func push(dictionary: [String: Any]? = nil) {
|
||||
dictionaries.append(dictionary ?? [:])
|
||||
}
|
||||
|
||||
public func ==(lhs:Context, rhs:Context) -> Bool {
|
||||
return lhs.dictionaries == rhs.dictionaries
|
||||
/// Pop the last level off of the Context
|
||||
public func pop() -> [String: Any]? {
|
||||
return dictionaries.popLast()
|
||||
}
|
||||
|
||||
/// Push a new level onto the context for the duration of the execution of the given closure
|
||||
public func push<Result>(dictionary: [String: Any]? = nil, @noescape closure: (() throws -> Result)) rethrows -> Result {
|
||||
push(dictionary)
|
||||
let result = try closure()
|
||||
pop()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
33
Stencil/Filters.swift
Normal file
33
Stencil/Filters.swift
Normal 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
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.cocode.$(PRODUCT_NAME:rfc1034identifier)</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
|
||||
@@ -1,57 +1,59 @@
|
||||
import Foundation
|
||||
|
||||
public struct Lexer {
|
||||
public let templateString:String
|
||||
let regex = NSRegularExpression(pattern: "(\\{\\{.*?\\}\\}|\\{%.*?%\\}|\\{#.*?#\\})", options: nil, error: nil)!
|
||||
public let templateString:String
|
||||
let regex = try! NSRegularExpression(pattern: "(\\{\\{.*?\\}\\}|\\{%.*?%\\}|\\{#.*?#\\})", options: [])
|
||||
|
||||
public init(templateString:String) {
|
||||
self.templateString = templateString
|
||||
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())
|
||||
}
|
||||
|
||||
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)
|
||||
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())
|
||||
}
|
||||
|
||||
/// 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
|
||||
return Token.Text(value: string)
|
||||
}
|
||||
|
||||
var tokens = [Token]()
|
||||
/// 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
|
||||
|
||||
let range = NSMakeRange(0, countElements(templateString))
|
||||
var lastIndex = 0
|
||||
let nsTemplateString = templateString as NSString
|
||||
let options = NSMatchingOptions(0)
|
||||
regex.enumerateMatchesInString(templateString, options: options, range: range) { (result, flags, b) in
|
||||
if result.range.location != lastIndex {
|
||||
let previousMatch = nsTemplateString.substringWithRange(NSMakeRange(lastIndex, result.range.location - lastIndex))
|
||||
tokens.append(self.createToken(previousMatch))
|
||||
}
|
||||
var tokens = [Token]()
|
||||
|
||||
let match = nsTemplateString.substringWithRange(result.range)
|
||||
tokens.append(self.createToken(match))
|
||||
|
||||
lastIndex = result.range.location + result.range.length
|
||||
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))
|
||||
}
|
||||
|
||||
if lastIndex < countElements(templateString) {
|
||||
let substring = (templateString as NSString).substringFromIndex(lastIndex)
|
||||
tokens.append(Token.Text(value: substring))
|
||||
}
|
||||
let match = nsTemplateString.substringWithRange(result.range)
|
||||
tokens.append(self.createToken(match))
|
||||
|
||||
return tokens
|
||||
lastIndex = result.range.location + result.range.length
|
||||
}
|
||||
}
|
||||
|
||||
if lastIndex < templateString.characters.count {
|
||||
let substring = (templateString as NSString).substringFromIndex(lastIndex)
|
||||
tokens.append(Token.Text(value: substring))
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,305 +1,254 @@
|
||||
import Foundation
|
||||
|
||||
struct NodeError : Error {
|
||||
let token:Token
|
||||
let message:String
|
||||
public struct TemplateSyntaxError : ErrorType, Equatable, CustomStringConvertible {
|
||||
public let description:String
|
||||
|
||||
init(token:Token, message:String) {
|
||||
self.token = token
|
||||
self.message = message
|
||||
}
|
||||
|
||||
var description:String {
|
||||
return "\(token.components().first!): \(message)"
|
||||
}
|
||||
public init(_ description:String) {
|
||||
self.description = description
|
||||
}
|
||||
}
|
||||
|
||||
public protocol Node {
|
||||
/// Return the node rendered as a string, or returns a failure
|
||||
func render(context:Context) -> Result
|
||||
public func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool {
|
||||
return lhs.description == rhs.description
|
||||
}
|
||||
|
||||
extension Array {
|
||||
func map<U>(block:((Element) -> (U?, Error?))) -> ([U]?, Error?) {
|
||||
var results = [U]()
|
||||
|
||||
for item in self {
|
||||
let (result, error) = block(item)
|
||||
|
||||
if let error = error {
|
||||
return (nil, error)
|
||||
} else if (result != nil) {
|
||||
// let result = result exposing a bug in the Swift compier :(
|
||||
results.append(result!)
|
||||
}
|
||||
}
|
||||
|
||||
return (results, nil)
|
||||
}
|
||||
public protocol NodeType {
|
||||
/// Render the node in the given context
|
||||
func render(context:Context) throws -> String
|
||||
}
|
||||
|
||||
public func renderNodes(nodes:[Node], context:Context) -> Result {
|
||||
var result = ""
|
||||
|
||||
for item in nodes {
|
||||
switch item.render(context) {
|
||||
case .Success(let string):
|
||||
result += string
|
||||
case .Error(let error):
|
||||
return .Error(error)
|
||||
}
|
||||
}
|
||||
|
||||
return .Success(result)
|
||||
/// 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 : Node {
|
||||
let handler:(Context) -> (Result)
|
||||
public class SimpleNode : NodeType {
|
||||
let handler:Context throws -> String
|
||||
|
||||
public init(handler:((Context) -> (Result))) {
|
||||
self.handler = handler
|
||||
}
|
||||
public init(handler:Context throws -> String) {
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
public func render(context:Context) -> Result {
|
||||
return handler(context)
|
||||
}
|
||||
public func render(context: Context) throws -> String {
|
||||
return try handler(context)
|
||||
}
|
||||
}
|
||||
|
||||
public class TextNode : Node {
|
||||
public let text:String
|
||||
public class TextNode : NodeType {
|
||||
public let text:String
|
||||
|
||||
public init(text:String) {
|
||||
self.text = text
|
||||
}
|
||||
public init(text:String) {
|
||||
self.text = text
|
||||
}
|
||||
|
||||
public func render(context:Context) -> Result {
|
||||
return .Success(self.text)
|
||||
}
|
||||
public func render(context:Context) throws -> String {
|
||||
return self.text
|
||||
}
|
||||
}
|
||||
|
||||
public class VariableNode : Node {
|
||||
public let variable:Variable
|
||||
|
||||
public init(variable:Variable) {
|
||||
self.variable = variable
|
||||
}
|
||||
|
||||
public init(variable:String) {
|
||||
self.variable = Variable(variable)
|
||||
}
|
||||
|
||||
public func render(context:Context) -> Result {
|
||||
let result:AnyObject? = variable.resolve(context)
|
||||
|
||||
if let result = result as? String {
|
||||
return .Success(result)
|
||||
} else if let result = result as? NSObject {
|
||||
return .Success(result.description)
|
||||
}
|
||||
|
||||
return .Success("")
|
||||
}
|
||||
public protocol Resolvable {
|
||||
func resolve(context: Context) throws -> Any?
|
||||
}
|
||||
|
||||
public class NowNode : Node {
|
||||
public let format:Variable
|
||||
public class VariableNode : NodeType {
|
||||
public let variable: Resolvable
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) -> TokenParser.Result {
|
||||
var format:Variable?
|
||||
public init(variable: Resolvable) {
|
||||
self.variable = variable
|
||||
}
|
||||
|
||||
let components = token.components()
|
||||
if components.count == 2 {
|
||||
format = Variable(components[1])
|
||||
}
|
||||
public init(variable: String) {
|
||||
self.variable = Variable(variable)
|
||||
}
|
||||
|
||||
return .Success(node:NowNode(format:format))
|
||||
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
|
||||
}
|
||||
|
||||
public init(format:Variable?) {
|
||||
if let format = format {
|
||||
self.format = format
|
||||
} else {
|
||||
self.format = Variable("\"yyyy-MM-dd 'at' HH:mm\"")
|
||||
}
|
||||
}
|
||||
|
||||
public func render(context: Context) -> Result {
|
||||
let date = NSDate()
|
||||
let format: AnyObject? = self.format.resolve(context)
|
||||
var formatter:NSDateFormatter?
|
||||
|
||||
if let format = format as? NSDateFormatter {
|
||||
formatter = format
|
||||
} else if let format = format as? String {
|
||||
formatter = NSDateFormatter()
|
||||
formatter!.dateFormat = format
|
||||
} else {
|
||||
return .Success("")
|
||||
}
|
||||
|
||||
return .Success(formatter!.stringFromDate(date))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
public class ForNode : Node {
|
||||
let variable:Variable
|
||||
let loopVariable:String
|
||||
let nodes:[Node]
|
||||
public class NowNode : NodeType {
|
||||
public let format:Variable
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) -> TokenParser.Result {
|
||||
let components = token.components()
|
||||
let count = countElements(components)
|
||||
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
var format:Variable?
|
||||
|
||||
if count == 4 && components[2] == "in" {
|
||||
let loopVariable = components[1]
|
||||
let variable = components[3]
|
||||
|
||||
var forNodes:[Node]!
|
||||
var emptyNodes = [Node]()
|
||||
|
||||
switch parser.parse(until(["endfor", "empty"])) {
|
||||
case .Success(let nodes):
|
||||
forNodes = nodes
|
||||
case .Error(let error):
|
||||
return .Error(error)
|
||||
}
|
||||
|
||||
if let token = parser.nextToken() {
|
||||
if token.contents == "empty" {
|
||||
switch parser.parse(until(["endfor"])) {
|
||||
case .Success(let nodes):
|
||||
emptyNodes = nodes
|
||||
case .Error(let error):
|
||||
return .Error(error)
|
||||
}
|
||||
|
||||
parser.nextToken()
|
||||
}
|
||||
} else {
|
||||
return .Error(error: NodeError(token: token, message: "`endfor` was not found."))
|
||||
}
|
||||
|
||||
return .Success(node:ForNode(variable: variable, loopVariable: loopVariable, nodes: forNodes, emptyNodes:emptyNodes))
|
||||
}
|
||||
|
||||
return .Error(error: NodeError(token: token, message: "Invalid syntax. Expected `for x in y`."))
|
||||
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])
|
||||
}
|
||||
|
||||
public init(variable:String, loopVariable:String, nodes:[Node], emptyNodes:[Node]) {
|
||||
self.variable = Variable(variable)
|
||||
self.loopVariable = loopVariable
|
||||
self.nodes = nodes
|
||||
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 ""
|
||||
}
|
||||
|
||||
public func render(context: Context) -> Result {
|
||||
let values = variable.resolve(context) as? [AnyObject]
|
||||
var output = ""
|
||||
|
||||
if let values = values {
|
||||
for item in values {
|
||||
context.push()
|
||||
context[loopVariable] = item
|
||||
let result = renderNodes(nodes, context)
|
||||
context.pop()
|
||||
|
||||
switch result {
|
||||
case .Success(let string):
|
||||
output += string
|
||||
case .Error(let error):
|
||||
return .Error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return .Success(output)
|
||||
}
|
||||
return formatter!.stringFromDate(date)
|
||||
}
|
||||
}
|
||||
|
||||
public class IfNode : Node {
|
||||
public let variable:Variable
|
||||
public let trueNodes:[Node]
|
||||
public let falseNodes:[Node]
|
||||
public class ForNode : NodeType {
|
||||
let variable:Variable
|
||||
let loopVariable:String
|
||||
let nodes:[NodeType]
|
||||
let emptyNodes: [NodeType]
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) -> TokenParser.Result {
|
||||
let variable = token.components()[1]
|
||||
var trueNodes = [Node]()
|
||||
var falseNodes = [Node]()
|
||||
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
|
||||
switch parser.parse(until(["endif", "else"])) {
|
||||
case .Success(let nodes):
|
||||
trueNodes = nodes
|
||||
case .Error(let error):
|
||||
return .Error(error)
|
||||
}
|
||||
|
||||
if let token = parser.nextToken() {
|
||||
if token.contents == "else" {
|
||||
switch parser.parse(until(["endif"])) {
|
||||
case .Success(let nodes):
|
||||
falseNodes = nodes
|
||||
case .Error(let error):
|
||||
return .Error(error)
|
||||
}
|
||||
parser.nextToken()
|
||||
}
|
||||
} else {
|
||||
return .Error(error:NodeError(token: token, message: "`endif` was not found."))
|
||||
}
|
||||
|
||||
return .Success(node:IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes))
|
||||
guard components.count == 4 && components[2] == "in" else {
|
||||
throw TemplateSyntaxError("'for' statements should use the following 'for x in y' `\(token.contents)`.")
|
||||
}
|
||||
|
||||
public class func parse_ifnot(parser:TokenParser, token:Token) -> TokenParser.Result {
|
||||
let variable = token.components()[1]
|
||||
var trueNodes = [Node]()
|
||||
var falseNodes = [Node]()
|
||||
let loopVariable = components[1]
|
||||
let variable = components[3]
|
||||
|
||||
switch parser.parse(until(["endif", "else"])) {
|
||||
case .Success(let nodes):
|
||||
falseNodes = nodes
|
||||
case .Error(let error):
|
||||
return .Error(error)
|
||||
}
|
||||
var emptyNodes = [NodeType]()
|
||||
|
||||
if let token = parser.nextToken() {
|
||||
if token.contents == "else" {
|
||||
switch parser.parse(until(["endif"])) {
|
||||
case .Success(let nodes):
|
||||
trueNodes = nodes
|
||||
case .Error(let error):
|
||||
return .Error(error)
|
||||
}
|
||||
parser.nextToken()
|
||||
}
|
||||
} else {
|
||||
return .Error(error:NodeError(token: token, message: "`endif` was not found."))
|
||||
}
|
||||
let forNodes = try parser.parse(until(["endfor", "empty"]))
|
||||
|
||||
return .Success(node:IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes))
|
||||
guard let token = parser.nextToken() else {
|
||||
throw TemplateSyntaxError("`endfor` was not found.")
|
||||
}
|
||||
|
||||
public init(variable:String, trueNodes:[Node], falseNodes:[Node]) {
|
||||
self.variable = Variable(variable)
|
||||
self.trueNodes = trueNodes
|
||||
self.falseNodes = falseNodes
|
||||
if token.contents == "empty" {
|
||||
emptyNodes = try parser.parse(until(["endfor"]))
|
||||
parser.nextToken()
|
||||
}
|
||||
|
||||
public func render(context: Context) -> Result {
|
||||
let result: AnyObject? = variable.resolve(context)
|
||||
var truthy = false
|
||||
return ForNode(variable: variable, loopVariable: loopVariable, nodes: forNodes, emptyNodes:emptyNodes)
|
||||
}
|
||||
|
||||
if let result = result as? [AnyObject] {
|
||||
if result.count > 0 {
|
||||
truthy = true
|
||||
}
|
||||
} else if let result: AnyObject = result {
|
||||
truthy = true
|
||||
public init(variable:String, loopVariable:String, nodes:[NodeType], emptyNodes:[NodeType]) {
|
||||
self.variable = Variable(variable)
|
||||
self.loopVariable = loopVariable
|
||||
self.nodes = nodes
|
||||
self.emptyNodes = emptyNodes
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
let values = try variable.resolve(context)
|
||||
|
||||
if let values = values as? NSArray where values.count > 0 {
|
||||
return try values.map { item in
|
||||
try context.push([loopVariable: item]) {
|
||||
try renderNodes(nodes, context)
|
||||
}
|
||||
|
||||
context.push()
|
||||
let output = renderNodes(truthy ? trueNodes : falseNodes, context)
|
||||
context.pop()
|
||||
|
||||
return output
|
||||
}.joinWithSeparator("")
|
||||
}
|
||||
|
||||
return try context.push {
|
||||
try renderNodes(emptyNodes, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class IfNode : NodeType {
|
||||
public let variable:Variable
|
||||
public let trueNodes:[NodeType]
|
||||
public let falseNodes:[NodeType]
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
guard components.count == 2 else {
|
||||
throw TemplateSyntaxError("'if' statements should use the following 'if condition' `\(token.contents)`.")
|
||||
}
|
||||
let variable = components[1]
|
||||
var trueNodes = [NodeType]()
|
||||
var falseNodes = [NodeType]()
|
||||
|
||||
trueNodes = try parser.parse(until(["endif", "else"]))
|
||||
|
||||
guard let token = parser.nextToken() else {
|
||||
throw TemplateSyntaxError("`endif` was not found.")
|
||||
}
|
||||
|
||||
if token.contents == "else" {
|
||||
falseNodes = try parser.parse(until(["endif"]))
|
||||
parser.nextToken()
|
||||
}
|
||||
|
||||
return IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)
|
||||
}
|
||||
|
||||
public class func parse_ifnot(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
guard components.count == 2 else {
|
||||
throw TemplateSyntaxError("'ifnot' statements should use the following 'if condition' `\(token.contents)`.")
|
||||
}
|
||||
let variable = components[1]
|
||||
var trueNodes = [NodeType]()
|
||||
var falseNodes = [NodeType]()
|
||||
|
||||
falseNodes = try parser.parse(until(["endif", "else"]))
|
||||
|
||||
guard let token = parser.nextToken() else {
|
||||
throw TemplateSyntaxError("`endif` was not found.")
|
||||
}
|
||||
|
||||
if token.contents == "else" {
|
||||
trueNodes = try parser.parse(until(["endif"]))
|
||||
parser.nextToken()
|
||||
}
|
||||
|
||||
return IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)
|
||||
}
|
||||
|
||||
public init(variable:String, trueNodes:[NodeType], falseNodes:[NodeType]) {
|
||||
self.variable = Variable(variable)
|
||||
self.trueNodes = trueNodes
|
||||
self.falseNodes = falseNodes
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
let result = try variable.resolve(context)
|
||||
var truthy = false
|
||||
|
||||
if let result = result as? NSArray {
|
||||
if result.count > 0 {
|
||||
truthy = true
|
||||
}
|
||||
} else if result != nil {
|
||||
truthy = true
|
||||
}
|
||||
|
||||
context.push()
|
||||
let output:String
|
||||
if truthy {
|
||||
output = try renderNodes(trueNodes, context)
|
||||
} else {
|
||||
output = try renderNodes(falseNodes, context)
|
||||
}
|
||||
context.pop()
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,108 +1,115 @@
|
||||
import Foundation
|
||||
|
||||
public func until(tags:[String])(parser:TokenParser, token:Token) -> Bool {
|
||||
if let name = token.components().first {
|
||||
for tag in tags {
|
||||
if name == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if let name = token.components().first {
|
||||
for tag in tags {
|
||||
if name == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return false
|
||||
}
|
||||
|
||||
public typealias Filter = Any? throws -> Any?
|
||||
|
||||
/// 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) -> Result
|
||||
public typealias NodeList = [Node]
|
||||
public typealias TagParser = (TokenParser, Token) throws -> NodeType
|
||||
|
||||
public enum Result {
|
||||
case Success(node: Node)
|
||||
case Error(error: Stencil.Error)
|
||||
}
|
||||
private var tokens:[Token]
|
||||
private var tags = [String:TagParser]()
|
||||
private var filters = [String: Filter]()
|
||||
|
||||
public enum Results {
|
||||
case Success(nodes: NodeList)
|
||||
case Error(error: Stencil.Error)
|
||||
}
|
||||
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)
|
||||
registerFilter("capitalize", filter: capitalise)
|
||||
registerFilter("uppercase", filter: uppercase)
|
||||
registerFilter("lowercase", filter: lowercase)
|
||||
}
|
||||
|
||||
private var tokens:[Token]
|
||||
private var tags = Dictionary<String, TagParser>()
|
||||
/// Registers a new template tag
|
||||
public func registerTag(name:String, parser:TagParser) {
|
||||
tags[name] = parser
|
||||
}
|
||||
|
||||
public init(tokens:[Token]) {
|
||||
self.tokens = tokens
|
||||
registerTag("for", ForNode.parse)
|
||||
registerTag("if", IfNode.parse)
|
||||
registerTag("ifnot", IfNode.parse_ifnot)
|
||||
registerTag("now", NowNode.parse)
|
||||
}
|
||||
/// 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 new template tag
|
||||
public func registerTag(name:String, parser:TagParser) {
|
||||
tags[name] = parser
|
||||
}
|
||||
public func registerFilter(name: String, filter: Filter) {
|
||||
filters[name] = filter
|
||||
}
|
||||
|
||||
/// Registers a simple template tag with a name and a handler
|
||||
public func registerSimpleTag(name:String, handler:((Context) -> (Stencil.Result))) {
|
||||
registerTag(name, parser: { (parser, token) -> TokenParser.Result in
|
||||
return .Success(node:SimpleNode(handler: handler))
|
||||
})
|
||||
}
|
||||
/// Parse the given tokens into nodes
|
||||
public func parse() throws -> [NodeType] {
|
||||
return try parse(nil)
|
||||
}
|
||||
|
||||
/// Parse the given tokens into nodes
|
||||
public func parse() -> Results {
|
||||
return parse(nil)
|
||||
}
|
||||
public func parse(parse_until:((parser:TokenParser, token:Token) -> (Bool))?) throws -> [NodeType] {
|
||||
var nodes = [NodeType]()
|
||||
|
||||
public func parse(parse_until:((parser:TokenParser, token:Token) -> (Bool))?) -> TokenParser.Results {
|
||||
var nodes = NodeList()
|
||||
while tokens.count > 0 {
|
||||
let token = nextToken()!
|
||||
|
||||
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
|
||||
|
||||
switch token {
|
||||
case .Text(let text):
|
||||
nodes.append(TextNode(text: text))
|
||||
case .Variable(let variable):
|
||||
nodes.append(VariableNode(variable: variable))
|
||||
case .Block(let value):
|
||||
let tag = token.components().first
|
||||
|
||||
if let parse_until = parse_until {
|
||||
if parse_until(parser: self, token: token) {
|
||||
prependToken(token)
|
||||
return .Success(nodes:nodes)
|
||||
}
|
||||
}
|
||||
|
||||
if let tag = tag {
|
||||
if let parser = self.tags[tag] {
|
||||
switch parser(self, token) {
|
||||
case .Success(let node):
|
||||
nodes.append(node)
|
||||
case .Error(let error):
|
||||
return .Error(error:error)
|
||||
}
|
||||
}
|
||||
}
|
||||
case .Comment(let value):
|
||||
continue
|
||||
}
|
||||
if let parse_until = parse_until where parse_until(parser: self, token: token) {
|
||||
prependToken(token)
|
||||
return nodes
|
||||
}
|
||||
|
||||
return .Success(nodes:nodes)
|
||||
}
|
||||
|
||||
public func nextToken() -> Token? {
|
||||
if tokens.count > 0 {
|
||||
return tokens.removeAtIndex(0)
|
||||
if let tag = tag {
|
||||
if let parser = self.tags[tag] {
|
||||
nodes.append(try parser(self, token))
|
||||
} else {
|
||||
throw TemplateSyntaxError("Unknown template tag '\(tag)'")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
case .Comment:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
public func prependToken(token:Token) {
|
||||
tokens.insert(token, atIndex: 0)
|
||||
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 = filters[name] {
|
||||
return filter
|
||||
}
|
||||
|
||||
throw TemplateSyntaxError("Invalid filter '\(name)'")
|
||||
}
|
||||
|
||||
func compileFilter(token: String) throws -> Resolvable {
|
||||
return try FilterExpression(token: token, parser: self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public protocol Error : Printable {
|
||||
|
||||
}
|
||||
|
||||
public func ==(lhs:Error, rhs:Error) -> Bool {
|
||||
return lhs.description == rhs.description
|
||||
}
|
||||
|
||||
public enum Result : Equatable {
|
||||
case Success(String)
|
||||
case Error(Stencil.Error)
|
||||
}
|
||||
|
||||
public func ==(lhs:Result, rhs:Result) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.Success(let lhsValue), .Success(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.Error(let lhsValue), .Error(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,60 +1,44 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
/// A class representing a template
|
||||
public class Template {
|
||||
public let parser:TokenParser
|
||||
public let parser:TokenParser
|
||||
private var nodes:[NodeType]? = nil
|
||||
|
||||
/// Create a template with the given name inside the main bundle
|
||||
public convenience init?(named:String) {
|
||||
self.init(named:named, inBundle:nil)
|
||||
/// Create a template with the given name inside the given bundle
|
||||
public convenience init(named:String, inBundle bundle:NSBundle? = nil) throws {
|
||||
let useBundle = bundle ?? NSBundle.mainBundle()
|
||||
guard let url = useBundle.URLForResource(named, withExtension: nil) else {
|
||||
throw NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError, userInfo: nil)
|
||||
}
|
||||
|
||||
/// Create a template with the given name inside the given bundle
|
||||
public convenience init?(named:String, inBundle bundle:NSBundle?) {
|
||||
var url:NSURL?
|
||||
try self.init(URL:url)
|
||||
}
|
||||
|
||||
if let bundle = bundle {
|
||||
url = bundle.URLForResource(named, withExtension: nil)
|
||||
} else {
|
||||
url = NSBundle.mainBundle().URLForResource(named, withExtension: nil)
|
||||
}
|
||||
/// Create a template with a file found at the given URL
|
||||
public convenience init(URL:NSURL) throws {
|
||||
try self.init(path: Path(URL.path!))
|
||||
}
|
||||
|
||||
self.init(URL:url!)
|
||||
/// Create a template with a file found at the given path
|
||||
public convenience init(path:Path) throws {
|
||||
self.init(templateString: try path.read())
|
||||
}
|
||||
|
||||
/// Create a template with a template string
|
||||
public init(templateString:String) {
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
parser = TokenParser(tokens: tokens)
|
||||
}
|
||||
|
||||
/// Render the given template
|
||||
public func render(context:Context? = nil) throws -> String {
|
||||
if nodes == nil {
|
||||
nodes = try parser.parse()
|
||||
}
|
||||
|
||||
/// Create a template with a file found at the given URL
|
||||
public convenience init?(URL:NSURL) {
|
||||
var error:NSError?
|
||||
let maybeTemplateString = NSString(contentsOfURL: URL, encoding: NSUTF8StringEncoding, error: &error)
|
||||
if let templateString = maybeTemplateString {
|
||||
self.init(templateString:templateString)
|
||||
} else {
|
||||
self.init(templateString:"")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a template with a template string
|
||||
public init(templateString:String) {
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
parser = TokenParser(tokens: tokens)
|
||||
}
|
||||
|
||||
/// Render the given template in a context
|
||||
public func render(context:Context) -> Result {
|
||||
switch parser.parse() {
|
||||
case .Success(let nodes):
|
||||
return renderNodes(nodes, context)
|
||||
|
||||
case .Error(let error):
|
||||
return .Error(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the given template without a context
|
||||
public func render() -> Result {
|
||||
let context = Context()
|
||||
return render(context)
|
||||
}
|
||||
return try renderNodes(nodes!, context ?? Context())
|
||||
}
|
||||
}
|
||||
|
||||
37
Stencil/TemplateLoader.swift
Normal file
37
Stencil/TemplateLoader.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
// A class for loading a template from disk
|
||||
public class TemplateLoader {
|
||||
public let paths:[Path]
|
||||
|
||||
public init(paths:[Path]) {
|
||||
self.paths = paths
|
||||
}
|
||||
|
||||
public init(bundle:[NSBundle]) {
|
||||
self.paths = bundle.map {
|
||||
return Path($0.bundlePath)
|
||||
}
|
||||
}
|
||||
|
||||
public func loadTemplate(templateName:String) -> Template? {
|
||||
return loadTemplate([templateName])
|
||||
}
|
||||
|
||||
public func loadTemplate(templateNames:[String]) -> Template? {
|
||||
for path in paths {
|
||||
for templateName in templateNames {
|
||||
let templatePath = path + Path(templateName)
|
||||
|
||||
if templatePath.exists {
|
||||
if let template = try? Template(path: templatePath) {
|
||||
return template
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
35
Stencil/TemplateLoader/Include.swift
Normal file
35
Stencil/TemplateLoader/Include.swift
Normal file
@@ -0,0 +1,35 @@
|
||||
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("\"")
|
||||
|
||||
guard bits.count == 3 else {
|
||||
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 {
|
||||
guard let loader = context["loader"] as? TemplateLoader else {
|
||||
throw TemplateSyntaxError("Template loader not in context")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
107
Stencil/TemplateLoader/Inheritence.swift
Normal file
107
Stencil/TemplateLoader/Inheritence.swift
Normal file
@@ -0,0 +1,107 @@
|
||||
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("\"")
|
||||
|
||||
guard bits.count == 3 else {
|
||||
throw TemplateSyntaxError("'extends' takes one argument, the template file to be extended")
|
||||
}
|
||||
|
||||
let parsedNodes = try parser.parse()
|
||||
guard (any(parsedNodes) { $0 is ExtendsNode }) == nil else {
|
||||
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 {
|
||||
guard let loader = context["loader"] as? TemplateLoader else {
|
||||
throw TemplateSyntaxError("Template loader not in context")
|
||||
}
|
||||
|
||||
guard let template = loader.loadTemplate(templateName) else {
|
||||
let paths:String = loader.paths.map { $0.description }.joinWithSeparator(", ")
|
||||
throw TemplateSyntaxError("'\(templateName)' template not found in \(paths)")
|
||||
}
|
||||
|
||||
let blockContext = BlockContext(blocks: blocks)
|
||||
context.push([BlockContext.contextKey: blockContext])
|
||||
let result = try template.render(context)
|
||||
context.pop()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -2,64 +2,64 @@ import Foundation
|
||||
|
||||
|
||||
public enum Token : Equatable {
|
||||
/// A token representing a piece of text.
|
||||
case Text(value:String)
|
||||
/// A token representing a piece of text.
|
||||
case Text(value:String)
|
||||
|
||||
/// A token representing a variable.
|
||||
case Variable(value:String)
|
||||
/// A token representing a variable.
|
||||
case Variable(value:String)
|
||||
|
||||
/// A token representing a comment.
|
||||
case Comment(value:String)
|
||||
/// A token representing a comment.
|
||||
case Comment(value:String)
|
||||
|
||||
/// A token representing a template block.
|
||||
case Block(value:String)
|
||||
/// A token representing a template block.
|
||||
case Block(value:String)
|
||||
|
||||
/// Returns the underlying value as an array seperated by spaces
|
||||
func components() -> [String] {
|
||||
// TODO: Make this smarter and treat quoted strings as a single component
|
||||
let characterSet = NSCharacterSet.whitespaceAndNewlineCharacterSet()
|
||||
|
||||
func strip(value: String) -> [String] {
|
||||
return value.stringByTrimmingCharactersInSet(characterSet).componentsSeparatedByCharactersInSet(characterSet)
|
||||
}
|
||||
/// Returns the underlying value as an array seperated by spaces
|
||||
public func components() -> [String] {
|
||||
// TODO: Make this smarter and treat quoted strings as a single component
|
||||
let characterSet = NSCharacterSet.whitespaceAndNewlineCharacterSet()
|
||||
|
||||
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)
|
||||
}
|
||||
func strip(value: String) -> [String] {
|
||||
return value.stringByTrimmingCharactersInSet(characterSet).componentsSeparatedByCharactersInSet(characterSet)
|
||||
}
|
||||
|
||||
var contents:String {
|
||||
switch self {
|
||||
case .Block(let value):
|
||||
return value
|
||||
case .Variable(let value):
|
||||
return value
|
||||
case .Text(let value):
|
||||
return value
|
||||
case .Comment(let value):
|
||||
return value
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
public var contents:String {
|
||||
switch self {
|
||||
case .Block(let value):
|
||||
return value
|
||||
case .Variable(let value):
|
||||
return value
|
||||
case .Text(let value):
|
||||
return value
|
||||
case .Comment(let value):
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func ==(lhs:Token, rhs:Token) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.Text(let lhsValue), .Text(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.Variable(let lhsValue), .Variable(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.Block(let lhsValue), .Block(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.Comment(let lhsValue), .Comment(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
default:
|
||||
return false
|
||||
}
|
||||
switch (lhs, rhs) {
|
||||
case (.Text(let lhsValue), .Text(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.Variable(let lhsValue), .Variable(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.Block(let lhsValue), .Block(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.Comment(let lhsValue), .Comment(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,98 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
class FilterExpression : Resolvable {
|
||||
let filters: [Filter]
|
||||
let variable: Variable
|
||||
|
||||
init(token: String, parser: TokenParser) throws {
|
||||
let bits = token.componentsSeparatedByString("|")
|
||||
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 {
|
||||
public let variable:String
|
||||
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
|
||||
/// 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) 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()]
|
||||
}
|
||||
|
||||
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: Any] {
|
||||
current = dictionary[bit]
|
||||
} else if let dictionary = current as? [String: AnyObject] {
|
||||
current = dictionary[bit]
|
||||
} else if let array = current as? [Any] {
|
||||
if let index = Int(bit) {
|
||||
current = array[index]
|
||||
} else if bit == "first" {
|
||||
current = array.first
|
||||
} else if bit == "last" {
|
||||
current = array.last
|
||||
} else if bit == "count" {
|
||||
current = array.count
|
||||
}
|
||||
|
||||
for bit in lookup() {
|
||||
if let context = current as? Context {
|
||||
current = context[bit]
|
||||
} else if let dictionary = current as? Dictionary<String, AnyObject> {
|
||||
current = dictionary[bit]
|
||||
} else if let array = current as? [AnyObject] {
|
||||
if let index = bit.toInt() {
|
||||
current = array[index]
|
||||
} else if bit == "first" {
|
||||
current = array.first
|
||||
} else if bit == "last" {
|
||||
current = array.last
|
||||
} else if bit == "count" {
|
||||
current = countElements(array)
|
||||
}
|
||||
} else if let object = current as? NSObject {
|
||||
current = object.valueForKey(bit)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else if let array = current as? NSArray {
|
||||
if let index = Int(bit) {
|
||||
current = array[index]
|
||||
} else if bit == "first" {
|
||||
current = array.firstObject
|
||||
} else if bit == "last" {
|
||||
current = array.lastObject
|
||||
} else if bit == "count" {
|
||||
current = array.count
|
||||
}
|
||||
|
||||
return current
|
||||
} 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
|
||||
return lhs.variable == rhs.variable
|
||||
}
|
||||
|
||||
65
StencilSpecs/ContextSpec.swift
Normal file
65
StencilSpecs/ContextSpec.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
describe("Context") {
|
||||
var context: Context!
|
||||
|
||||
$0.before {
|
||||
context = Context(dictionary: ["name": "Kyle"])
|
||||
}
|
||||
|
||||
$0.it("allows you to get a value via subscripting") {
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to set a value via subscripting") {
|
||||
context["name"] = "Katie"
|
||||
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
|
||||
$0.it("allows you to remove a value via subscripting") {
|
||||
context["name"] = nil
|
||||
|
||||
try expect(context["name"]).to.beNil()
|
||||
}
|
||||
|
||||
$0.it("allows you to retrieve a value from a parent") {
|
||||
context.push()
|
||||
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to override a parent's value") {
|
||||
context.push()
|
||||
context["name"] = "Katie"
|
||||
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
|
||||
$0.it("allows you to pop to restore previous state") {
|
||||
context.push()
|
||||
context["name"] = "Katie"
|
||||
context.pop()
|
||||
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to push a dictionary onto the stack") {
|
||||
context.push(["name": "Katie"])
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
|
||||
$0.it("allows you to push a dictionary and run a closure then restoring previous state") {
|
||||
var didRun = false
|
||||
|
||||
try context.push(["name": "Katie"]) {
|
||||
didRun = true
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
|
||||
try expect(didRun).to.beTrue()
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
}
|
||||
60
StencilSpecs/FilterSpec.swift
Normal file
60
StencilSpecs/FilterSpec.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
describe("template filters") {
|
||||
let context = Context(dictionary: ["name": "Kyle"])
|
||||
|
||||
$0.it("allows you to register a custom filter") {
|
||||
let template = Template(templateString: "{{ name|repeat }}")
|
||||
template.parser.registerFilter("repeat") { value in
|
||||
if let value = value as? String {
|
||||
return "\(value) \(value)"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
let result = try template.render(context)
|
||||
try expect(result) == "Kyle Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to register a custom filter") {
|
||||
let template = Template(templateString: "{{ name|repeat }}")
|
||||
template.parser.registerFilter("repeat") { value in
|
||||
throw TemplateSyntaxError("No Repeat")
|
||||
}
|
||||
|
||||
try expect(try template.render(context)).toThrow(TemplateSyntaxError("No Repeat"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe("capitalize filter") {
|
||||
let template = Template(templateString: "{{ name|capitalize }}")
|
||||
|
||||
$0.it("capitalizes a string") {
|
||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe("uppercase filter") {
|
||||
let template = Template(templateString: "{{ name|uppercase }}")
|
||||
|
||||
$0.it("transforms a string to be uppercase") {
|
||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
||||
try expect(result) == "KYLE"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe("lowercase filter") {
|
||||
let template = Template(templateString: "{{ name|lowercase }}")
|
||||
|
||||
$0.it("transforms a string to be lowercase") {
|
||||
let result = try template.render(Context(dictionary: ["name": "Kyle"]))
|
||||
try expect(result) == "kyle"
|
||||
}
|
||||
}
|
||||
48
StencilSpecs/LexerSpec.swift
Normal file
48
StencilSpecs/LexerSpec.swift
Normal file
@@ -0,0 +1,48 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
describe("Lexer") {
|
||||
$0.it("can tokenize text") {
|
||||
let lexer = Lexer(templateString: "Hello World")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == Token.Text(value: "Hello World")
|
||||
}
|
||||
|
||||
$0.it("can tokenize a comment") {
|
||||
let lexer = Lexer(templateString: "{# Comment #}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == (1)
|
||||
try expect(tokens.first) == Token.Comment(value: "Comment")
|
||||
}
|
||||
|
||||
$0.it("can tokenize a variable") {
|
||||
let lexer = Lexer(templateString: "{{ Variable }}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == Token.Variable(value: "Variable")
|
||||
}
|
||||
|
||||
$0.it("can tokenize a mixture of content") {
|
||||
let lexer = Lexer(templateString: "My name is {{ name }}.")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 3
|
||||
try expect(tokens[0]) == Token.Text(value: "My name is ")
|
||||
try expect(tokens[1]) == Token.Variable(value: "name")
|
||||
try expect(tokens[2]) == Token.Text(value: ".")
|
||||
}
|
||||
|
||||
$0.it("can tokenize two variables without being greedy") {
|
||||
let lexer = Lexer(templateString: "{{ thing }}{{ name }}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 2
|
||||
try expect(tokens[0]) == Token.Variable(value: "thing")
|
||||
try expect(tokens[1]) == Token.Variable(value: "name")
|
||||
}
|
||||
}
|
||||
23
StencilSpecs/Nodes/ForNodeSpec.swift
Normal file
23
StencilSpecs/Nodes/ForNodeSpec.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
describe("ForNode") {
|
||||
let context = Context(dictionary: [
|
||||
"items": [1, 2, 3],
|
||||
"emptyItems": [Int](),
|
||||
])
|
||||
|
||||
$0.it("renders the given nodes for each item") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "123"
|
||||
}
|
||||
|
||||
$0.it("renders the given empty nodes when no items found item") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||
let node = ForNode(variable: "emptyItems", loopVariable: "item", nodes: nodes, emptyNodes: emptyNodes)
|
||||
try expect(try node.render(context)) == "empty"
|
||||
}
|
||||
}
|
||||
100
StencilSpecs/Nodes/IfNodeSpec.swift
Normal file
100
StencilSpecs/Nodes/IfNodeSpec.swift
Normal file
@@ -0,0 +1,100 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
describe("IfNode") {
|
||||
$0.describe("parsing") {
|
||||
$0.it("can parse an if block") {
|
||||
let tokens = [
|
||||
Token.Block(value: "if value"),
|
||||
Token.Text(value: "true"),
|
||||
Token.Block(value: "else"),
|
||||
Token.Text(value: "false"),
|
||||
Token.Block(value: "endif")
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
let trueNode = node?.trueNodes.first as? TextNode
|
||||
let falseNode = node?.falseNodes.first as? TextNode
|
||||
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.variable.variable) == "value"
|
||||
try expect(node?.trueNodes.count) == 1
|
||||
try expect(trueNode?.text) == "true"
|
||||
try expect(node?.falseNodes.count) == 1
|
||||
try expect(falseNode?.text) == "false"
|
||||
}
|
||||
|
||||
$0.it("can parse an ifnot block") {
|
||||
let tokens = [
|
||||
Token.Block(value: "ifnot value"),
|
||||
Token.Text(value: "false"),
|
||||
Token.Block(value: "else"),
|
||||
Token.Text(value: "true"),
|
||||
Token.Block(value: "endif")
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
let trueNode = node?.trueNodes.first as? TextNode
|
||||
let falseNode = node?.falseNodes.first as? TextNode
|
||||
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.variable.variable) == "value"
|
||||
try expect(node?.trueNodes.count) == 1
|
||||
try expect(trueNode?.text) == "true"
|
||||
try expect(node?.falseNodes.count) == 1
|
||||
try expect(falseNode?.text) == "false"
|
||||
}
|
||||
|
||||
$0.it("throws an error when parsing an if block without an endif") {
|
||||
let tokens = [
|
||||
Token.Block(value: "if value"),
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
let error = TemplateSyntaxError("`endif` was not found.")
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
|
||||
$0.it("throws an error when parsing an ifnot without an endif") {
|
||||
let tokens = [
|
||||
Token.Block(value: "ifnot value"),
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
let error = TemplateSyntaxError("`endif` was not found.")
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("rendering") {
|
||||
let context = Context(dictionary: ["items": true])
|
||||
|
||||
$0.it("renders the truth when expression evaluates to true") {
|
||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
try expect(try node.render(context)) == "true"
|
||||
}
|
||||
|
||||
$0.it("renders the false when expression evaluates to false") {
|
||||
let node = IfNode(variable: "unknown", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
try expect(try node.render(context)) == "false"
|
||||
}
|
||||
|
||||
$0.it("renders the truth when array expression is not empty") {
|
||||
let items: Array<[String:AnyObject]> = [["key":"key1","value":42],["key":"key2","value":1337]]
|
||||
let arrayContext = Context(dictionary: ["items": [items]])
|
||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
try expect(try node.render(arrayContext)) == "true"
|
||||
}
|
||||
|
||||
$0.it("renders the false when array expression is empty") {
|
||||
let emptyItems = Array<[String:AnyObject]>()
|
||||
let arrayContext = Context(dictionary: ["items": emptyItems])
|
||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
try expect(try node.render(arrayContext)) == "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
58
StencilSpecs/Nodes/NodeSpec.swift
Normal file
58
StencilSpecs/Nodes/NodeSpec.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
class ErrorNode : NodeType {
|
||||
func render(context: Context) throws -> String {
|
||||
throw TemplateSyntaxError("Custom Error")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe("Node") {
|
||||
let context = Context(dictionary: [
|
||||
"name": "Kyle",
|
||||
"age": 27,
|
||||
"items": [1, 2, 3],
|
||||
])
|
||||
|
||||
$0.describe("TextNode") {
|
||||
$0.it("renders the given text") {
|
||||
let node = TextNode(text: "Hello World")
|
||||
try expect(try node.render(context)) == "Hello World"
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("VariableNode") {
|
||||
$0.it("resolves and renders the variable") {
|
||||
let node = VariableNode(variable: Variable("name"))
|
||||
try expect(try node.render(context)) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("resolves and renders a non string variable") {
|
||||
let node = VariableNode(variable: Variable("age"))
|
||||
try expect(try node.render(context)) == "27"
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("rendering nodes") {
|
||||
$0.it("renders the nodes") {
|
||||
let nodes: [NodeType] = [
|
||||
TextNode(text:"Hello "),
|
||||
VariableNode(variable: "name"),
|
||||
]
|
||||
|
||||
try expect(try renderNodes(nodes, context)) == "Hello Kyle"
|
||||
}
|
||||
|
||||
$0.it("correctly throws a nodes failure") {
|
||||
let nodes: [NodeType] = [
|
||||
TextNode(text:"Hello "),
|
||||
VariableNode(variable: "name"),
|
||||
ErrorNode(),
|
||||
]
|
||||
|
||||
try expect(try renderNodes(nodes, context)).toThrow(TemplateSyntaxError("Custom Error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
39
StencilSpecs/Nodes/NowNodeSpec.swift
Normal file
39
StencilSpecs/Nodes/NowNodeSpec.swift
Normal file
@@ -0,0 +1,39 @@
|
||||
import Foundation
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
describe("NowNode") {
|
||||
$0.describe("parsing") {
|
||||
$0.it("parses default format without any now arguments") {
|
||||
let tokens = [ Token.Block(value: "now") ]
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? NowNode
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.format.variable) == "\"yyyy-MM-dd 'at' HH:mm\""
|
||||
}
|
||||
|
||||
$0.it("parses now with a format") {
|
||||
let tokens = [ Token.Block(value: "now \"HH:mm\"") ]
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? NowNode
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.format.variable) == "\"HH:mm\""
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("rendering") {
|
||||
$0.it("renders the date") {
|
||||
let node = NowNode(format: Variable("\"yyyy-MM-dd\""))
|
||||
|
||||
let formatter = NSDateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
let date = formatter.stringFromDate(NSDate())
|
||||
|
||||
try expect(try node.render(Context())) == date
|
||||
}
|
||||
}
|
||||
}
|
||||
55
StencilSpecs/ParserSpec.swift
Normal file
55
StencilSpecs/ParserSpec.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
describe("TokenParser") {
|
||||
$0.it("can parse a text token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Text(value: "Hello World")
|
||||
])
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? TextNode
|
||||
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.text) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can parse a variable token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Variable(value: "'name'")
|
||||
])
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? VariableNode
|
||||
try expect(nodes.count) == 1
|
||||
let result = try node?.render(Context())
|
||||
try expect(result) == "name"
|
||||
}
|
||||
|
||||
$0.it("can parse a comment token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Comment(value: "Secret stuff!")
|
||||
])
|
||||
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.count) == 0
|
||||
}
|
||||
|
||||
$0.it("can parse a tag token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Block(value: "now"),
|
||||
])
|
||||
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.count) == 1
|
||||
}
|
||||
|
||||
$0.it("errors when parsing an unknown tag") {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Block(value: "unknown"),
|
||||
])
|
||||
|
||||
try expect(try parser.parse()).toThrow(TemplateSyntaxError("Unknown template tag 'unknown'"))
|
||||
}
|
||||
}
|
||||
61
StencilSpecs/StencilSpec.swift
Normal file
61
StencilSpecs/StencilSpec.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
class CustomNode : NodeType {
|
||||
func render(context:Context) throws -> String {
|
||||
return "Hello World"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe("Stencil") {
|
||||
$0.it("can render the README example") {
|
||||
let templateString = "There are {{ articles.count }} articles.\n" +
|
||||
"\n" +
|
||||
"{% for article in articles %}" +
|
||||
" - {{ article.title }} by {{ article.author }}.\n" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let context = Context(dictionary: [
|
||||
"articles": [
|
||||
[ "title": "Migrating from OCUnit to XCTest", "author": "Kyle Fuller" ],
|
||||
[ "title": "Memory Management with ARC", "author": "Kyle Fuller" ],
|
||||
]
|
||||
])
|
||||
|
||||
let template = Template(templateString:templateString)
|
||||
let result = try template.render(context)
|
||||
|
||||
let fixture = "There are 2 articles.\n" +
|
||||
"\n" +
|
||||
" - Migrating from OCUnit to XCTest by Kyle Fuller.\n" +
|
||||
" - Memory Management with ARC by Kyle Fuller.\n" +
|
||||
"\n"
|
||||
|
||||
try expect(result) == fixture
|
||||
}
|
||||
|
||||
$0.it("can render a custom template tag") {
|
||||
let templateString = "{% custom %}"
|
||||
let template = Template(templateString:templateString)
|
||||
|
||||
template.parser.registerTag("custom") { parser, token in
|
||||
return CustomNode()
|
||||
}
|
||||
|
||||
let result = try template.render()
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can render a simple custom tag") {
|
||||
let templateString = "{% custom %}"
|
||||
let template = Template(templateString:templateString)
|
||||
|
||||
template.parser.registerSimpleTag("custom") { context in
|
||||
return "Hello World"
|
||||
}
|
||||
|
||||
try expect(try template.render()) == "Hello World"
|
||||
}
|
||||
}
|
||||
58
StencilSpecs/TemplateLoader/IncludeSpec.swift
Normal file
58
StencilSpecs/TemplateLoader/IncludeSpec.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import PathKit
|
||||
|
||||
|
||||
describe("Include") {
|
||||
let path = Path(__FILE__) + ".." + ".." + "StencilSpecs" + "fixtures"
|
||||
let loader = TemplateLoader(paths: [path])
|
||||
|
||||
$0.describe("parsing") {
|
||||
$0.it("throws an error when no template is given") {
|
||||
let tokens = [ Token.Block(value: "include") ]
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
|
||||
let error = TemplateSyntaxError("'include' tag takes one argument, the template file to be included")
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
|
||||
$0.it("can parse a valid include block") {
|
||||
let tokens = [ Token.Block(value: "include \"test.html\"") ]
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IncludeNode
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.templateName) == "test.html"
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("rendering") {
|
||||
$0.it("throws an error when rendering without a loader") {
|
||||
let node = IncludeNode(templateName: "test.html")
|
||||
|
||||
do {
|
||||
try node.render(Context())
|
||||
} catch {
|
||||
try expect("\(error)") == "Template loader not in context"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("throws an error when it cannot find the included template") {
|
||||
let node = IncludeNode(templateName: "unknown.html")
|
||||
|
||||
do {
|
||||
try node.render(Context(dictionary: ["loader": loader]))
|
||||
} catch {
|
||||
try expect("\(error)".hasPrefix("'unknown.html' template not found")).to.beTrue()
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("successfully renders a found included template") {
|
||||
let node = IncludeNode(templateName: "test.html")
|
||||
let context = Context(dictionary: ["loader":loader, "target": "World"])
|
||||
let value = try node.render(context)
|
||||
try expect(value) == "Hello World!"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
StencilSpecs/TemplateLoader/InheritenceSpec.swift
Normal file
15
StencilSpecs/TemplateLoader/InheritenceSpec.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import PathKit
|
||||
|
||||
|
||||
describe("Inheritence") {
|
||||
let path = Path(__FILE__) + ".." + ".." + "StencilSpecs" + "fixtures"
|
||||
let loader = TemplateLoader(paths: [path])
|
||||
|
||||
$0.it("can inherit from another template") {
|
||||
let context = Context(dictionary: ["loader": loader])
|
||||
let template = loader.loadTemplate("child.html")
|
||||
try expect(try template?.render(context)) == "Header\nChild"
|
||||
}
|
||||
}
|
||||
23
StencilSpecs/TemplateLoaderSpec.swift
Normal file
23
StencilSpecs/TemplateLoaderSpec.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import PathKit
|
||||
|
||||
|
||||
describe("TemplateLoader") {
|
||||
let path = Path(__FILE__) + ".." + ".." + "StencilSpecs" + "fixtures"
|
||||
let loader = TemplateLoader(paths: [path])
|
||||
|
||||
$0.it("returns nil when a template cannot be found") {
|
||||
try expect(loader.loadTemplate("unknown.html")).to.beNil()
|
||||
}
|
||||
|
||||
$0.it("returns nil when an array of templates cannot be found") {
|
||||
try expect(loader.loadTemplate(["unknown.html", "unknown2.html"])).to.beNil()
|
||||
}
|
||||
|
||||
$0.it("can load a template from a file") {
|
||||
if loader.loadTemplate("test.html") == nil {
|
||||
throw failure("didn't find the template")
|
||||
}
|
||||
}
|
||||
}
|
||||
12
StencilSpecs/TemplateSpec.swift
Normal file
12
StencilSpecs/TemplateSpec.swift
Normal file
@@ -0,0 +1,12 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
describe("Template") {
|
||||
$0.it("can render a template from a string") {
|
||||
let context = Context(dictionary: [ "name": "Kyle" ])
|
||||
let template = Template(templateString: "Hello World")
|
||||
let result = try template.render(context)
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
}
|
||||
68
StencilSpecs/VariableSpec.swift
Normal file
68
StencilSpecs/VariableSpec.swift
Normal file
@@ -0,0 +1,68 @@
|
||||
import Foundation
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
@objc class Object : NSObject {
|
||||
let title = "Hello World"
|
||||
}
|
||||
|
||||
|
||||
describe("Variable") {
|
||||
let context = Context(dictionary: [
|
||||
"name": "Kyle",
|
||||
"contacts": ["Katie", "Carlton"],
|
||||
"profiles": [
|
||||
"github": "kylef",
|
||||
],
|
||||
"object": Object(),
|
||||
])
|
||||
|
||||
$0.it("can resolve a string literal with double quotes") {
|
||||
let variable = Variable("\"name\"")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "name"
|
||||
}
|
||||
|
||||
$0.it("can resolve a string literal with single quotes") {
|
||||
let variable = Variable("'name'")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "name"
|
||||
}
|
||||
|
||||
$0.it("can resolve a string variable") {
|
||||
let variable = Variable("name")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("can resolve an item from a dictionary") {
|
||||
let variable = Variable("profiles.github")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "kylef"
|
||||
}
|
||||
|
||||
$0.it("can resolve an item from an array via it's index") {
|
||||
let variable = Variable("contacts.0")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Katie"
|
||||
}
|
||||
|
||||
$0.it("can resolve the first item from an array") {
|
||||
let variable = Variable("contacts.first")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Katie"
|
||||
}
|
||||
|
||||
$0.it("can resolve the last item from an array") {
|
||||
let variable = Variable("contacts.last")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Carlton"
|
||||
}
|
||||
|
||||
$0.it("can resolve a value via KVO") {
|
||||
let variable = Variable("object.title")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
}
|
||||
2
StencilSpecs/fixtures/base.html
Normal file
2
StencilSpecs/fixtures/base.html
Normal file
@@ -0,0 +1,2 @@
|
||||
{% block header %}Header{% endblock %}
|
||||
{% block body %}Body{% endblock %}
|
||||
2
StencilSpecs/fixtures/child.html
Normal file
2
StencilSpecs/fixtures/child.html
Normal file
@@ -0,0 +1,2 @@
|
||||
{% extends "base.html" %}
|
||||
{% block body %}Child{% endblock %}
|
||||
1
StencilSpecs/fixtures/test.html
Normal file
1
StencilSpecs/fixtures/test.html
Normal file
@@ -0,0 +1 @@
|
||||
Hello {{ target }}!
|
||||
@@ -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 )
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.cocode.$(PRODUCT_NAME:rfc1034identifier)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
import Stencil
|
||||
|
||||
class ErrorNodeError : Error {
|
||||
var description: String {
|
||||
return "Node Error"
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorNode : Node {
|
||||
func render(context: Context) -> Result {
|
||||
|
||||
return .Error(ErrorNodeError())
|
||||
}
|
||||
}
|
||||
|
||||
class NodeTests: XCTestCase {
|
||||
var context:Context!
|
||||
|
||||
override func setUp() {
|
||||
context = Context(dictionary: [
|
||||
"name": "Kyle",
|
||||
"age": 27,
|
||||
"items": [1,2,3],
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
class TextNodeTests: NodeTests {
|
||||
func testTextNodeResolvesText() {
|
||||
let node = TextNode(text:"Hello World")
|
||||
let result = node.render(context)
|
||||
|
||||
switch node.render(context) {
|
||||
case .Success(let string):
|
||||
XCTAssertEqual(string, "Hello World")
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class VariableNodeTests: NodeTests {
|
||||
func testVariableNodeResolvesVariable() {
|
||||
let node = VariableNode(variable:Variable("name"))
|
||||
let result = node.render(context)
|
||||
|
||||
switch node.render(context) {
|
||||
case .Success(let string):
|
||||
XCTAssertEqual(string, "Kyle")
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
func testVariableNodeResolvesNonStringVariable() {
|
||||
let node = VariableNode(variable:Variable("age"))
|
||||
let result = node.render(context)
|
||||
|
||||
switch node.render(context) {
|
||||
case .Success(let string):
|
||||
XCTAssertEqual(string, "27")
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RenderNodeTests: NodeTests {
|
||||
func testRenderingNodes() {
|
||||
let nodes = [TextNode(text:"Hello "), VariableNode(variable: "name")] as [Node]
|
||||
switch renderNodes(nodes, context) {
|
||||
case .Success(let result):
|
||||
XCTAssertEqual(result, "Hello Kyle")
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
func testRenderingNodesWithFailure() {
|
||||
let nodes = [TextNode(text:"Hello "), VariableNode(variable: "name"), ErrorNode()] as [Node]
|
||||
|
||||
switch renderNodes(nodes, context) {
|
||||
case .Success(let result):
|
||||
XCTAssert(false, "Unexpected success")
|
||||
case .Error(let error):
|
||||
XCTAssertEqual("\(error)", "Node Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ForNodeTests: NodeTests {
|
||||
func testForNodeRender() {
|
||||
let node = ForNode(variable: "items", loopVariable: "item", nodes: [VariableNode(variable: "item")], emptyNodes:[])
|
||||
let result = node.render(context)
|
||||
|
||||
switch node.render(context) {
|
||||
case .Success(let string):
|
||||
XCTAssertEqual(string, "123")
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IfNodeTests: NodeTests {
|
||||
|
||||
// MARK: Parsing
|
||||
|
||||
func testParseIf() {
|
||||
let tokens = [
|
||||
Token.Block(value: "if value"),
|
||||
Token.Text(value: "true"),
|
||||
Token.Block(value: "else"),
|
||||
Token.Text(value: "false"),
|
||||
Token.Block(value: "endif")
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
assertSuccess(parser.parse()) { nodes in
|
||||
let node = nodes.first! as IfNode
|
||||
let trueNode = node.trueNodes.first! as TextNode
|
||||
let falseNode = node.falseNodes.first! as TextNode
|
||||
|
||||
XCTAssertEqual(nodes.count, 1)
|
||||
XCTAssertEqual(node.variable.variable, "value")
|
||||
XCTAssertEqual(node.trueNodes.count, 1)
|
||||
XCTAssertEqual(trueNode.text, "true")
|
||||
XCTAssertEqual(node.falseNodes.count, 1)
|
||||
XCTAssertEqual(falseNode.text, "false")
|
||||
}
|
||||
}
|
||||
|
||||
func testParseIfNot() {
|
||||
let tokens = [
|
||||
Token.Block(value: "ifnot value"),
|
||||
Token.Text(value: "false"),
|
||||
Token.Block(value: "else"),
|
||||
Token.Text(value: "true"),
|
||||
Token.Block(value: "endif")
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
assertSuccess(parser.parse()) { nodes in
|
||||
let node = nodes.first! as IfNode
|
||||
let trueNode = node.trueNodes.first! as TextNode
|
||||
let falseNode = node.falseNodes.first! as TextNode
|
||||
|
||||
XCTAssertEqual(nodes.count, 1)
|
||||
XCTAssertEqual(node.variable.variable, "value")
|
||||
XCTAssertEqual(node.trueNodes.count, 1)
|
||||
XCTAssertEqual(trueNode.text, "true")
|
||||
XCTAssertEqual(node.falseNodes.count, 1)
|
||||
XCTAssertEqual(falseNode.text, "false")
|
||||
}
|
||||
}
|
||||
|
||||
func testParseIfWithoutEndIfError() {
|
||||
let tokens = [
|
||||
Token.Block(value: "if value"),
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
assertFailure(parser.parse(), "if: `endif` was not found.")
|
||||
}
|
||||
|
||||
func testParseIfNotWithoutEndIfError() {
|
||||
let tokens = [
|
||||
Token.Block(value: "ifnot value"),
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
assertFailure(parser.parse(), "ifnot: `endif` was not found.")
|
||||
}
|
||||
|
||||
// MARK: Rendering
|
||||
|
||||
func testIfNodeRenderTruth() {
|
||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
let result = node.render(context)
|
||||
|
||||
switch node.render(context) {
|
||||
case .Success(let string):
|
||||
XCTAssertEqual(string, "true")
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
func testIfNodeRenderFalse() {
|
||||
let node = IfNode(variable: "unknown", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
let result = node.render(context)
|
||||
|
||||
switch node.render(context) {
|
||||
case .Success(let string):
|
||||
XCTAssertEqual(string, "false")
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class NowNodeTests: NodeTests {
|
||||
|
||||
// MARK: Parsing
|
||||
|
||||
func testParseDefaultNow() {
|
||||
let tokens = [ Token.Block(value: "now") ]
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
|
||||
assertSuccess(parser.parse()) { nodes in
|
||||
let node = nodes.first! as NowNode
|
||||
XCTAssertEqual(nodes.count, 1)
|
||||
XCTAssertEqual(node.format.variable, "\"yyyy-MM-dd 'at' HH:mm\"")
|
||||
}
|
||||
}
|
||||
|
||||
func testParseNowWithFormat() {
|
||||
let tokens = [ Token.Block(value: "now \"HH:mm\"") ]
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
|
||||
assertSuccess(parser.parse()) { nodes in
|
||||
let node = nodes.first! as NowNode
|
||||
XCTAssertEqual(nodes.count, 1)
|
||||
XCTAssertEqual(node.format.variable, "\"HH:mm\"")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Rendering
|
||||
|
||||
func testRenderNowNode() {
|
||||
let node = NowNode(format: Variable("\"yyyy-MM-dd\""))
|
||||
let result = node.render(context)
|
||||
|
||||
let formatter = NSDateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
let date = formatter.stringFromDate(NSDate())
|
||||
|
||||
switch node.render(context) {
|
||||
case .Success(let string):
|
||||
XCTAssertEqual(string, date)
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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(parser.parse()) { nodes in
|
||||
let node = nodes.first as TextNode!
|
||||
XCTAssertEqual(nodes.count, 1)
|
||||
XCTAssertEqual(node.text, "Hello World")
|
||||
}
|
||||
}
|
||||
|
||||
func testParsingVariableToken() {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Variable(value: "name")
|
||||
])
|
||||
|
||||
assertSuccess(parser.parse()) { nodes in
|
||||
let node = nodes.first as VariableNode!
|
||||
XCTAssertEqual(nodes.count, 1)
|
||||
XCTAssertEqual(node.variable, Variable("name"))
|
||||
}
|
||||
}
|
||||
|
||||
func testParsingCommentToken() {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Comment(value: "Secret stuff!")
|
||||
])
|
||||
|
||||
assertSuccess(parser.parse()) { nodes in
|
||||
XCTAssertEqual(nodes.count, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func testParsingTagToken() {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Block(value: "now"),
|
||||
])
|
||||
|
||||
assertSuccess(parser.parse()) { nodes in
|
||||
XCTAssertEqual(nodes.count, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
import Stencil
|
||||
|
||||
func assertSuccess(result:TokenParser.Results, block:(([Node]) -> ())) {
|
||||
switch result {
|
||||
case .Success(let nodes):
|
||||
block(nodes)
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
func assertFailure(result:TokenParser.Results, description:String) {
|
||||
switch result {
|
||||
case .Success(let nodes):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
case .Error(let error):
|
||||
XCTAssertEqual("\(error)", description)
|
||||
}
|
||||
}
|
||||
|
||||
class CustomNode : Node {
|
||||
func render(context:Context) -> Result {
|
||||
return .Success("Hello World")
|
||||
}
|
||||
}
|
||||
|
||||
class StencilTests: XCTestCase {
|
||||
func testReadmeExample() {
|
||||
let templateString = "There are {{ articles.count }} articles.\n" +
|
||||
"\n" +
|
||||
"{% for article in articles %}" +
|
||||
" - {{ article.title }} by {{ article.author }}.\n" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let context = Context(dictionary: [
|
||||
"articles": [
|
||||
[ "title": "Migrating from OCUnit to XCTest", "author": "Kyle Fuller" ],
|
||||
[ "title": "Memory Management with ARC", "author": "Kyle Fuller" ],
|
||||
]
|
||||
])
|
||||
|
||||
let template = Template(templateString:templateString)
|
||||
let result = template.render(context)
|
||||
|
||||
let fixture = "There are 2 articles.\n" +
|
||||
"\n" +
|
||||
" - Migrating from OCUnit to XCTest by Kyle Fuller.\n" +
|
||||
" - Memory Management with ARC by Kyle Fuller.\n" +
|
||||
"\n"
|
||||
|
||||
XCTAssertEqual(result, Result.Success(fixture))
|
||||
}
|
||||
|
||||
func testCustomTag() {
|
||||
let templateString = "{% custom %}"
|
||||
let template = Template(templateString:templateString)
|
||||
|
||||
template.parser.registerTag("custom") { parser, token in
|
||||
return .Success(node:CustomNode())
|
||||
}
|
||||
|
||||
let result = template.render()
|
||||
XCTAssertEqual(result, Result.Success("Hello World"))
|
||||
}
|
||||
|
||||
func testSimpleCustomTag() {
|
||||
let templateString = "{% custom %}"
|
||||
let template = Template(templateString:templateString)
|
||||
|
||||
template.parser.registerSimpleTag("custom") { context in
|
||||
return .Success("Hello World")
|
||||
}
|
||||
|
||||
let result = template.render()
|
||||
XCTAssertEqual(result, Result.Success("Hello World"))
|
||||
}
|
||||
}
|
||||
@@ -1,14 +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 = template.render(context)
|
||||
XCTAssertEqual(result, Result.Success("Hello World"))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
21
circle.yml
Normal file
21
circle.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
machine:
|
||||
xcode:
|
||||
version: "7.0"
|
||||
environment:
|
||||
XCODE_SCHEME: NONE
|
||||
XCODE_PROJECT: NONE
|
||||
|
||||
dependencies:
|
||||
post:
|
||||
- brew install --HEAD kylef/formulae/conche
|
||||
|
||||
test:
|
||||
override:
|
||||
- conche test
|
||||
|
||||
deployment:
|
||||
release:
|
||||
tag: /.*/
|
||||
commands:
|
||||
- pod trunk push
|
||||
|
||||
Reference in New Issue
Block a user