56 Commits
0.5.3 ... 0.7.0

Author SHA1 Message Date
Kyle Fuller
98edad3566 chore: Release 0.7.0 2016-11-29 12:26:22 +00:00
Kyle Fuller
872784f9b3 chore(podspec): Update homepage 2016-11-29 12:24:31 +00:00
Kyle Fuller
1a01ec592e chore(README): Update documentation links 2016-11-28 19:48:43 +00:00
Kyle Fuller
f0591408be refactor(filter): Filters should be private, we only expose resolvable 2016-11-28 19:24:27 +00:00
Kyle Fuller
b7e200a8a0 feat(for): Support filters
Closes #70
2016-11-28 19:24:27 +00:00
Kyle Fuller
b1da85b140 feat(default filter): Support multiple defaults 2016-11-28 19:24:27 +00:00
Kyle Fuller
679344f53b refactor(if): Deprecate ifnot tag 2016-11-28 19:24:27 +00:00
Kyle Fuller
ada4e81082 fix(changelog): Public APIs 2016-11-28 19:24:27 +00:00
Kyle Fuller
c99a40c5d9 refactor(loader): Rename TemplateLoader to Loader 2016-11-28 19:24:27 +00:00
Kyle Fuller
c59b263446 feat(if): Support >, >=, < and <= operators
Closes #52
2016-11-28 19:24:26 +00:00
Kyle Fuller
ab6f1a032d feat(if): Support inequality operator 2016-11-28 19:24:26 +00:00
Kyle Fuller
e989317929 feat(if): Support equatable operator 2016-11-28 19:24:26 +00:00
Kyle Fuller
111306fb60 fix(if): Empty strings should be falsy 2016-11-28 19:24:26 +00:00
Kyle Fuller
3eb2657a62 fix(if): Support resolving all number types 0 or below as false 2016-11-28 19:24:26 +00:00
Kyle Fuller
6ad609e562 refactor(if): Move expressions to separate file 2016-11-28 19:24:26 +00:00
Kyle Fuller
38d7ec87f6 feat(variable): Allow Swift type introspection 2016-11-28 19:24:26 +00:00
Kyle Fuller
9af9cf4005 fix(context): Allow removing a value at a pushed state 2016-11-28 19:24:25 +00:00
Kyle Fuller
1975cfd627 feat(context): Add flatten method 2016-11-28 19:24:25 +00:00
Kyle Fuller
429290e0b7 refactor: TemplateLoader to protocol, follow Swift API guidelines 2016-11-28 19:24:17 +00:00
Kyle Fuller
5ca1b78854 chore: Switch to Swift 3.0.1 2016-11-28 03:37:20 +00:00
Kyle Fuller
a2673bd66b chore: Update README to point to documentation 2016-11-28 03:15:56 +00:00
Kyle Fuller
9b6ee14aa3 feat(docs): Add custom tags and filters to documentation 2016-11-28 03:11:38 +00:00
Kyle Fuller
3b5e8f2468 chore: Remove debug print during test 2016-11-28 03:00:51 +00:00
Kyle Fuller
e84f8a41d4 feat(if): Support and, or and not during if expressions
Closes #73
2016-11-28 02:56:04 +00:00
Kyle Fuller
2324808dca fix(if): Treat below 0 numbers as negative 2016-11-27 20:11:51 +00:00
Kyle Fuller
9fdbbc99e9 feat(filters): Add a join filter 2016-11-27 19:47:50 +00:00
Kyle Fuller
dfd57e9571 feat: Add a default filter 2016-11-27 18:27:30 +00:00
Kyle Fuller
3293d8a526 feat: Add documentation 2016-11-27 18:03:39 +00:00
Kyle Fuller
393dc88a10 fix(extends): Support multiple extends
Fixes #60
2016-11-27 04:22:37 +00:00
Kyle Fuller
a014fecd23 fix(filters): Smartly split arguments
Ensure that `"value"|filter:"arg , with comma"` works
2016-11-27 02:46:43 +00:00
Kyle Fuller
a13401b046 chore: Lock down APIs 2016-11-27 02:20:46 +00:00
Kyle Fuller
60b378d482 feat(filters): Allow filters with arguments 2016-11-27 01:59:57 +00:00
Kyle Fuller
1e3afc0dd5 fix(variable): Prevent crash on unknown index in array 2016-10-13 13:11:02 +01:00
Kyle Fuller
72f3cb579a Switch to Swift 3.0 2016-10-13 13:04:49 +01:00
Valentin Knabel
68e6ce3022 feat: Creating of templates from a string literal (#71) 2016-10-11 12:45:15 +01:00
Kyle Fuller
65c3052aee Release 0.6.0 2016-09-13 19:15:52 +01:00
Kyle Fuller
7bbd4f2817 [Pod] Add support for tvos 2016-09-13 19:12:47 +01:00
Kyle Fuller
7416e6150d [Travis CI] Test on Linux 2016-09-13 19:12:47 +01:00
Kyle Fuller
feff3b18b1 Add support for Swift 3.0 2016-09-13 19:12:47 +01:00
Kyle Fuller
f393efbd0b Merge pull request #68 from zhangbozhb/master
bug fix:fix memory leak when parse template
2016-09-04 16:17:28 +02:00
Kyle Fuller
df650c6b20 [Travis CI] Test on Swift 2.2 2016-09-04 09:26:43 +02:00
travel
3285bac373 bug fix:fix memory leak when parse template 2016-08-05 08:26:16 +08:00
Kyle Fuller
80427a51e6 Merge pull request #67 from ikesyo/ifnode-evaluate-bool-value
[IfNode] Accept and evaluate a `Bool` value as a valid expression
2016-07-19 19:15:26 +01:00
Syo Ikeda
7bfb69cc82 [IfNode] Fix the ifnot error message 2016-07-20 02:42:09 +09:00
Syo Ikeda
5007ba2c9a [IfNode] Accept and evaluate a Bool value as a valid expression 2016-07-20 02:42:09 +09:00
Syo Ikeda
2d73c58df6 [IfNodeSpec] Add a failing test for bool expression 2016-07-20 02:42:09 +09:00
Kyle Fuller
4ffc888ba4 Release 0.6.0-beta.1 2016-04-04 22:39:19 +02:00
Kyle Fuller
3c21975b97 Merge pull request #61 from GregKaleka/patch-1
Minor grammatical fixes to README.md
2016-03-16 13:53:25 +00:00
Greg Kaleka
df9065f5a8 Minor grammatical fixes to README.md 2016-03-13 21:30:48 -07:00
Kyle Fuller
05b71736aa Merge pull request #59 from shnhrrsn/namespace-fix
Added namespace to Context
2016-03-09 23:41:43 +00:00
shnhrrsn
aa1399be55 Fixed tests for namespace changes 2016-03-06 00:39:10 -05:00
shnhrrsn
bdc14ab1e1 Added namespace to Context 2016-03-05 23:57:15 -05:00
Kyle Fuller
67d4c52535 [Context] Ensure pop happens when an error is thrown 2016-03-05 00:37:12 +00:00
Kyle Fuller
48026cde2c Split tags into separate files 2016-03-05 00:15:18 +00:00
Kyle Fuller
dc4b965aaa Merge pull request #55 from dtrenz/dtrenz-readme-usage
Added `try` to throwing expressions in example.
2016-02-27 15:46:05 -05:00
Dan Trenz
2190afee0d Added try to throwing expressions in example.
Both `Template(named: "template.stencil")` and `Template(named: "template.stencil")` throw but were not preceded by `try`. This usage example, in it's current form, triggers compiler errors.
2016-02-27 15:20:55 -05:00
58 changed files with 2897 additions and 908 deletions

View File

@@ -1 +1 @@
DEVELOPMENT-SNAPSHOT-2016-01-25-a
3.0.1

View File

@@ -1,11 +1,11 @@
os:
- osx
- linux
- osx
- linux
language: generic
sudo: required
dist: trusty
osx_image: xcode7.2
osx_image: xcode8
install:
- eval "$(curl -sL https://gist.githubusercontent.com/kylef/5c0475ff02b7c7671d2a/raw/02090c7ede5a637b76e6df1710e83cd0bbe7dcdf/swiftenv-install.sh)"
- eval "$(curl -sL https://gist.githubusercontent.com/kylef/5c0475ff02b7c7671d2a/raw/9f442512a46d7a2af7b850d65a7e9bd31edfb09b/swiftenv-install.sh)"
script:
- make test
- swift test

67
CHANGELOG.md Normal file
View File

@@ -0,0 +1,67 @@
# Stencil Changelog
## 0.7.0
### Breaking
- `TemplateLoader` has been renamed to `FileSystemLoader`. The
`loadTemplate(s)` methods are now throwing and now take labels for the `name`
and `names` arguments.
- Many internal classes are no longer public. Some APIs were previously
accessible due to earlier versions of Swift requiring the types to be public
to be able to test. Now we have access to `@testable` these can correctly be
private.
- `{% ifnot %}` tag is now deprecated, please use `{% if not %}` instead.
### Enhancements
- Variable lookup now supports introspection of Swift types. You can now lookup
values of Swift structures and classes inside a Context.
- If tags can now use prefix and infix operators such as `not`, `and`, `or`,
`==`, `!=`, `>`, `>=`, `<` and `<=`.
```html+django
{% if one or two and not three %}
```
- You may now register custom template filters which make use of arguments.
- There is now a `default` filter.
```html+django
Hello {{ name|default:"World" }}
```
- There is now a `join` filter.
```html+django
{{ value|join:", " }}
```
- `{% for %}` tag now supports filters.
```html+django
{% for user in non_admins|default:admins %}
{{ user }}
{% endfor %}
```
### Bug Fixes
- Variables (`{{ variable.5 }}`) that reference an array index at an unknown
index will now resolve to `nil` instead of causing a crash.
[#72](https://github.com/kylef/Stencil/issues/72)
- Templates can now extend templates that extend other templates.
[#60](https://github.com/kylef/Stencil/issues/60)
- If comparisons will now treat 0 and below numbers as negative.
## 0.6.0
### Enhancements
- Adds support for Swift 3.0.

View File

@@ -1,7 +0,0 @@
stencil:
@echo "Building Stencil"
@swift build
test: stencil
@echo "Running Tests"
@.build/debug/spectre-build

View File

@@ -3,9 +3,9 @@ import PackageDescription
let package = Package(
name: "Stencil",
dependencies: [
.Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 6),
],
testDependencies: [
.Package(url: "https://github.com/kylef/spectre-build", majorVersion: 0),
.Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 7),
// https://github.com/apple/swift-package-manager/pull/597
.Package(url: "https://github.com/kylef/Spectre", majorVersion: 0, minor: 7),
]
)

213
README.md
View File

@@ -11,22 +11,29 @@ feel right at home with Stencil.
```html+django
There are {{ articles.count }} articles.
{% for article in articles %}
- {{ article.title }} by {{ article.author }}.
{% endfor %}
<ul>
{% for article in articles %}
<li>{{ article.title }} by {{ article.author }}</li>
{% endfor %}
</ul>
```
```swift
struct Article {
let title: String
let author: String
}
let context = Context(dictionary: [
"articles": [
[ "title": "Migrating from OCUnit to XCTest", "author": "Kyle Fuller" ],
[ "title": "Memory Management with ARC", "author": "Kyle Fuller" ],
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
]
])
do {
let template = Template(named: "template.stencil")
let rendered = template.render(context)
let template = try Template(named: "template.html")
let rendered = try template.render(context)
print(rendered)
} catch {
print("Failed to render template \(error)")
@@ -35,7 +42,9 @@ do {
## Installation
Installation with CocoaPods is recommended.
Installation with Swift Package Manager is recommended.
### CocoaPods
```ruby
pod 'Stencil'
@@ -51,191 +60,11 @@ Stencil follows the same philosophy of Django:
> design: the template system is meant to express presentation, not program
> logic.
## Templates
## The User Guide
### Variables
A variable can be defined in your template using the following:
```html+django
{{ variable }}
```
Stencil will look up the variable inside the current variable context and
evaluate it. When a variable contains a dot, it will try doing the
following lookup:
- Context lookup
- Dictionary lookup
- Array lookup (first, last, count, index)
- Key value coding lookup
For example, if `people` was an array:
```html+django
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 }}
```
### Tags
Tags are a mechanism to execute a piece of code, allowing you to have
control flow within your template.
```html+django
{% if variable %}
{{ variable }} was found.
{% endif %}
```
A tag can also affect the context and define variables as follows:
```html+django
{% for item in items %}
{{ item }}
{% endfor %}
```
Stencil has a couple of built-in tags which are listed below. You can also
extend Stencil by providing your own tags.
#### for
A for loop allows you to iterate over an array found by variable lookup.
```html+django
{% for item in items %}
{{ item }}
{% empty %}
There we're no items.
{% endfor %}
```
#### if
```html+django
{% if variable %}
The variable was found in the current context.
{% else %}
The variable was not found.
{% endif %}
```
#### ifnot
```html+django
{% ifnot variable %}
The variable was NOT found in the current context.
{% else %}
The variable was found.
{% endif %}
```
#### include
You can include another template using the `include` tag.
```html+django
{% include "comment.html" %}
```
The `include` tag requires a TemplateLoader to be found inside your context with the paths, or bundles used to lookup the template.
```swift
let context = Context(dictionary: [
"loader": TemplateLoader(bundle:[NSBundle.mainBundle()])
])
```
### Customisation
You can build your own custom filters and tags and pass them down while
rendering your template. Any custom filters or tags must be registered
with a namespace which contains all filters and tags available to the template.
```swift
let namespace = Namespace()
// Register your filters and tags with the namespace
let rendered = try template.render(context, namespace: namespace)
```
#### Registering custom filters
```swift
namespace.registerFilter("double") { value in
if let value = value as? Int {
return value * 2
}
return value
}
```
#### Building custom tags
You can build a custom template tag. There are a couple of APIs to allow
you to write your own custom tags. The following is the simplest form:
```swift
namespace.registerSimpleTag("custom") { context in
return "Hello World"
}
```
When your tag is used via `{% custom %}` it will execute the registered block
of code allowing you to modify or retrieve a value from the context. Then
return either a string rendered in your template, or 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`.
The architecture of Stencil along with how to build advanced plugins can be
found in the [architecture](ARCHITECTURE.md) document.
### Comments
To comment out part of your template, you can use the following syntax:
```html+django
{# My comment is completely hidden #}
```
- [Templates](http://stencil.fuller.li/en/latest/templates.html)
- [Built-in template tags and filters](http://stencil.fuller.li/en/latest/builtins.html)
- [Custom Template Tags and Filters](http://stencil.fuller.li/en/latest/custom-template-tags-and-filters.html)
## License

View File

@@ -1,21 +1,23 @@
/// A container for template variables.
public class Context {
var dictionaries:[[String: Any]]
var dictionaries: [[String: Any?]]
let namespace: Namespace
/// Initialise a Context with a dictionary
public init(dictionary:[String: Any]) {
dictionaries = [dictionary]
}
/// Initialise a Context with an optional dictionary and optional namespace
public init(dictionary: [String: Any]? = nil, namespace: Namespace = Namespace()) {
if let dictionary = dictionary {
dictionaries = [dictionary]
} else {
dictionaries = []
}
/// Initialise an empty Context
public init() {
dictionaries = []
self.namespace = namespace
}
public subscript(key: String) -> Any? {
/// Retrieves a variable's value, starting at the current context and going upwards
get {
for dictionary in Array(dictionaries.reverse()) {
for dictionary in Array(dictionaries.reversed()) {
if let value = dictionary[key] {
return value
}
@@ -35,20 +37,33 @@ public class Context {
}
/// Push a new level into the Context
public func push(dictionary: [String: Any]? = nil) {
fileprivate func push(_ dictionary: [String: Any]? = nil) {
dictionaries.append(dictionary ?? [:])
}
/// Pop the last level off of the Context
public func pop() -> [String: Any]? {
fileprivate 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 {
public func push<Result>(dictionary: [String: Any]? = nil, closure: (() throws -> Result)) rethrows -> Result {
push(dictionary)
let result = try closure()
pop()
return result
defer { _ = pop() }
return try closure()
}
public func flatten() -> [String: Any] {
var accumulator: [String: Any] = [:]
for dictionary in dictionaries {
for (key, value) in dictionary {
if let value = value {
accumulator.updateValue(value, forKey: key)
}
}
}
return accumulator
}
}

300
Sources/Expression.swift Normal file
View File

@@ -0,0 +1,300 @@
protocol Expression: CustomStringConvertible {
func evaluate(context: Context) throws -> Bool
}
protocol InfixOperator: Expression {
init(lhs: Expression, rhs: Expression)
}
protocol PrefixOperator: Expression {
init(expression: Expression)
}
final class StaticExpression: Expression, CustomStringConvertible {
let value: Bool
init(value: Bool) {
self.value = value
}
func evaluate(context: Context) throws -> Bool {
return value
}
var description: String {
return "\(value)"
}
}
final class VariableExpression: Expression, CustomStringConvertible {
let variable: Variable
init(variable: Variable) {
self.variable = variable
}
var description: String {
return "(variable: \(variable.variable))"
}
/// Resolves a variable in the given context as boolean
func resolve(context: Context, variable: Variable) throws -> Bool {
let result = try variable.resolve(context)
var truthy = false
if let result = result as? [Any] {
truthy = !result.isEmpty
} else if let result = result as? [String:Any] {
truthy = !result.isEmpty
} else if let result = result as? Bool {
truthy = result
} else if let result = result as? String {
truthy = !result.isEmpty
} else if let value = result, let result = toNumber(value: value) {
truthy = result > 0
} else if result != nil {
truthy = true
}
return truthy
}
func evaluate(context: Context) throws -> Bool {
return try resolve(context: context, variable: variable)
}
}
final class NotExpression: Expression, PrefixOperator, CustomStringConvertible {
let expression: Expression
init(expression: Expression) {
self.expression = expression
}
var description: String {
return "not \(expression)"
}
func evaluate(context: Context) throws -> Bool {
return try !expression.evaluate(context: context)
}
}
final class OrExpression: Expression, InfixOperator, CustomStringConvertible {
let lhs: Expression
let rhs: Expression
init(lhs: Expression, rhs: Expression) {
self.lhs = lhs
self.rhs = rhs
}
var description: String {
return "(\(lhs) or \(rhs))"
}
func evaluate(context: Context) throws -> Bool {
let lhs = try self.lhs.evaluate(context: context)
if lhs {
return lhs
}
return try rhs.evaluate(context: context)
}
}
final class AndExpression: Expression, InfixOperator, CustomStringConvertible {
let lhs: Expression
let rhs: Expression
init(lhs: Expression, rhs: Expression) {
self.lhs = lhs
self.rhs = rhs
}
var description: String {
return "(\(lhs) and \(rhs))"
}
func evaluate(context: Context) throws -> Bool {
let lhs = try self.lhs.evaluate(context: context)
if !lhs {
return lhs
}
return try rhs.evaluate(context: context)
}
}
class EqualityExpression: Expression, InfixOperator, CustomStringConvertible {
let lhs: Expression
let rhs: Expression
required init(lhs: Expression, rhs: Expression) {
self.lhs = lhs
self.rhs = rhs
}
var description: String {
return "(\(lhs) == \(rhs))"
}
func evaluate(context: Context) throws -> Bool {
if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression {
let lhsValue = try lhs.variable.resolve(context)
let rhsValue = try rhs.variable.resolve(context)
if let lhs = lhsValue, let rhs = rhsValue {
if let lhs = toNumber(value: lhs), let rhs = toNumber(value: rhs) {
return lhs == rhs
} else if let lhs = lhsValue as? String, let rhs = rhsValue as? String {
return lhs == rhs
} else if let lhs = lhsValue as? Bool, let rhs = rhsValue as? Bool {
return lhs == rhs
}
} else if lhsValue == nil && rhsValue == nil {
return true
}
}
return false
}
}
class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
let lhs: Expression
let rhs: Expression
required init(lhs: Expression, rhs: Expression) {
self.lhs = lhs
self.rhs = rhs
}
var description: String {
return "(\(lhs) \(op) \(rhs))"
}
func evaluate(context: Context) throws -> Bool {
if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression {
let lhsValue = try lhs.variable.resolve(context)
let rhsValue = try rhs.variable.resolve(context)
if let lhs = lhsValue, let rhs = rhsValue {
if let lhs = toNumber(value: lhs), let rhs = toNumber(value: rhs) {
return compare(lhs: lhs, rhs: rhs)
}
}
}
return false
}
var op: String {
return ""
}
func compare(lhs: Float80, rhs: Float80) -> Bool {
return false
}
}
class MoreThanExpression: NumericExpression {
override var op: String {
return ">"
}
override func compare(lhs: Float80, rhs: Float80) -> Bool {
return lhs > rhs
}
}
class MoreThanEqualExpression: NumericExpression {
override var op: String {
return ">="
}
override func compare(lhs: Float80, rhs: Float80) -> Bool {
return lhs >= rhs
}
}
class LessThanExpression: NumericExpression {
override var op: String {
return "<"
}
override func compare(lhs: Float80, rhs: Float80) -> Bool {
return lhs < rhs
}
}
class LessThanEqualExpression: NumericExpression {
override var op: String {
return "<="
}
override func compare(lhs: Float80, rhs: Float80) -> Bool {
return lhs <= rhs
}
}
class InequalityExpression: EqualityExpression {
override var description: String {
return "(\(lhs) != \(rhs))"
}
override func evaluate(context: Context) throws -> Bool {
return try !super.evaluate(context: context)
}
}
func toNumber(value: Any) -> Float80? {
if let value = value as? Float {
return Float80(value)
} else if let value = value as? Double {
return Float80(value)
} else if let value = value as? UInt {
return Float80(value)
} else if let value = value as? Int {
return Float80(value)
} else if let value = value as? Int8 {
return Float80(value)
} else if let value = value as? Int16 {
return Float80(value)
} else if let value = value as? Int32 {
return Float80(value)
} else if let value = value as? Int64 {
return Float80(value)
} else if let value = value as? UInt8 {
return Float80(value)
} else if let value = value as? UInt16 {
return Float80(value)
} else if let value = value as? UInt32 {
return Float80(value)
} else if let value = value as? UInt64 {
return Float80(value)
} else if let value = value as? Float80 {
return value
} else if let value = value as? Float64 {
return Float80(value)
} else if let value = value as? Float32 {
return Float80(value)
}
return nil
}

View File

@@ -1,4 +1,4 @@
func toString(value: Any?) -> String? {
func toString(_ value: Any?) -> String? {
if let value = value as? String {
return value
} else if let value = value as? CustomStringConvertible {
@@ -8,26 +8,56 @@ func toString(value: Any?) -> String? {
return nil
}
func capitalise(value: Any?) -> Any? {
func capitalise(_ value: Any?) -> Any? {
if let value = toString(value) {
return value.capitalizedString
return value.capitalized
}
return value
}
func uppercase(value: Any?) -> Any? {
func uppercase(_ value: Any?) -> Any? {
if let value = toString(value) {
return value.uppercaseString
return value.uppercased()
}
return value
}
func lowercase(value: Any?) -> Any? {
func lowercase(_ value: Any?) -> Any? {
if let value = toString(value) {
return value.lowercaseString
return value.lowercased()
}
return value
}
func defaultFilter(value: Any?, arguments: [Any?]) -> Any? {
if let value = value {
return value
}
for argument in arguments {
if let argument = argument {
return argument
}
}
return nil
}
func joinFilter(value: Any?, arguments: [Any?]) throws -> Any? {
guard arguments.count == 1 else {
throw TemplateSyntaxError("'join' filter takes a single argument")
}
guard let separator = arguments.first as? String else {
throw TemplateSyntaxError("'join' filter takes a separator as string")
}
if let value = value as? [String] {
return value.joined(separator: separator)
}
return nil
}

63
Sources/ForTag.swift Normal file
View File

@@ -0,0 +1,63 @@
class ForNode : NodeType {
let resolvable: Resolvable
let loopVariable:String
let nodes:[NodeType]
let emptyNodes: [NodeType]
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
let components = token.components()
guard components.count == 4 && components[2] == "in" else {
throw TemplateSyntaxError("'for' statements should use the following 'for x in y' `\(token.contents)`.")
}
let loopVariable = components[1]
let variable = components[3]
var emptyNodes = [NodeType]()
let forNodes = try parser.parse(until(["endfor", "empty"]))
guard let token = parser.nextToken() else {
throw TemplateSyntaxError("`endfor` was not found.")
}
if token.contents == "empty" {
emptyNodes = try parser.parse(until(["endfor"]))
_ = parser.nextToken()
}
let filter = try parser.compileFilter(variable)
return ForNode(resolvable: filter, loopVariable: loopVariable, nodes: forNodes, emptyNodes:emptyNodes)
}
init(resolvable: Resolvable, loopVariable:String, nodes:[NodeType], emptyNodes:[NodeType]) {
self.resolvable = resolvable
self.loopVariable = loopVariable
self.nodes = nodes
self.emptyNodes = emptyNodes
}
func render(_ context: Context) throws -> String {
let values = try resolvable.resolve(context)
if let values = values as? [Any] , values.count > 0 {
let count = values.count
return try values.enumerated().map { index, item in
let forContext: [String: Any] = [
"first": index == 0,
"last": index == (count - 1),
"counter": index + 1,
]
return try context.push(dictionary: [loopVariable: item, "forloop": forContext]) {
try renderNodes(nodes, context)
}
}.joined(separator: "")
}
return try context.push {
try renderNodes(emptyNodes, context)
}
}
}

233
Sources/IfTag.swift Normal file
View File

@@ -0,0 +1,233 @@
enum Operator {
case infix(String, Int, InfixOperator.Type)
case prefix(String, Int, PrefixOperator.Type)
var name: String {
switch self {
case .infix(let name, _, _):
return name
case .prefix(let name, _, _):
return name
}
}
}
let operators: [Operator] = [
.infix("or", 6, OrExpression.self),
.infix("and", 7, AndExpression.self),
.prefix("not", 8, NotExpression.self),
.infix("==", 10, EqualityExpression.self),
.infix("!=", 10, InequalityExpression.self),
.infix(">", 10, MoreThanExpression.self),
.infix(">=", 10, MoreThanEqualExpression.self),
.infix("<", 10, LessThanExpression.self),
.infix("<=", 10, LessThanEqualExpression.self),
]
func findOperator(name: String) -> Operator? {
for op in operators {
if op.name == name {
return op
}
}
return nil
}
enum IfToken {
case infix(name: String, bindingPower: Int, op: InfixOperator.Type)
case prefix(name: String, bindingPower: Int, op: PrefixOperator.Type)
case variable(Variable)
case end
var bindingPower: Int {
switch self {
case .infix(_, let bindingPower, _):
return bindingPower
case .prefix(_, let bindingPower, _):
return bindingPower
case .variable(_):
return 0
case .end:
return 0
}
}
func nullDenotation(parser: IfExpressionParser) throws -> Expression {
switch self {
case .infix(let name, _, _):
throw TemplateSyntaxError("'if' expression error: infix operator '\(name)' doesn't have a left hand side")
case .prefix(_, let bindingPower, let op):
let expression = try parser.expression(bindingPower: bindingPower)
return op.init(expression: expression)
case .variable(let variable):
return VariableExpression(variable: variable)
case .end:
throw TemplateSyntaxError("'if' expression error: end")
}
}
func leftDenotation(left: Expression, parser: IfExpressionParser) throws -> Expression {
switch self {
case .infix(_, let bindingPower, let op):
let right = try parser.expression(bindingPower: bindingPower)
return op.init(lhs: left, rhs: right)
case .prefix(let name, _, _):
throw TemplateSyntaxError("'if' expression error: prefix operator '\(name)' was called with a left hand side")
case .variable(let variable):
throw TemplateSyntaxError("'if' expression error: variable '\(variable)' was called with a left hand side")
case .end:
throw TemplateSyntaxError("'if' expression error: end")
}
}
var isEnd: Bool {
switch self {
case .end:
return true
default:
return false
}
}
}
final class IfExpressionParser {
let tokens: [IfToken]
var position: Int = 0
init(components: [String]) {
self.tokens = components.map { component in
if let op = findOperator(name: component) {
switch op {
case .infix(let name, let bindingPower, let cls):
return .infix(name: name, bindingPower: bindingPower, op: cls)
case .prefix(let name, let bindingPower, let cls):
return .prefix(name: name, bindingPower: bindingPower, op: cls)
}
}
return .variable(Variable(component))
}
}
var currentToken: IfToken {
if tokens.count > position {
return tokens[position]
}
return .end
}
var nextToken: IfToken {
position += 1
return currentToken
}
func parse() throws -> Expression {
let expression = try self.expression()
if !currentToken.isEnd {
throw TemplateSyntaxError("'if' expression error: dangling token")
}
return expression
}
func expression(bindingPower: Int = 0) throws -> Expression {
var token = currentToken
position += 1
var left = try token.nullDenotation(parser: self)
while bindingPower < currentToken.bindingPower {
token = currentToken
position += 1
left = try token.leftDenotation(left: left, parser: self)
}
return left
}
}
func parseExpression(components: [String]) throws -> Expression {
let parser = IfExpressionParser(components: components)
return try parser.parse()
}
class IfNode : NodeType {
let expression: Expression
let trueNodes: [NodeType]
let falseNodes: [NodeType]
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
var components = token.components()
guard components.count == 2 else {
throw TemplateSyntaxError("'if' statements should use the following 'if condition' `\(token.contents)`.")
}
components.removeFirst()
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()
}
let expression = try parseExpression(components: components)
return IfNode(expression: expression, trueNodes: trueNodes, falseNodes: falseNodes)
}
class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType {
var components = token.components()
guard components.count == 2 else {
throw TemplateSyntaxError("'ifnot' statements should use the following 'ifnot condition' `\(token.contents)`.")
}
components.removeFirst()
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()
}
let expression = try parseExpression(components: components)
return IfNode(expression: expression, trueNodes: trueNodes, falseNodes: falseNodes)
}
init(expression: Expression, trueNodes: [NodeType], falseNodes: [NodeType]) {
self.expression = expression
self.trueNodes = trueNodes
self.falseNodes = falseNodes
}
func render(_ context: Context) throws -> String {
let truthy = try expression.evaluate(context: context)
return try context.push {
if truthy {
return try renderNodes(trueNodes, context)
} else {
return try renderNodes(falseNodes, context)
}
}
}
}

View File

@@ -1,10 +1,10 @@
import PathKit
public class IncludeNode : NodeType {
public let templateName: Variable
class IncludeNode : NodeType {
let templateName: Variable
public class func parse(parser: TokenParser, token: Token) throws -> NodeType {
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
let bits = token.components()
guard bits.count == 2 else {
@@ -14,12 +14,12 @@ public class IncludeNode : NodeType {
return IncludeNode(templateName: Variable(bits[1]))
}
public init(templateName: Variable) {
init(templateName: Variable) {
self.templateName = templateName
}
public func render(context: Context) throws -> String {
guard let loader = context["loader"] as? TemplateLoader else {
func render(_ context: Context) throws -> String {
guard let loader = context["loader"] as? Loader else {
throw TemplateSyntaxError("Template loader not in context")
}
@@ -27,9 +27,8 @@ public class IncludeNode : NodeType {
throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string")
}
guard let template = loader.loadTemplate(templateName) else {
let paths = loader.paths.map { $0.description }.joinWithSeparator(", ")
throw TemplateSyntaxError("'\(templateName)' template not found in \(paths)")
guard let template = try loader.loadTemplate(name: templateName) else {
throw TemplateSyntaxError("'\(templateName)' template not found")
}
return try template.render(context)

View File

@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2014 Cocode. All rights reserved.</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>

View File

@@ -1,20 +1,20 @@
class BlockContext {
class var contextKey: String { return "block_context" }
var blocks: [String:BlockNode]
var blocks: [String: BlockNode]
init(blocks: [String:BlockNode]) {
init(blocks: [String: BlockNode]) {
self.blocks = blocks
}
func pop(blockName: String) -> BlockNode? {
return blocks.removeValueForKey(blockName)
func pop(_ blockName: String) -> BlockNode? {
return blocks.removeValue(forKey: blockName)
}
}
extension CollectionType {
func any(closure: Generator.Element -> Bool) -> Generator.Element? {
extension Collection {
func any(_ closure: (Iterator.Element) -> Bool) -> Iterator.Element? {
for element in self {
if closure(element) {
return element
@@ -30,7 +30,7 @@ class ExtendsNode : NodeType {
let templateName: Variable
let blocks: [String:BlockNode]
class func parse(parser: TokenParser, token: Token) throws -> NodeType {
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
let bits = token.components()
guard bits.count == 2 else {
@@ -42,10 +42,9 @@ class ExtendsNode : NodeType {
throw TemplateSyntaxError("'extends' cannot appear more than once in the same template")
}
let blockNodes = parsedNodes.filter { node in node is BlockNode }
let blockNodes = parsedNodes.flatMap { $0 as? BlockNode }
let nodes = blockNodes.reduce([String:BlockNode]()) { (accumulator, node:NodeType) -> [String:BlockNode] in
let node = (node as! BlockNode)
let nodes = blockNodes.reduce([String: BlockNode]()) { (accumulator, node) -> [String: BlockNode] in
var dict = accumulator
dict[node.name] = node
return dict
@@ -59,8 +58,8 @@ class ExtendsNode : NodeType {
self.blocks = blocks
}
func render(context: Context) throws -> String {
guard let loader = context["loader"] as? TemplateLoader else {
func render(_ context: Context) throws -> String {
guard let loader = context["loader"] as? Loader else {
throw TemplateSyntaxError("Template loader not in context")
}
@@ -68,16 +67,26 @@ class ExtendsNode : NodeType {
throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string")
}
guard let template = loader.loadTemplate(templateName) else {
let paths:String = loader.paths.map { $0.description }.joinWithSeparator(", ")
throw TemplateSyntaxError("'\(templateName)' template not found in \(paths)")
guard let template = try loader.loadTemplate(name: templateName) else {
throw TemplateSyntaxError("'\(templateName)' template not found")
}
let blockContext = BlockContext(blocks: blocks)
context.push([BlockContext.contextKey: blockContext])
let result = try template.render(context)
context.pop()
return result
let blockContext: BlockContext
if let context = context[BlockContext.contextKey] as? BlockContext {
blockContext = context
for (key, value) in blocks {
if !blockContext.blocks.keys.contains(key) {
blockContext.blocks[key] = value
}
}
} else {
blockContext = BlockContext(blocks: blocks)
}
return try context.push(dictionary: [BlockContext.contextKey: blockContext]) {
return try template.render(context)
}
}
}
@@ -86,16 +95,16 @@ class BlockNode : NodeType {
let name: String
let nodes: [NodeType]
class func parse(parser: TokenParser, token: Token) throws -> 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")
throw TemplateSyntaxError("'block' tag takes one argument, the block name")
}
let blockName = bits[1]
let nodes = try parser.parse(until(["endblock"]))
parser.nextToken()
_ = parser.nextToken()
return BlockNode(name:blockName, nodes:nodes)
}
@@ -104,8 +113,8 @@ class BlockNode : NodeType {
self.nodes = nodes
}
func render(context: Context) throws -> String {
if let blockContext = context[BlockContext.contextKey] as? BlockContext, node = blockContext.pop(name) {
func render(_ context: Context) throws -> String {
if let blockContext = context[BlockContext.contextKey] as? BlockContext, let node = blockContext.pop(name) {
return try node.render(context)
}

View File

@@ -1,28 +1,30 @@
public struct Lexer {
public let templateString: String
struct Lexer {
let templateString: String
public init(templateString: String) {
init(templateString: String) {
self.templateString = templateString
}
func createToken(string:String) -> Token {
func strip() -> String {
return string[string.startIndex.successor().successor()..<string.endIndex.predecessor().predecessor()].trim(" ")
let start = string.index(string.startIndex, offsetBy: 2)
let end = string.index(string.endIndex, offsetBy: -2)
return string[start..<end].trim(character: " ")
}
if string.hasPrefix("{{") {
return Token.Variable(value: strip())
return .variable(value: strip())
} else if string.hasPrefix("{%") {
return Token.Block(value: strip())
return .block(value: strip())
} else if string.hasPrefix("{#") {
return Token.Comment(value: strip())
return .comment(value: strip())
}
return Token.Text(value: string)
return .text(value: string)
}
/// Returns an array of tokens from a given template string.
public func tokenize() -> [Token] {
func tokenize() -> [Token] {
var tokens: [Token] = []
let scanner = Scanner(templateString)
@@ -36,14 +38,14 @@ public struct Lexer {
while !scanner.isEmpty {
if let text = scanner.scan(until: ["{{", "{%", "{#"]) {
if !text.1.isEmpty {
tokens.append(createToken(text.1))
tokens.append(createToken(string: text.1))
}
let end = map[text.0]!
let result = scanner.scan(until: end, returnUntil: true)
tokens.append(createToken(result))
tokens.append(createToken(string: result))
} else {
tokens.append(createToken(scanner.content))
tokens.append(createToken(string: scanner.content))
scanner.content = ""
}
}
@@ -64,49 +66,50 @@ class Scanner {
return content.isEmpty
}
func scan(until until: String, returnUntil: Bool = false) -> String {
func scan(until: String, returnUntil: Bool = false) -> String {
if until.isEmpty {
return ""
}
var index = content.startIndex
while index != content.endIndex {
let substring = content[index..<content.endIndex]
let substring = content.substring(from: index)
if substring.hasPrefix(until) {
let result = content[content.startIndex..<index]
let result = content.substring(to: index)
content = substring
if returnUntil {
content = content[until.endIndex..<content.endIndex]
content = content.substring(from: until.endIndex)
return result + until
}
return result
}
index = index.successor()
index = content.index(after: index)
}
return ""
}
func scan(until until: [String]) -> (String, String)? {
func scan(until: [String]) -> (String, String)? {
if until.isEmpty {
return nil
}
var index = content.startIndex
while index != content.endIndex {
let substring = content[index..<content.endIndex]
let substring = content.substring(from: index)
for string in until {
if substring.hasPrefix(string) {
let result = content[content.startIndex..<index]
let result = content.substring(to: index)
content = substring
return (string, result)
}
}
index = index.successor()
index = content.index(after: index)
}
return nil
@@ -117,31 +120,33 @@ class Scanner {
extension String {
func findFirstNot(character: Character) -> String.Index? {
var index = startIndex
while index != endIndex {
if character != self[index] {
return index
}
index = index.successor()
index = self.index(after: index)
}
return nil
}
func findLastNot(character: Character) -> String.Index? {
var index = endIndex.predecessor()
var index = self.index(before: endIndex)
while index != startIndex {
if character != self[index] {
return index.successor()
return self.index(after: index)
}
index = index.predecessor()
index = self.index(before: index)
}
return nil
}
func trim(character: Character) -> String {
let first = findFirstNot(character) ?? startIndex
let last = findLastNot(character) ?? endIndex
let first = findFirstNot(character: character) ?? startIndex
let last = findLastNot(character: character) ?? endIndex
return self[first..<last]
}
}

View File

@@ -1,3 +1,26 @@
protocol FilterType {
func invoke(value: Any?, arguments: [Any?]) throws -> Any?
}
enum Filter: FilterType {
case simple(((Any?) throws -> Any?))
case arguments(((Any?, [Any?]) throws -> Any?))
func invoke(value: Any?, arguments: [Any?]) throws -> Any? {
switch self {
case let .simple(filter):
if !arguments.isEmpty {
throw TemplateSyntaxError("cannot invoke filter with an argument")
}
return try filter(value)
case let .arguments(filter):
return try filter(value, arguments)
}
}
}
public class Namespace {
public typealias TagParser = (TokenParser, Token) throws -> NodeType
@@ -9,7 +32,7 @@ public class Namespace {
registerDefaultFilters()
}
private func registerDefaultTags() {
fileprivate func registerDefaultTags() {
registerTag("for", parser: ForNode.parse)
registerTag("if", parser: IfNode.parse)
registerTag("ifnot", parser: IfNode.parse_ifnot)
@@ -21,26 +44,33 @@ public class Namespace {
registerTag("block", parser: BlockNode.parse)
}
private func registerDefaultFilters() {
fileprivate func registerDefaultFilters() {
registerFilter("default", filter: defaultFilter)
registerFilter("capitalize", filter: capitalise)
registerFilter("uppercase", filter: uppercase)
registerFilter("lowercase", filter: lowercase)
registerFilter("join", filter: joinFilter)
}
/// Registers a new template tag
public func registerTag(name: String, parser: TagParser) {
public func registerTag(_ name: String, parser: @escaping TagParser) {
tags[name] = parser
}
/// Registers a simple template tag with a name and a handler
public func registerSimpleTag(name: String, handler: Context throws -> String) {
public func registerSimpleTag(_ name: String, handler: @escaping (Context) throws -> String) {
registerTag(name, parser: { parser, token in
return SimpleNode(handler: handler)
})
}
/// Registers a template filter with the given name
public func registerFilter(name: String, filter: Filter) {
filters[name] = filter
public func registerFilter(_ name: String, filter: @escaping (Any?) throws -> Any?) {
filters[name] = .simple(filter)
}
/// Registers a template filter with the given name
public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?]) throws -> Any?) {
filters[name] = .arguments(filter)
}
}

View File

@@ -1,6 +1,7 @@
import Foundation
public struct TemplateSyntaxError : ErrorType, Equatable, CustomStringConvertible {
public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
public let description:String
public init(_ description:String) {
@@ -8,32 +9,36 @@ public struct TemplateSyntaxError : ErrorType, Equatable, CustomStringConvertibl
}
}
public func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool {
return lhs.description == rhs.description
}
public protocol NodeType {
/// Render the node in the given context
func render(context:Context) throws -> String
func render(_ context:Context) throws -> String
}
/// Render the collection of nodes in the given context
public func renderNodes(nodes:[NodeType], _ context:Context) throws -> String {
return try nodes.map { try $0.render(context) }.joinWithSeparator("")
public func renderNodes(_ nodes:[NodeType], _ context:Context) throws -> String {
return try nodes.map { try $0.render(context) }.joined(separator: "")
}
public class SimpleNode : NodeType {
let handler:Context throws -> String
public let handler:(Context) throws -> String
public init(handler:Context throws -> String) {
public init(handler: @escaping (Context) throws -> String) {
self.handler = handler
}
public func render(context: Context) throws -> String {
public func render(_ context: Context) throws -> String {
return try handler(context)
}
}
public class TextNode : NodeType {
public let text:String
@@ -41,15 +46,17 @@ public class TextNode : NodeType {
self.text = text
}
public func render(context:Context) throws -> String {
public func render(_ context:Context) throws -> String {
return self.text
}
}
public protocol Resolvable {
func resolve(context: Context) throws -> Any?
func resolve(_ context: Context) throws -> Any?
}
public class VariableNode : NodeType {
public let variable: Resolvable
@@ -61,7 +68,7 @@ public class VariableNode : NodeType {
self.variable = Variable(variable)
}
public func render(context: Context) throws -> String {
public func render(_ context: Context) throws -> String {
let result = try variable.resolve(context)
if let result = result as? String {
@@ -75,191 +82,3 @@ public class VariableNode : NodeType {
return ""
}
}
#if !os(Linux)
public class NowNode : NodeType {
public let format:Variable
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
var format:Variable?
let components = token.components()
guard components.count <= 2 else {
throw TemplateSyntaxError("'now' tags may only have one argument: the format string `\(token.contents)`.")
}
if components.count == 2 {
format = Variable(components[1])
}
return NowNode(format:format)
}
public init(format:Variable?) {
self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"")
}
public func render(context: Context) throws -> String {
let date = NSDate()
let format = try self.format.resolve(context)
var formatter:NSDateFormatter?
if let format = format as? NSDateFormatter {
formatter = format
} else if let format = format as? String {
formatter = NSDateFormatter()
formatter!.dateFormat = format
} else {
return ""
}
return formatter!.stringFromDate(date)
}
}
#endif
public class ForNode : NodeType {
let variable:Variable
let loopVariable:String
let nodes:[NodeType]
let emptyNodes: [NodeType]
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
let components = token.components()
guard components.count == 4 && components[2] == "in" else {
throw TemplateSyntaxError("'for' statements should use the following 'for x in y' `\(token.contents)`.")
}
let loopVariable = components[1]
let variable = components[3]
var emptyNodes = [NodeType]()
let forNodes = try parser.parse(until(["endfor", "empty"]))
guard let token = parser.nextToken() else {
throw TemplateSyntaxError("`endfor` was not found.")
}
if token.contents == "empty" {
emptyNodes = try parser.parse(until(["endfor"]))
parser.nextToken()
}
return ForNode(variable: variable, loopVariable: loopVariable, nodes: forNodes, emptyNodes:emptyNodes)
}
public init(variable:String, loopVariable:String, nodes:[NodeType], emptyNodes:[NodeType]) {
self.variable = Variable(variable)
self.loopVariable = loopVariable
self.nodes = nodes
self.emptyNodes = emptyNodes
}
public func render(context: Context) throws -> String {
let values = try variable.resolve(context)
if let values = values as? [Any] where values.count > 0 {
let count = values.count
return try values.enumerate().map { index, item in
let forContext: [String: Any] = [
"first": index == 0,
"last": index == (count - 1),
"counter": index + 1,
]
return try context.push([loopVariable: item, "forloop": forContext]) {
try renderNodes(nodes, context)
}
}.joinWithSeparator("")
}
return try context.push {
try renderNodes(emptyNodes, context)
}
}
}
public class IfNode : NodeType {
public let variable:Variable
public let trueNodes:[NodeType]
public let falseNodes:[NodeType]
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
let components = token.components()
guard components.count == 2 else {
throw TemplateSyntaxError("'if' statements should use the following 'if condition' `\(token.contents)`.")
}
let variable = components[1]
var trueNodes = [NodeType]()
var falseNodes = [NodeType]()
trueNodes = try parser.parse(until(["endif", "else"]))
guard let token = parser.nextToken() else {
throw TemplateSyntaxError("`endif` was not found.")
}
if token.contents == "else" {
falseNodes = try parser.parse(until(["endif"]))
parser.nextToken()
}
return IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)
}
public class func parse_ifnot(parser:TokenParser, token:Token) throws -> NodeType {
let components = token.components()
guard components.count == 2 else {
throw TemplateSyntaxError("'ifnot' statements should use the following 'if condition' `\(token.contents)`.")
}
let variable = components[1]
var trueNodes = [NodeType]()
var falseNodes = [NodeType]()
falseNodes = try parser.parse(until(["endif", "else"]))
guard let token = parser.nextToken() else {
throw TemplateSyntaxError("`endif` was not found.")
}
if token.contents == "else" {
trueNodes = try parser.parse(until(["endif"]))
parser.nextToken()
}
return IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)
}
public init(variable:String, trueNodes:[NodeType], falseNodes:[NodeType]) {
self.variable = Variable(variable)
self.trueNodes = trueNodes
self.falseNodes = falseNodes
}
public func render(context: Context) throws -> String {
let result = try variable.resolve(context)
var truthy = false
if let result = result as? [Any] {
truthy = !result.isEmpty
} else if let result = result as? [String:Any] {
truthy = !result.isEmpty
} else if result != nil {
truthy = true
}
context.push()
let output:String
if truthy {
output = try renderNodes(trueNodes, context)
} else {
output = try renderNodes(falseNodes, context)
}
context.pop()
return output
}
}

43
Sources/NowTag.swift Normal file
View File

@@ -0,0 +1,43 @@
#if !os(Linux)
import Foundation
class NowNode : NodeType {
let format:Variable
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
var format:Variable?
let components = token.components()
guard components.count <= 2 else {
throw TemplateSyntaxError("'now' tags may only have one argument: the format string `\(token.contents)`.")
}
if components.count == 2 {
format = Variable(components[1])
}
return NowNode(format:format)
}
init(format:Variable?) {
self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"")
}
func render(_ context: Context) throws -> String {
let date = Date()
let format = try self.format.resolve(context)
var formatter:DateFormatter?
if let format = format as? DateFormatter {
formatter = format
} else if let format = format as? String {
formatter = DateFormatter()
formatter!.dateFormat = format
} else {
return ""
}
return formatter!.string(from: date)
}
}
#endif

View File

@@ -1,4 +1,4 @@
public func until(tags: [String]) -> ((TokenParser, Token) -> Bool) {
public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) {
return { parser, token in
if let name = token.components().first {
for tag in tags {
@@ -12,14 +12,13 @@ public func until(tags: [String]) -> ((TokenParser, Token) -> Bool) {
}
}
public typealias Filter = Any? throws -> Any?
/// A class for parsing an array of tokens and converts them into a collection of Node's
public class TokenParser {
public typealias TagParser = (TokenParser, Token) throws -> NodeType
private var tokens: [Token]
private let namespace: Namespace
fileprivate var tokens: [Token]
fileprivate let namespace: Namespace
public init(tokens: [Token], namespace: Namespace) {
self.tokens = tokens
@@ -31,21 +30,21 @@ public class TokenParser {
return try parse(nil)
}
public func parse(parse_until:((parser:TokenParser, token:Token) -> (Bool))?) throws -> [NodeType] {
public func parse(_ parse_until:((_ parser:TokenParser, _ token:Token) -> (Bool))?) throws -> [NodeType] {
var nodes = [NodeType]()
while tokens.count > 0 {
let token = nextToken()!
switch token {
case .Text(let text):
case .text(let text):
nodes.append(TextNode(text: text))
case .Variable:
case .variable:
nodes.append(VariableNode(variable: try compileFilter(token.contents)))
case .Block:
case .block:
let tag = token.components().first
if let parse_until = parse_until where parse_until(parser: self, token: token) {
if let parse_until = parse_until , parse_until(self, token) {
prependToken(token)
return nodes
}
@@ -57,7 +56,7 @@ public class TokenParser {
throw TemplateSyntaxError("Unknown template tag '\(tag)'")
}
}
case .Comment:
case .comment:
continue
}
}
@@ -67,17 +66,17 @@ public class TokenParser {
public func nextToken() -> Token? {
if tokens.count > 0 {
return tokens.removeAtIndex(0)
return tokens.remove(at: 0)
}
return nil
}
public func prependToken(token:Token) {
tokens.insert(token, atIndex: 0)
public func prependToken(_ token:Token) {
tokens.insert(token, at: 0)
}
public func findFilter(name: String) throws -> Filter {
func findFilter(_ name: String) throws -> FilterType {
if let filter = namespace.filters[name] {
return filter
}
@@ -85,7 +84,7 @@ public class TokenParser {
throw TemplateSyntaxError("Invalid filter '\(name)'")
}
func compileFilter(token: String) throws -> Resolvable {
public func compileFilter(_ token: String) throws -> Resolvable {
return try FilterExpression(token: token, parser: self)
}
}

View File

@@ -6,13 +6,19 @@ let NSFileNoSuchFileError = 4
#endif
/// A class representing a template
public class Template {
public class Template: ExpressibleByStringLiteral {
let tokens: [Token]
/// Create a template with a template string
public init(templateString: String) {
let lexer = Lexer(templateString: templateString)
tokens = lexer.tokenize()
}
/// 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 {
public convenience init(named:String, inBundle bundle:Bundle? = nil) throws {
let useBundle = bundle ?? Bundle.main
guard let url = useBundle.url(forResource: named, withExtension: nil) else {
throw NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError, userInfo: nil)
}
@@ -20,25 +26,35 @@ public class Template {
}
/// Create a template with a file found at the given URL
public convenience init(URL:NSURL) throws {
try self.init(path: Path(URL.path!))
public convenience init(URL:Foundation.URL) throws {
try self.init(path: Path(URL.path))
}
/// Create a template with a file found at the given path
public convenience init(path:Path) throws {
public convenience init(path: Path) throws {
self.init(templateString: try path.read())
}
/// Create a template with a template string
public init(templateString:String) {
let lexer = Lexer(templateString: templateString)
tokens = lexer.tokenize()
// Create a template with a template string literal
public convenience required init(stringLiteral value: String) {
self.init(templateString: value)
}
// Create a template with a template string literal
public convenience required init(extendedGraphemeClusterLiteral value: StringLiteralType) {
self.init(stringLiteral: value)
}
// Create a template with a template string literal
public convenience required init(unicodeScalarLiteral value: StringLiteralType) {
self.init(stringLiteral: value)
}
/// Render the given template
public func render(context: Context? = nil, namespace: Namespace? = nil) throws -> String {
let parser = TokenParser(tokens: tokens, namespace: namespace ?? Namespace())
public func render(_ context: Context? = nil) throws -> String {
let context = context ?? Context()
let parser = TokenParser(tokens: tokens, namespace: context.namespace)
let nodes = try parser.parse()
return try renderNodes(nodes, context ?? Context())
return try renderNodes(nodes, context)
}
}

View File

@@ -2,33 +2,60 @@ import Foundation
import PathKit
public protocol Loader {
func loadTemplate(name: String) throws -> Template?
func loadTemplate(names: [String]) throws -> Template?
}
extension Loader {
func loadTemplate(names: [String]) throws -> Template? {
for name in names {
let template = try loadTemplate(name: name)
if template != nil {
return template
}
}
return nil
}
}
// A class for loading a template from disk
public class TemplateLoader {
public class FileSystemLoader: Loader {
public let paths: [Path]
public init(paths: [Path]) {
self.paths = paths
}
public init(bundle: [NSBundle]) {
public init(bundle: [Bundle]) {
self.paths = bundle.map {
return Path($0.bundlePath)
}
}
public func loadTemplate(templateName: String) -> Template? {
return loadTemplate([templateName])
public func loadTemplate(name: String) throws -> Template? {
for path in paths {
let templatePath = path + Path(name)
if templatePath.exists {
return try Template(path: templatePath)
}
}
return nil
}
public func loadTemplate(templateNames: [String]) -> Template? {
public func loadTemplate(names: [String]) throws -> Template? {
for path in paths {
for templateName in templateNames {
for templateName in names {
let templatePath = path + Path(templateName)
if templatePath.exists {
if let template = try? Template(path: templatePath) {
return template
}
return try Template(path: templatePath)
}
}
}

View File

@@ -1,76 +1,78 @@
import Foundation
/// Split a string by spaces leaving quoted phrases together
func smartSplit(value: String) -> [String] {
var word = ""
var separator: Character = " "
var components: [String] = []
extension String {
/// Split a string by a separator leaving quoted phrases together
func smartSplit(separator: Character = " ") -> [String] {
var word = ""
var components: [String] = []
var separate: Character = separator
for character in value.characters {
if character == separator {
if separator != " " {
word.append(separator)
}
for character in self.characters {
if character == separate {
if separate != separator {
word.append(separate)
}
if !word.isEmpty {
components.append(word)
word = ""
}
if !word.isEmpty {
components.append(word)
word = ""
}
separator = " "
} else {
if separator == " " && (character == "'" || character == "\"") {
separator = character
separate = separator
} else {
if separate == separator && (character == "'" || character == "\"") {
separate = character
}
word.append(character)
}
word.append(character)
}
}
if !word.isEmpty {
components.append(word)
}
if !word.isEmpty {
components.append(word)
}
return components
return components
}
}
public enum Token : Equatable {
/// A token representing a piece of text.
case Text(value: String)
case text(value: String)
/// A token representing a variable.
case Variable(value: String)
case variable(value: String)
/// A token representing a comment.
case Comment(value: String)
case comment(value: String)
/// A token representing a template block.
case Block(value: String)
case block(value: String)
/// Returns the underlying value as an array seperated by spaces
public func components() -> [String] {
switch self {
case .Block(let value):
return smartSplit(value)
case .Variable(let value):
return smartSplit(value)
case .Text(let value):
return smartSplit(value)
case .Comment(let value):
return smartSplit(value)
case .block(let value):
return value.smartSplit()
case .variable(let value):
return value.smartSplit()
case .text(let value):
return value.smartSplit()
case .comment(let value):
return value.smartSplit()
}
}
public var contents: String {
switch self {
case .Block(let value):
case .block(let value):
return value
case .Variable(let value):
case .variable(let value):
return value
case .Text(let value):
case .text(let value):
return value
case .Comment(let value):
case .comment(let value):
return value
}
}
@@ -79,13 +81,13 @@ public enum Token : Equatable {
public func == (lhs: Token, rhs: Token) -> Bool {
switch (lhs, rhs) {
case (.Text(let lhsValue), .Text(let rhsValue)):
case (.text(let lhsValue), .text(let rhsValue)):
return lhsValue == rhsValue
case (.Variable(let lhsValue), .Variable(let rhsValue)):
case (.variable(let lhsValue), .variable(let rhsValue)):
return lhsValue == rhsValue
case (.Block(let lhsValue), .Block(let rhsValue)):
case (.block(let lhsValue), .block(let rhsValue)):
return lhsValue == rhsValue
case (.Comment(let lhsValue), .Comment(let rhsValue)):
case (.comment(let lhsValue), .comment(let rhsValue)):
return lhsValue == rhsValue
default:
return false

View File

@@ -2,11 +2,11 @@ import Foundation
class FilterExpression : Resolvable {
let filters: [Filter]
let filters: [(FilterType, [Variable])]
let variable: Variable
init(token: String, parser: TokenParser) throws {
let bits = token.characters.split("|").map({ String($0).trim(" ") })
let bits = token.characters.split(separator: "|").map({ String($0).trim(character: " ") })
if bits.isEmpty {
filters = []
variable = Variable("")
@@ -14,21 +14,26 @@ class FilterExpression : Resolvable {
}
variable = Variable(bits[0])
let filterBits = bits[1 ..< bits.endIndex]
let filterBits = bits[bits.indices.suffix(from: 1)]
do {
filters = try filterBits.map { try parser.findFilter($0) }
filters = try filterBits.map {
let (name, arguments) = parseFilterComponents(token: $0)
let filter = try parser.findFilter(name)
return (filter, arguments)
}
} catch {
filters = []
throw error
}
}
func resolve(context: Context) throws -> Any? {
func resolve(_ context: Context) throws -> Any? {
let result = try variable.resolve(context)
return try filters.reduce(result) { x, y in
return try y(x)
let arguments = try y.1.map { try $0.resolve(context) }
return try y.0.invoke(value: x, arguments: arguments)
}
}
}
@@ -42,17 +47,17 @@ public struct Variable : Equatable, Resolvable {
self.variable = variable
}
private func lookup() -> [String] {
return variable.characters.split(".").map(String.init)
fileprivate func lookup() -> [String] {
return variable.characters.split(separator: ".").map(String.init)
}
/// Resolve the variable in the given context
public func resolve(context: Context) throws -> Any? {
public func resolve(_ context: Context) throws -> Any? {
var current: Any? = context
if (variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\"")) {
// String literal
return variable[variable.startIndex.successor() ..< variable.endIndex.predecessor()]
return variable[variable.characters.index(after: variable.startIndex) ..< variable.characters.index(before: variable.endIndex)]
}
for bit in lookup() {
@@ -64,7 +69,11 @@ public struct Variable : Equatable, Resolvable {
current = dictionary[bit]
} else if let array = current as? [Any] {
if let index = Int(bit) {
current = array[index]
if index >= 0 && index < array.count {
current = array[index]
} else {
current = nil
}
} else if bit == "first" {
current = array.first
} else if bit == "last" {
@@ -76,8 +85,15 @@ public struct Variable : Equatable, Resolvable {
#if os(Linux)
return nil
#else
current = object.valueForKey(bit)
current = object.value(forKey: bit)
#endif
} else if let value = current {
let mirror = Mirror(reflecting: value)
current = mirror.descendant(bit)
if current == nil {
return nil
}
} else {
return nil
}
@@ -92,7 +108,7 @@ public func ==(lhs: Variable, rhs: Variable) -> Bool {
}
func normalize(current: Any?) -> Any? {
func normalize(_ current: Any?) -> Any? {
if let current = current as? Normalizable {
return current.normalize()
}
@@ -131,3 +147,13 @@ extension Dictionary : Normalizable {
return dictionary
}
}
func parseFilterComponents(token: String) -> (String, [Variable]) {
var components = token.smartSplit(separator: ":")
let name = components.removeFirst()
let variables = components
.joined(separator: ":")
.smartSplit(separator: ",")
.map { Variable($0) }
return (name, variables)
}

View File

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

View File

@@ -1,70 +0,0 @@
import Spectre
import Stencil
func testFilter() {
describe("template filters") {
let context = Context(dictionary: ["name": "Kyle"])
$0.it("allows you to register a custom filter") {
let template = Template(templateString: "{{ name|repeat }}")
let namespace = Namespace()
namespace.registerFilter("repeat") { value in
if let value = value as? String {
return "\(value) \(value)"
}
return nil
}
let result = try template.render(context, namespace: namespace)
try expect(result) == "Kyle Kyle"
}
$0.it("allows you to register a custom which throws") {
let template = Template(templateString: "{{ name|repeat }}")
let namespace = Namespace()
namespace.registerFilter("repeat") { value in
throw TemplateSyntaxError("No Repeat")
}
try expect(try template.render(context, namespace: namespace)).toThrow(TemplateSyntaxError("No Repeat"))
}
$0.it("allows whitespace in expression") {
let template = Template(templateString: "{{ name | uppercase }}")
let result = try template.render(Context(dictionary: ["name": "kyle"]))
try expect(result) == "KYLE"
}
}
describe("capitalize filter") {
let template = Template(templateString: "{{ name|capitalize }}")
$0.it("capitalizes a string") {
let result = try template.render(Context(dictionary: ["name": "kyle"]))
try expect(result) == "Kyle"
}
}
describe("uppercase filter") {
let template = Template(templateString: "{{ name|uppercase }}")
$0.it("transforms a string to be uppercase") {
let result = try template.render(Context(dictionary: ["name": "kyle"]))
try expect(result) == "KYLE"
}
}
describe("lowercase filter") {
let template = Template(templateString: "{{ name|lowercase }}")
$0.it("transforms a string to be lowercase") {
let result = try template.render(Context(dictionary: ["name": "Kyle"]))
try expect(result) == "kyle"
}
}
}

3
Tests/LinuxMain.swift Normal file
View File

@@ -0,0 +1,3 @@
import StencilTests
stencilTests()

View File

@@ -1,116 +0,0 @@
import Spectre
import Stencil
func testIfNode() {
describe("IfNode") {
$0.describe("parsing") {
$0.it("can parse an if block") {
let tokens = [
Token.Block(value: "if value"),
Token.Text(value: "true"),
Token.Block(value: "else"),
Token.Text(value: "false"),
Token.Block(value: "endif")
]
let parser = TokenParser(tokens: tokens, namespace: Namespace())
let nodes = try parser.parse()
let node = nodes.first as? IfNode
let trueNode = node?.trueNodes.first as? TextNode
let falseNode = node?.falseNodes.first as? TextNode
try expect(nodes.count) == 1
try expect(node?.variable.variable) == "value"
try expect(node?.trueNodes.count) == 1
try expect(trueNode?.text) == "true"
try expect(node?.falseNodes.count) == 1
try expect(falseNode?.text) == "false"
}
$0.it("can parse an ifnot block") {
let tokens = [
Token.Block(value: "ifnot value"),
Token.Text(value: "false"),
Token.Block(value: "else"),
Token.Text(value: "true"),
Token.Block(value: "endif")
]
let parser = TokenParser(tokens: tokens, namespace: Namespace())
let nodes = try parser.parse()
let node = nodes.first as? IfNode
let trueNode = node?.trueNodes.first as? TextNode
let falseNode = node?.falseNodes.first as? TextNode
try expect(nodes.count) == 1
try expect(node?.variable.variable) == "value"
try expect(node?.trueNodes.count) == 1
try expect(trueNode?.text) == "true"
try expect(node?.falseNodes.count) == 1
try expect(falseNode?.text) == "false"
}
$0.it("throws an error when parsing an if block without an endif") {
let tokens = [
Token.Block(value: "if value"),
]
let parser = TokenParser(tokens: tokens, namespace: Namespace())
let error = TemplateSyntaxError("`endif` was not found.")
try expect(try parser.parse()).toThrow(error)
}
$0.it("throws an error when parsing an ifnot without an endif") {
let tokens = [
Token.Block(value: "ifnot value"),
]
let parser = TokenParser(tokens: tokens, namespace: Namespace())
let error = TemplateSyntaxError("`endif` was not found.")
try expect(try parser.parse()).toThrow(error)
}
}
$0.describe("rendering") {
let context = Context(dictionary: ["items": true])
$0.it("renders the truth when expression evaluates to true") {
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
try expect(try node.render(context)) == "true"
}
$0.it("renders the false when expression evaluates to false") {
let node = IfNode(variable: "unknown", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
try expect(try node.render(context)) == "false"
}
$0.it("renders the truth when array expression is not empty") {
let items: [[String: Any]] = [["key":"key1","value":42],["key":"key2","value":1337]]
let arrayContext = Context(dictionary: ["items": [items]])
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
try expect(try node.render(arrayContext)) == "true"
}
$0.it("renders the false when array expression is empty") {
let emptyItems = [[String: Any]]()
let arrayContext = Context(dictionary: ["items": emptyItems])
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
try expect(try node.render(arrayContext)) == "false"
}
$0.it("renders the false when dictionary expression is empty") {
let emptyItems = [String:Any]()
let arrayContext = Context(dictionary: ["items": emptyItems])
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
try expect(try node.render(arrayContext)) == "false"
}
$0.it("renders the false when Array<Any> variable is empty") {
let arrayContext = Context(dictionary: ["items": ([] as [Any])])
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
try expect(try node.render(arrayContext)) == "false"
}
}
}
}

View File

@@ -27,35 +27,39 @@ func testContext() {
}
$0.it("allows you to retrieve a value from a parent") {
context.push()
try expect(context["name"] as? String) == "Kyle"
try 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"
try context.push {
context["name"] = "Katie"
try expect(context["name"] as? String) == "Katie"
}
}
$0.it("allows you to pop to restore previous state") {
context.push()
context["name"] = "Katie"
context.pop()
context.push {
context["name"] = "Katie"
}
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 remove a parent's value in a level") {
try context.push {
context["name"] = nil
try expect(context["name"]).to.beNil()
}
try expect(context["name"] as? String) == "Kyle"
}
$0.it("allows you to push a dictionary and run a closure then restoring previous state") {
var didRun = false
try context.push(["name": "Katie"]) {
try context.push(dictionary: ["name": "Katie"]) {
didRun = true
try expect(context["name"] as? String) == "Katie"
}
@@ -63,5 +67,15 @@ func testContext() {
try expect(didRun).to.beTrue()
try expect(context["name"] as? String) == "Kyle"
}
$0.it("allows you to flatten the context contents") {
try context.push(dictionary: ["test": "abc"]) {
let flattened = context.flatten()
try expect(flattened.count) == 2
try expect(flattened["name"] as? String) == "Kyle"
try expect(flattened["test"] as? String) == "abc"
}
}
}
}

View File

@@ -0,0 +1,282 @@
import Spectre
@testable import Stencil
func testExpressions() {
describe("Expression") {
$0.describe("VariableExpression") {
let expression = VariableExpression(variable: Variable("value"))
$0.it("evaluates to true when value is not nil") {
let context = Context(dictionary: ["value": "known"])
try expect(try expression.evaluate(context: context)).to.beTrue()
}
$0.it("evaluates to false when value is unset") {
let context = Context()
try expect(try expression.evaluate(context: context)).to.beFalse()
}
$0.it("evaluates to true when array variable is not empty") {
let items: [[String: Any]] = [["key":"key1","value":42],["key":"key2","value":1337]]
let context = Context(dictionary: ["value": [items]])
try expect(try expression.evaluate(context: context)).to.beTrue()
}
$0.it("evaluates to false when array value is empty") {
let emptyItems = [[String: Any]]()
let context = Context(dictionary: ["value": emptyItems])
try expect(try expression.evaluate(context: context)).to.beFalse()
}
$0.it("evaluates to false when dictionary value is empty") {
let emptyItems = [String:Any]()
let context = Context(dictionary: ["value": emptyItems])
try expect(try expression.evaluate(context: context)).to.beFalse()
}
$0.it("evaluates to false when Array<Any> value is empty") {
let context = Context(dictionary: ["value": ([] as [Any])])
try expect(try expression.evaluate(context: context)).to.beFalse()
}
$0.it("evaluates to true when integer value is above 0") {
let context = Context(dictionary: ["value": 1])
try expect(try expression.evaluate(context: context)).to.beTrue()
}
$0.it("evaluates to true with string") {
let context = Context(dictionary: ["value": "test"])
try expect(try expression.evaluate(context: context)).to.beTrue()
}
$0.it("evaluates to false when empty string") {
let context = Context(dictionary: ["value": ""])
try expect(try expression.evaluate(context: context)).to.beFalse()
}
$0.it("evaluates to false when integer value is below 0 or below") {
let context = Context(dictionary: ["value": 0])
try expect(try expression.evaluate(context: context)).to.beFalse()
let negativeContext = Context(dictionary: ["value": 0])
try expect(try expression.evaluate(context: negativeContext)).to.beFalse()
}
$0.it("evaluates to true when float value is above 0") {
let context = Context(dictionary: ["value": Float(0.5)])
try expect(try expression.evaluate(context: context)).to.beTrue()
}
$0.it("evaluates to false when float is 0 or below") {
let context = Context(dictionary: ["value": Float(0)])
try expect(try expression.evaluate(context: context)).to.beFalse()
}
$0.it("evaluates to true when double value is above 0") {
let context = Context(dictionary: ["value": Double(0.5)])
try expect(try expression.evaluate(context: context)).to.beTrue()
}
$0.it("evaluates to false when double is 0 or below") {
let context = Context(dictionary: ["value": Double(0)])
try expect(try expression.evaluate(context: context)).to.beFalse()
}
$0.it("evaluates to false when uint is 0") {
let context = Context(dictionary: ["value": UInt(0)])
try expect(try expression.evaluate(context: context)).to.beFalse()
}
}
$0.describe("NotExpression") {
$0.it("returns truthy for positive expressions") {
let expression = NotExpression(expression: StaticExpression(value: true))
try expect(expression.evaluate(context: Context())).to.beFalse()
}
$0.it("returns falsy for negative expressions") {
let expression = NotExpression(expression: StaticExpression(value: false))
try expect(expression.evaluate(context: Context())).to.beTrue()
}
}
$0.describe("expression parsing") {
$0.it("can parse a variable expression") {
let expression = try parseExpression(components: ["value"])
try expect(expression.evaluate(context: Context())).to.beFalse()
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue()
}
$0.it("can parse a not expression") {
let expression = try parseExpression(components: ["not", "value"])
try expect(expression.evaluate(context: Context())).to.beTrue()
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse()
}
$0.describe("and expression") {
let expression = try! parseExpression(components: ["lhs", "and", "rhs"])
$0.it("evaluates to false with lhs false") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse()
}
$0.it("evaluates to false with rhs false") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse()
}
$0.it("evaluates to false with lhs and rhs false") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse()
}
$0.it("evaluates to true with lhs and rhs true") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
}
}
$0.describe("or expression") {
let expression = try! parseExpression(components: ["lhs", "or", "rhs"])
$0.it("evaluates to true with lhs true") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue()
}
$0.it("evaluates to true with rhs true") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beTrue()
}
$0.it("evaluates to true with lhs and rhs true") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
}
$0.it("evaluates to false with lhs and rhs false") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse()
}
}
$0.describe("equality expression") {
let expression = try! parseExpression(components: ["lhs", "==", "rhs"])
$0.it("evaluates to true with equal lhs/rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue()
}
$0.it("evaluates to false with non equal lhs/rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beFalse()
}
$0.it("evaluates to true with nils") {
try expect(expression.evaluate(context: Context(dictionary: [:]))).to.beTrue()
}
$0.it("evaluates to true with numbers") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.0]))).to.beTrue()
}
$0.it("evaluates to false with non equal numbers") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.1]))).to.beFalse()
}
$0.it("evaluates to true with booleans") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
}
$0.it("evaluates to false with falsy booleans") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse()
}
$0.it("evaluates to false with different types") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": 1]))).to.beFalse()
}
}
$0.describe("inequality expression") {
let expression = try! parseExpression(components: ["lhs", "!=", "rhs"])
$0.it("evaluates to true with inequal lhs/rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue()
}
$0.it("evaluates to false with equal lhs/rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "b", "rhs": "b"]))).to.beFalse()
}
}
$0.describe("more than expression") {
let expression = try! parseExpression(components: ["lhs", ">", "rhs"])
$0.it("evaluates to true with lhs > rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue()
}
$0.it("evaluates to false with lhs == rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse()
}
}
$0.describe("more than equal expression") {
let expression = try! parseExpression(components: ["lhs", ">=", "rhs"])
$0.it("evaluates to true with lhs == rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
}
$0.it("evaluates to false with lhs < rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.1]))).to.beFalse()
}
}
$0.describe("less than expression") {
let expression = try! parseExpression(components: ["lhs", "<", "rhs"])
$0.it("evaluates to true with lhs < rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue()
}
$0.it("evaluates to false with lhs == rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse()
}
}
$0.describe("less than equal expression") {
let expression = try! parseExpression(components: ["lhs", "<=", "rhs"])
$0.it("evaluates to true with lhs == rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
}
$0.it("evaluates to false with lhs > rhs") {
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.1, "rhs": 5.0]))).to.beFalse()
}
}
$0.describe("multiple expression") {
let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"])
$0.it("evaluates to true with one") {
try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue()
}
$0.it("evaluates to true with one and three") {
try expect(expression.evaluate(context: Context(dictionary: ["one": true, "three": true]))).to.beTrue()
}
$0.it("evaluates to true with two") {
try expect(expression.evaluate(context: Context(dictionary: ["two": true]))).to.beTrue()
}
$0.it("evaluates to false with two and three") {
try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse()
}
$0.it("evaluates to false with two and three") {
try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse()
}
$0.it("evaluates to false with nothing") {
try expect(expression.evaluate(context: Context())).to.beFalse()
}
}
}
}
}

View File

@@ -0,0 +1,120 @@
import Spectre
import Stencil
func testFilter() {
describe("template filters") {
let context: [String: Any] = ["name": "Kyle"]
$0.it("allows you to register a custom filter") {
let template = Template(templateString: "{{ name|repeat }}")
let namespace = Namespace()
namespace.registerFilter("repeat") { (value: Any?) in
if let value = value as? String {
return "\(value) \(value)"
}
return nil
}
let result = try template.render(Context(dictionary: context, namespace: namespace))
try expect(result) == "Kyle Kyle"
}
$0.it("allows you to register a custom filter which accepts arguments") {
let template = Template(templateString: "{{ name|repeat:'value' }}")
let namespace = Namespace()
namespace.registerFilter("repeat") { value, arguments in
if !arguments.isEmpty {
return "\(value!) \(value!) with args \(arguments.first!!)"
}
return nil
}
let result = try template.render(Context(dictionary: context, namespace: namespace))
try expect(result) == "Kyle Kyle with args value"
}
$0.it("allows you to register a custom which throws") {
let template = Template(templateString: "{{ name|repeat }}")
let namespace = Namespace()
namespace.registerFilter("repeat") { (value: Any?) in
throw TemplateSyntaxError("No Repeat")
}
try expect(try template.render(Context(dictionary: context, namespace: namespace))).toThrow(TemplateSyntaxError("No Repeat"))
}
$0.it("allows whitespace in expression") {
let template = Template(templateString: "{{ name | uppercase }}")
let result = try template.render(Context(dictionary: ["name": "kyle"]))
try expect(result) == "KYLE"
}
$0.it("throws when you pass arguments to simple filter") {
let template = Template(templateString: "{{ name|uppercase:5 }}")
try expect(try template.render(Context(dictionary: ["name": "kyle"]))).toThrow()
}
}
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"
}
}
describe("default filter") {
let template = Template(templateString: "Hello {{ name|default:\"World\" }}")
$0.it("shows the variable value") {
let result = try template.render(Context(dictionary: ["name": "Kyle"]))
try expect(result) == "Hello Kyle"
}
$0.it("shows the default value") {
let result = try template.render(Context(dictionary: [:]))
try expect(result) == "Hello World"
}
$0.it("supports multiple defaults") {
let template = Template(templateString: "Hello {{ name|default:a,b,c,\"World\" }}")
let result = try template.render(Context(dictionary: [:]))
try expect(result) == "Hello World"
}
}
describe("join filter") {
let template = Template(templateString: "{{ value|join:\", \" }}")
$0.it("transforms a string to be lowercase") {
let result = try template.render(Context(dictionary: ["value": ["One", "Two"]]))
try expect(result) == "One, Two"
}
}
}

View File

@@ -1,5 +1,5 @@
import Spectre
import Stencil
@testable import Stencil
import Foundation
@@ -12,14 +12,14 @@ func testForNode() {
$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: [])
let node = ForNode(resolvable: 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)
let node = ForNode(resolvable: Variable("emptyItems"), loopVariable: "item", nodes: nodes, emptyNodes: emptyNodes)
try expect(try node.render(context)) == "empty"
}
@@ -29,8 +29,8 @@ func testForNode() {
])
let nodes: [NodeType] = [VariableNode(variable: "item")]
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
try expect(try node.render(any_context)) == "123"
let node = ForNode(resolvable: Variable("items"), loopVariable: "item", nodes: nodes, emptyNodes: [])
try expect(try node.render(any_context)) == "123"
}
#if os(OSX)
@@ -40,27 +40,56 @@ func testForNode() {
])
let nodes: [NodeType] = [VariableNode(variable: "item")]
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
let node = ForNode(resolvable: Variable("items"), loopVariable: "item", nodes: nodes, emptyNodes: [])
try expect(try node.render(nsarray_context)) == "123"
}
#endif
$0.it("renders the given nodes while providing if the item is first in the context") {
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.first")]
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
let node = ForNode(resolvable: Variable("items"), loopVariable: "item", nodes: nodes, emptyNodes: [])
try expect(try node.render(context)) == "1true2false3false"
}
$0.it("renders the given nodes while providing if the item is last in the context") {
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.last")]
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
let node = ForNode(resolvable: Variable("items"), loopVariable: "item", nodes: nodes, emptyNodes: [])
try expect(try node.render(context)) == "1false2false3true"
}
$0.it("renders the given nodes while providing item counter") {
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
let node = ForNode(resolvable: Variable("items"), loopVariable: "item", nodes: nodes, emptyNodes: [])
try expect(try node.render(context)) == "112233"
}
$0.it("can render a filter") {
let templateString = "{% for article in ars|default:articles %}" +
"- {{ article.title }} by {{ article.author }}.\n" +
"{% endfor %}\n"
let context = Context(dictionary: [
"articles": [
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
]
])
let template = Template(templateString: templateString)
let result = try template.render(context)
let fixture = "" +
"- Migrating from OCUnit to XCTest by Kyle Fuller.\n" +
"- Memory Management with ARC by Kyle Fuller.\n" +
"\n"
try expect(result) == fixture
}
}
}
fileprivate struct Article {
let title: String
let author: String
}

View File

@@ -0,0 +1,85 @@
import Spectre
@testable import Stencil
func testIfNode() {
describe("IfNode") {
$0.describe("parsing") {
$0.it("can parse an if block") {
let tokens: [Token] = [
.block(value: "if value"),
.text(value: "true"),
.block(value: "else"),
.text(value: "false"),
.block(value: "endif")
]
let parser = TokenParser(tokens: tokens, namespace: Namespace())
let nodes = try parser.parse()
let node = nodes.first as? IfNode
let trueNode = node?.trueNodes.first as? TextNode
let falseNode = node?.falseNodes.first as? TextNode
try expect(nodes.count) == 1
try expect(node?.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"),
.text(value: "false"),
.block(value: "else"),
.text(value: "true"),
.block(value: "endif")
]
let parser = TokenParser(tokens: tokens, namespace: Namespace())
let nodes = try parser.parse()
let node = nodes.first as? IfNode
let trueNode = node?.trueNodes.first as? TextNode
let falseNode = node?.falseNodes.first as? TextNode
try expect(nodes.count) == 1
try expect(node?.trueNodes.count) == 1
try expect(trueNode?.text) == "true"
try expect(node?.falseNodes.count) == 1
try expect(falseNode?.text) == "false"
}
$0.it("throws an error when parsing an if block without an endif") {
let tokens: [Token] = [
.block(value: "if value"),
]
let parser = TokenParser(tokens: tokens, namespace: Namespace())
let error = TemplateSyntaxError("`endif` was not found.")
try expect(try parser.parse()).toThrow(error)
}
$0.it("throws an error when parsing an ifnot without an endif") {
let tokens: [Token] = [
.block(value: "ifnot value"),
]
let parser = TokenParser(tokens: tokens, namespace: Namespace())
let error = TemplateSyntaxError("`endif` was not found.")
try expect(try parser.parse()).toThrow(error)
}
}
$0.describe("rendering") {
$0.it("renders the truth when expression evaluates to true") {
let node = IfNode(expression: StaticExpression(value: true), 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(expression: StaticExpression(value: false), trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
try expect(try node.render(Context())) == "false"
}
}
}
}

View File

@@ -1,16 +1,16 @@
import Spectre
import Stencil
@testable import Stencil
import PathKit
func testInclude() {
describe("Include") {
let path = Path(__FILE__) + ".." + ".." + "fixtures"
let loader = TemplateLoader(paths: [path])
let path = Path(#file) + ".." + "fixtures"
let loader = FileSystemLoader(paths: [path])
$0.describe("parsing") {
$0.it("throws an error when no template is given") {
let tokens = [ Token.Block(value: "include") ]
let tokens: [Token] = [ .block(value: "include") ]
let parser = TokenParser(tokens: tokens, namespace: Namespace())
let error = TemplateSyntaxError("'include' tag takes one argument, the template file to be included")
@@ -18,7 +18,7 @@ func testInclude() {
}
$0.it("can parse a valid include block") {
let tokens = [ Token.Block(value: "include \"test.html\"") ]
let tokens: [Token] = [ .block(value: "include \"test.html\"") ]
let parser = TokenParser(tokens: tokens, namespace: Namespace())
let nodes = try parser.parse()
@@ -33,7 +33,7 @@ func testInclude() {
let node = IncludeNode(templateName: Variable("\"test.html\""))
do {
try node.render(Context())
_ = try node.render(Context())
} catch {
try expect("\(error)") == "Template loader not in context"
}
@@ -43,7 +43,7 @@ func testInclude() {
let node = IncludeNode(templateName: Variable("\"unknown.html\""))
do {
try node.render(Context(dictionary: ["loader": loader]))
_ = try node.render(Context(dictionary: ["loader": loader]))
} catch {
try expect("\(error)".hasPrefix("'unknown.html' template not found")).to.beTrue()
}

View File

@@ -0,0 +1,23 @@
import Spectre
import Stencil
import PathKit
func testInheritence() {
describe("Inheritence") {
let path = Path(#file) + ".." + "fixtures"
let loader = FileSystemLoader(paths: [path])
$0.it("can inherit from another template") {
let context = Context(dictionary: ["loader": loader])
let template = try loader.loadTemplate(name: "child.html")
try expect(try template?.render(context)) == "Header\nChild"
}
$0.it("can inherit from another template inheriting from another template") {
let context = Context(dictionary: ["loader": loader])
let template = try loader.loadTemplate(name: "child-child.html")
try expect(try template?.render(context)) == "Child Child Header\nChild"
}
}
}

View File

@@ -1,5 +1,5 @@
import Spectre
import Stencil
@testable import Stencil
func testLexer() {
@@ -9,7 +9,7 @@ func testLexer() {
let tokens = lexer.tokenize()
try expect(tokens.count) == 1
try expect(tokens.first) == Token.Text(value: "Hello World")
try expect(tokens.first) == .text(value: "Hello World")
}
$0.it("can tokenize a comment") {
@@ -17,7 +17,7 @@ func testLexer() {
let tokens = lexer.tokenize()
try expect(tokens.count) == (1)
try expect(tokens.first) == Token.Comment(value: "Comment")
try expect(tokens.first) == .comment(value: "Comment")
}
$0.it("can tokenize a variable") {
@@ -25,7 +25,7 @@ func testLexer() {
let tokens = lexer.tokenize()
try expect(tokens.count) == 1
try expect(tokens.first) == Token.Variable(value: "Variable")
try expect(tokens.first) == .variable(value: "Variable")
}
$0.it("can tokenize a mixture of content") {
@@ -33,9 +33,9 @@ func testLexer() {
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: ".")
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") {
@@ -43,8 +43,8 @@ func testLexer() {
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")
try expect(tokens[0]) == Token.variable(value: "thing")
try expect(tokens[1]) == Token.variable(value: "name")
}
}
}

View File

@@ -3,7 +3,7 @@ import Stencil
class ErrorNode : NodeType {
func render(context: Context) throws -> String {
func render(_ context: Context) throws -> String {
throw TemplateSyntaxError("Custom Error")
}
}

View File

@@ -1,6 +1,6 @@
import Foundation
import Spectre
import Stencil
@testable import Stencil
func testNowNode() {
@@ -8,7 +8,7 @@ func testNowNode() {
describe("NowNode") {
$0.describe("parsing") {
$0.it("parses default format without any now arguments") {
let tokens = [ Token.Block(value: "now") ]
let tokens: [Token] = [ .block(value: "now") ]
let parser = TokenParser(tokens: tokens, namespace: Namespace())
let nodes = try parser.parse()
@@ -18,7 +18,7 @@ func testNowNode() {
}
$0.it("parses now with a format") {
let tokens = [ Token.Block(value: "now \"HH:mm\"") ]
let tokens: [Token] = [ .block(value: "now \"HH:mm\"") ]
let parser = TokenParser(tokens: tokens, namespace: Namespace())
let nodes = try parser.parse()
let node = nodes.first as? NowNode
@@ -31,9 +31,9 @@ func testNowNode() {
$0.it("renders the date") {
let node = NowNode(format: Variable("\"yyyy-MM-dd\""))
let formatter = NSDateFormatter()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let date = formatter.stringFromDate(NSDate())
let date = formatter.string(from: NSDate() as Date)
try expect(try node.render(Context())) == date
}

View File

@@ -6,7 +6,7 @@ func testTokenParser() {
describe("TokenParser") {
$0.it("can parse a text token") {
let parser = TokenParser(tokens: [
Token.Text(value: "Hello World")
.text(value: "Hello World")
], namespace: Namespace())
let nodes = try parser.parse()
@@ -18,7 +18,7 @@ func testTokenParser() {
$0.it("can parse a variable token") {
let parser = TokenParser(tokens: [
Token.Variable(value: "'name'")
.variable(value: "'name'")
], namespace: Namespace())
let nodes = try parser.parse()
@@ -30,7 +30,7 @@ func testTokenParser() {
$0.it("can parse a comment token") {
let parser = TokenParser(tokens: [
Token.Comment(value: "Secret stuff!")
.comment(value: "Secret stuff!")
], namespace: Namespace())
let nodes = try parser.parse()
@@ -44,7 +44,7 @@ func testTokenParser() {
}
let parser = TokenParser(tokens: [
Token.Block(value: "known"),
.block(value: "known"),
], namespace: namespace)
let nodes = try parser.parse()
@@ -53,7 +53,7 @@ func testTokenParser() {
$0.it("errors when parsing an unknown tag") {
let parser = TokenParser(tokens: [
Token.Block(value: "unknown"),
.block(value: "unknown"),
], namespace: Namespace())
try expect(try parser.parse()).toThrow(TemplateSyntaxError("Unknown template tag 'unknown'"))

View File

@@ -2,16 +2,23 @@ import Spectre
import Stencil
class CustomNode : NodeType {
func render(context:Context) throws -> String {
fileprivate class CustomNode : NodeType {
func render(_ context:Context) throws -> String {
return "Hello World"
}
}
fileprivate struct Article {
let title: String
let author: String
}
func testStencil() {
describe("Stencil") {
$0.it("can render the README example") {
let templateString = "There are {{ articles.count }} articles.\n" +
"\n" +
"{% for article in articles %}" +
@@ -20,8 +27,8 @@ func testStencil() {
let context = Context(dictionary: [
"articles": [
[ "title": "Migrating from OCUnit to XCTest", "author": "Kyle Fuller" ],
[ "title": "Memory Management with ARC", "author": "Kyle Fuller" ],
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
]
])
@@ -46,7 +53,7 @@ func testStencil() {
return CustomNode()
}
let result = try template.render(namespace: namespace)
let result = try template.render(Context(namespace: namespace))
try expect(result) == "Hello World"
}
@@ -59,7 +66,7 @@ func testStencil() {
return "Hello World"
}
try expect(try template.render(namespace: namespace)) == "Hello World"
try expect(try template.render(Context(namespace: namespace))) == "Hello World"
}
}
}

View File

@@ -5,19 +5,19 @@ import PathKit
func testTemplateLoader() {
describe("TemplateLoader") {
let path = Path(__FILE__) + ".." + ".." + "Tests" + "fixtures"
let loader = TemplateLoader(paths: [path])
let path = Path(#file) + ".." + "fixtures"
let loader = FileSystemLoader(paths: [path])
$0.it("returns nil when a template cannot be found") {
try expect(loader.loadTemplate("unknown.html")).to.beNil()
try expect(try loader.loadTemplate(name: "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()
try expect(try loader.loadTemplate(names: ["unknown.html", "unknown2.html"])).to.beNil()
}
$0.it("can load a template from a file") {
if loader.loadTemplate("test.html") == nil {
if try loader.loadTemplate(name: "test.html") == nil {
throw failure("didn't find the template")
}
}

View File

@@ -10,5 +10,12 @@ func testTemplate() {
let result = try template.render(context)
try expect(result) == "Hello World"
}
$0.it("can render a template from a string literal") {
let context = Context(dictionary: [ "name": "Kyle" ])
let template: Template = "Hello World"
let result = try template.render(context)
try expect(result) == "Hello World"
}
}
}

View File

@@ -5,7 +5,7 @@ import Stencil
func testToken() {
describe("Token") {
$0.it("can split the contents into components") {
let token = Token.Text(value: "hello world")
let token = Token.text(value: "hello world")
let components = token.components()
try expect(components.count) == 2
@@ -14,7 +14,7 @@ func testToken() {
}
$0.it("can split the contents into components with single quoted strings") {
let token = Token.Text(value: "hello 'kyle fuller'")
let token = Token.text(value: "hello 'kyle fuller'")
let components = token.components()
try expect(components.count) == 2
@@ -23,7 +23,7 @@ func testToken() {
}
$0.it("can split the contents into components with double quoted strings") {
let token = Token.Text(value: "hello \"kyle fuller\"")
let token = Token.text(value: "hello \"kyle fuller\"")
let components = token.components()
try expect(components.count) == 2

View File

@@ -9,6 +9,14 @@ import Stencil
}
#endif
fileprivate struct Person {
let name: String
}
fileprivate struct Article {
let author: Person
}
func testVariable() {
describe("Variable") {
@@ -18,10 +26,11 @@ func testVariable() {
"profiles": [
"github": "kylef",
],
"article": Article(author: Person(name: "Kyle"))
])
#if os(OSX)
context.push(["object": Object()])
context["object"] = Object()
#endif
$0.it("can resolve a string literal with double quotes") {
@@ -52,6 +61,20 @@ func testVariable() {
let variable = Variable("contacts.0")
let result = try variable.resolve(context) as? String
try expect(result) == "Katie"
let variable1 = Variable("contacts.1")
let result1 = try variable1.resolve(context) as? String
try expect(result1) == "Carlton"
}
$0.it("can resolve an item from an array via unknown index") {
let variable = Variable("contacts.5")
let result = try variable.resolve(context) as? String
try expect(result).to.beNil()
let variable1 = Variable("contacts.-5")
let result1 = try variable1.resolve(context) as? String
try expect(result1).to.beNil()
}
$0.it("can resolve the first item from an array") {
@@ -66,6 +89,12 @@ func testVariable() {
try expect(result) == "Carlton"
}
$0.it("can resolve a property with reflection") {
let variable = Variable("article.author.name")
let result = try variable.resolve(context) as? String
try expect(result) == "Kyle"
}
#if os(OSX)
$0.it("can resolve a value via KVO") {
let variable = Variable("object.title")

View File

@@ -0,0 +1,28 @@
import XCTest
public func stencilTests() {
testContext()
testFilter()
testLexer()
testToken()
testTokenParser()
testTemplateLoader()
testTemplate()
testVariable()
testNode()
testForNode()
testExpressions()
testIfNode()
testNowNode()
testInclude()
testInheritence()
testStencil()
}
class StencilTests: XCTestCase {
func testRunStencilTests() {
stencilTests()
}
}

View File

@@ -0,0 +1,2 @@
{% extends "child.html" %}
{% block header %}Child Child Header{% endblock %}

View File

@@ -1,17 +0,0 @@
import Spectre
import Stencil
import PathKit
func testInheritence() {
describe("Inheritence") {
let path = Path(__FILE__) + ".." + ".." + "fixtures"
let loader = TemplateLoader(paths: [path])
$0.it("can inherit from another template") {
let context = Context(dictionary: ["loader": loader])
let template = loader.loadTemplate("child.html")
try expect(try template?.render(context)) == "Header\nChild"
}
}
}

View File

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

225
docs/Makefile Normal file
View File

@@ -0,0 +1,225 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " epub3 to make an epub3"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
@echo " dummy to check syntax errors of document sources"
.PHONY: clean
clean:
rm -rf $(BUILDDIR)/*
.PHONY: html
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
.PHONY: singlehtml
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
.PHONY: pickle
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
.PHONY: json
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
.PHONY: htmlhelp
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
.PHONY: qthelp
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Stencil.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Stencil.qhc"
.PHONY: applehelp
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
.PHONY: devhelp
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/Stencil"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Stencil"
@echo "# devhelp"
.PHONY: epub
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
.PHONY: epub3
epub3:
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
@echo
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
.PHONY: latex
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
.PHONY: latexpdf
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: latexpdfja
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: text
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
.PHONY: man
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
.PHONY: texinfo
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
.PHONY: info
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
.PHONY: gettext
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
.PHONY: changes
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
.PHONY: linkcheck
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
.PHONY: doctest
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
.PHONY: coverage
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
.PHONY: xml
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
.PHONY: pseudoxml
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
.PHONY: dummy
dummy:
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
@echo
@echo "Build finished. Dummy builder generates no files."

33
docs/_templates/sidebar_intro.html vendored Normal file
View File

@@ -0,0 +1,33 @@
<h1><a href="/">Stencil</a></h1>
<p>
<iframe
src="https://ghbtns.com/github-btn.html?user=kylef&repo=Stencil&type=watch&count=true&size=large"
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px">
</iframe>
</p>
<p>Stencil is a simple and powerful template language for Swift.</p>
<div class="social">
<p>
<iframe
src="https://ghbtns.com/github-btn.html?user=kylef&type=follow&count=false"
allowtransparency="true" frameborder="0" scrolling="0" width="200" height="20">
</iframe>
</p>
<p>
<a href="https://twitter.com/kylefuller" class="twitter-follow-button" data-show-count="false">Follow @kylefuller</a> <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');</script>
</p>
</div>
<h3>Other Projects</h3>
<p>More <a href="https://fuller.li/">Kyle Fuller</a> projects:</p>
<ul>
<li><a href="https://github.com/kylef/Commander">Commander</a></li>
<li><a href="https://curassow.fuller.li/">Curassow</a></li>
<li><a href="https://github.com/kylef/Spectre">Spectre</a></li>
<li><a href="https://github.com/kylef/heroku-buildpack-swift">Heroku Swift buildpack</a></li>
</ul>

51
docs/api/context.rst Normal file
View File

@@ -0,0 +1,51 @@
Context
=======
A Context is a structure containing any templates you would like to use in a
template. Its somewhat like a dictionary, however you can push and pop to
scope variables. So that means that when iterating over a for loop, you can
push a new scope into the context to store any variables local to the scope.
You can initialise a ``Context`` with a ``Dictionary``.
.. code-block:: swift
Context(dictionary: [String: Any]? = nil)
API
----
Subscripting
~~~~~~~~~~~~
You can use subscripting to get and set values from the context.
.. code-block:: swift
context["key"] = value
let value = context["key"]
``push()``
~~~~~~~~~~
A ``Context`` is a stack. You can push a new level onto the ``Context`` so that
modifications can easily be poped off. This is useful for isolating mutations
into scope of a template tag. Such as ``{% if %}`` and ``{% for %}`` tags.
.. code-block:: swift
context.push(["name": "example"]) {
// context contains name which is `example`.
}
// name is popped off the context after the duration of the closure.
``flatten()``
~~~~~~~~~~~~~
Using ``flatten()`` method you can get whole ``Context`` stack as one
dictionary including all variables.
.. code-block:: swift
let dictionary = context.flatten()

257
docs/builtins.rst Normal file
View File

@@ -0,0 +1,257 @@
Built-in template tags and filters
==================================
.. _built-in-tags:
Built-in Tags
-------------
``for``
~~~~~~~
A for loop allows you to iterate over an array found by variable lookup.
.. code-block:: html+django
<ul>
{% for user in users %}
<li>{{ user }}</li>
{% endfor %}
</ul>
The ``for`` tag can take an optional ``{% empty %}`` block that will be
displayed if the given list is empty or could not be found.
.. code-block:: html+django
<ul>
{% for user in users %}
<li>{{ user }}</li>
{% empty %}
<li>There are no users.</li>
{% endfor %}
</ul>
The for block sets a few variables available within the loop:
- ``first`` - True if this is the first time through the loop
- ``last`` - True if this is the last time through the loop
- ``counter`` - The current iteration of the loop
``if``
~~~~~~
The ``{% if %}`` tag evaluates a variable, and if that variable evaluates to
true the contents of the block are processed. Being true is defined as:
* Present in the context
* Being non-empty (dictionaries or arrays)
* Not being a false boolean value
* Not being a numerical value of 0 or below
* Not being an empty string
.. code-block:: html+django
{% if variable %}
The variable was found in the current context.
{% else %}
The variable was not found.
{% endif %}
Operators
^^^^^^^^^
``if`` tags may combine ``and``, ``or`` and ``not`` to test multiple variables
or to negate a variable.
.. code-block:: html+django
{% if one and two %}
Both one and two evaluate to true.
{% endif %}
{% if not one %}
One evaluates to false
{% endif %}
{% if one or two %}
Either one or two evaluates to true.
{% endif %}
{% if not one or two %}
One does not evaluate to false or two evaluates to true.
{% endif %}
You may use ``and``, ``or`` and ``not`` multiple times together. ``not`` has
higest prescidence followed by ``and``. For example:
.. code-block:: html+django
{% if one or two and three %}
Will be treated as:
.. code-block:: text
one or (two and three)
``==`` operator
"""""""""""""""
.. code-block:: html+django
{% if value == other_value %}
value is equal to other_value
{% endif %}
.. note:: The equality operator only supports numerical, string and boolean types.
``!=`` operator
"""""""""""""""
.. code-block:: html+django
{% if value != other_value %}
value is not equal to other_value
{% endif %}
.. note:: The inequality operator only supports numerical, string and boolean types.
``<`` operator
"""""""""""""""
.. code-block:: html+django
{% if value < other_value %}
value is less than other_value
{% endif %}
.. note:: The less than operator only supports numerical types.
``<=`` operator
"""""""""""""""
.. code-block:: html+django
{% if value <= other_value %}
value is less than or equal to other_value
{% endif %}
.. note:: The less than equal operator only supports numerical types.
``>`` operator
"""""""""""""""
.. code-block:: html+django
{% if value > other_value %}
value is more than other_value
{% endif %}
.. note:: The more than operator only supports numerical types.
``>=`` operator
"""""""""""""""
.. code-block:: html+django
{% if value >= other_value %}
value is more than or equal to other_value
{% endif %}
.. note:: The more than equal operator only supports numerical types.
``ifnot``
~~~~~~~~~
.. note:: ``{% ifnot %}`` is deprecated. You should use ``{% if not %}``.
.. code-block:: html+django
{% ifnot variable %}
The variable was NOT found in the current context.
{% else %}
The variable was found.
{% endif %}
``now``
~~~~~~~
``include``
~~~~~~~~~~~
You can include another template using the `include` tag.
.. code-block:: html+django
{% include "comment.html" %}
The `include` tag requires a FileSystemLoader to be found inside your context with the paths, or bundles used to lookup the template.
.. code-block:: swift
let context = Context(dictionary: [
"loader": FileSystemLoader(bundle: [NSBundle.mainBundle()])
])
``extends``
~~~~~~~~~~~
``block``
~~~~~~~~~
.. _built-in-filters:
Built-in Filters
----------------
``capitalize``
~~~~~~~~~~~~~~
The capitalize filter allows you to capitalize a string.
For example, `stencil` to `Stencil`.
.. code-block:: html+django
{{ "stencil"|capitalize }}
``uppercase``
~~~~~~~~~~~~~
The uppercase filter allows you to transform a string to uppercase.
For example, `Stencil` to `STENCIL`.
.. code-block:: html+django
{{ "Stencil"|uppercase }}
``lowercase``
~~~~~~~~~~~~~
The uppercase filter allows you to transform a string to lowercase.
For example, `Stencil` to `stencil`.
.. code-block:: html+django
{{ "Stencil"|lowercase }}
``default``
~~~~~~~~~~~
If a variable not present in the context, use given default. Otherwise, use the
value of the variable. For example:
.. code-block:: html+django
Hello {{ name|default:"World" }}
``join``
~~~~~~~~
Join an array with a string.
.. code-block:: html+django
{{ value|join:", " }}
.. note:: The value MUST be an array of Strngs and the separator must be a string.

341
docs/conf.py Normal file
View File

@@ -0,0 +1,341 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Stencil documentation build configuration file, created by
# sphinx-quickstart on Sun Nov 27 05:54:36 2016.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The encoding of source files.
#
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'Stencil'
copyright = '2016, Kyle Fuller'
author = 'Kyle Fuller'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.7.0'
# The full version, including alpha/beta/rc tags.
release = '0.7.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#
# today = ''
#
# Else, today_fmt is used as the format for a strftime call.
#
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
# The name for this set of Sphinx documents.
# "<project> v<release> documentation" by default.
#
# html_title = 'Stencil v0.6.0'
# A shorter title for the navigation bar. Default is the same as html_title.
#
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#
# html_logo = None
# The name of an image file (relative to this directory) to use as a favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#
# html_extra_path = []
# If not None, a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format.
# The empty string is equivalent to '%b %d, %Y'.
#
# html_last_updated_fmt = None
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
html_sidebars = {
'index': ['sidebar_intro.html', 'searchbox.html'],
'**': ['sidebar_intro.html', 'localtoc.html', 'relations.html', 'searchbox.html'],
}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#
# html_additional_pages = {}
# If false, no module index is generated.
#
# html_domain_indices = True
# If false, no index is generated.
#
# html_use_index = True
# If true, the index is split into individual pages for each letter.
#
# html_split_index = False
# If true, links to the reST sources are added to the pages.
#
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh'
#
# html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# 'ja' uses this config value.
# 'zh' user can custom change `jieba` dictionary path.
#
# html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#
# html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'Stencildoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'Stencil.tex', 'Stencil Documentation',
'Kyle Fuller', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#
# latex_use_parts = False
# If true, show page references after internal links.
#
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
#
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
#
# latex_appendices = []
# It false, will not define \strong, \code, itleref, \crossref ... but only
# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
# packages.
#
# latex_keep_old_macro_names = True
# If false, no module index is generated.
#
# latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'stencil', 'Stencil Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
#
# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'Stencil', 'Stencil Documentation',
author, 'Stencil', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#
# texinfo_appendices = []
# If false, no module index is generated.
#
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#
# texinfo_no_detailmenu = False

View File

@@ -0,0 +1,68 @@
Custom Template Tags and Filters
================================
You can build your own custom filters and tags and pass them down while
rendering your template. Any custom filters or tags must be registered with a
namespace which contains all filters and tags available to the template.
.. code-block:: swift
let namespace = Namespace()
// Register your filters and tags with the namespace
let rendered = try template.render(context, namespace: namespace)
Custom Filters
--------------
Registering custom filters:
.. code-block:: swift
namespace.registerFilter("double") { (value: Any?) in
if let value = value as? Int {
return value * 2
}
return value
}
Registering custom filters with arguments:
.. code-block:: swift
namespace.registerFilter("multiply") { (value: Any?, arguments: [Any?]) in
let amount: Int
if let value = arguments.first as? Int {
amount = value
} else {
throw TemplateSyntaxError("multiple tag must be called with an integer argument")
}
if let value = value as? Int {
return value * 2
}
return value
}
Custom Tags
-----------
You can build a custom template tag. There are a couple of APIs to allow you to
write your own custom tags. The following is the simplest form:
.. code-block:: swift
namespace.registerSimpleTag("custom") { context in
return "Hello World"
}
When your tag is used via ``{% custom %}`` it will execute the registered block
of code allowing you to modify or retrieve a value from the context. Then
return either a string rendered in your template, or 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 Stencil source code.

48
docs/index.rst Normal file
View File

@@ -0,0 +1,48 @@
The Stencil template language
=============================
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.
.. code-block:: html+django
There are {{ articles.count }} articles.
<ul>
{% for article in articles %}
<li>{{ article.title }} by {{ article.author }}</li>
{% endfor %}
</ul>
.. code-block:: swift
struct Article {
let title: String
let author: String
}
let context = Context(dictionary: [
"articles": [
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
]
])
do {
let template = try Template(named: "template.html")
let rendered = try template.render(context)
print(rendered)
} catch {
print("Failed to render template \(error)")
}
Contents:
.. toctree::
:maxdepth: 2
templates
builtins
api/context
custom-template-tags-and-filters

77
docs/templates.rst Normal file
View File

@@ -0,0 +1,77 @@
Templates
=========
- ``{{ ... }}`` for variables to print to the template output
- ``{% ... %}`` for tags
- ``{# ... #}`` for comments not included in the template output
Variables
---------
A variable can be defined in your template using the following:
.. code-block:: html+django
{{ variable }}
Stencil will look up the variable inside the current variable context and
evaluate it. When a variable contains a dot, it will try doing the
following lookup:
- Context lookup
- Dictionary lookup
- Array lookup (first, last, count, index)
- Key value coding lookup
- Type introspection
For example, if `people` was an array:
.. code-block:: html+django
There are {{ people.count }} people. {{ people.first }} is the first
person, followed by {{ people.1 }}.
Filters
~~~~~~~
Filters allow you to transform the values of variables. For example, they look like:
.. code-block:: html+django
{{ variable|uppercase }}
See :ref:`all builtin filters <built-in-filters>`.
Tags
----
Tags are a mechanism to execute a piece of code, allowing you to have
control flow within your template.
.. code-block:: html+django
{% if variable %}
{{ variable }} was found.
{% endif %}
A tag can also affect the context and define variables as follows:
.. code-block:: html+django
{% for item in items %}
{{ item }}
{% endfor %}
Stencil includes of built-in tags which are listed below. You can also
extend Stencil by providing your own tags.
See :ref:`all builtin tags <built-in-tags>`.
Comments
--------
To comment out part of your template, you can use the following syntax:
.. code-block:: html+django
{# My comment is completely hidden #}