Compare commits
46 Commits
0.6.0-beta
...
0.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98edad3566 | ||
|
|
872784f9b3 | ||
|
|
1a01ec592e | ||
|
|
f0591408be | ||
|
|
b7e200a8a0 | ||
|
|
b1da85b140 | ||
|
|
679344f53b | ||
|
|
ada4e81082 | ||
|
|
c99a40c5d9 | ||
|
|
c59b263446 | ||
|
|
ab6f1a032d | ||
|
|
e989317929 | ||
|
|
111306fb60 | ||
|
|
3eb2657a62 | ||
|
|
6ad609e562 | ||
|
|
38d7ec87f6 | ||
|
|
9af9cf4005 | ||
|
|
1975cfd627 | ||
|
|
429290e0b7 | ||
|
|
5ca1b78854 | ||
|
|
a2673bd66b | ||
|
|
9b6ee14aa3 | ||
|
|
3b5e8f2468 | ||
|
|
e84f8a41d4 | ||
|
|
2324808dca | ||
|
|
9fdbbc99e9 | ||
|
|
dfd57e9571 | ||
|
|
3293d8a526 | ||
|
|
393dc88a10 | ||
|
|
a014fecd23 | ||
|
|
a13401b046 | ||
|
|
60b378d482 | ||
|
|
1e3afc0dd5 | ||
|
|
72f3cb579a | ||
|
|
68e6ce3022 | ||
|
|
65c3052aee | ||
|
|
7bbd4f2817 | ||
|
|
7416e6150d | ||
|
|
feff3b18b1 | ||
|
|
f393efbd0b | ||
|
|
df650c6b20 | ||
|
|
3285bac373 | ||
|
|
80427a51e6 | ||
|
|
7bfb69cc82 | ||
|
|
5007ba2c9a | ||
|
|
2d73c58df6 |
@@ -1 +1 @@
|
||||
DEVELOPMENT-SNAPSHOT-2016-01-25-a
|
||||
3.0.1
|
||||
|
||||
10
.travis.yml
10
.travis.yml
@@ -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
67
CHANGELOG.md
Normal 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.
|
||||
7
Makefile
7
Makefile
@@ -1,7 +0,0 @@
|
||||
stencil:
|
||||
@echo "Building Stencil"
|
||||
@swift build
|
||||
|
||||
test: stencil
|
||||
@echo "Running Tests"
|
||||
@.build/debug/spectre-build
|
||||
@@ -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),
|
||||
]
|
||||
)
|
||||
|
||||
211
README.md
211
README.md
@@ -11,21 +11,28 @@ 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 = try Template(named: "template.stencil")
|
||||
let template = try Template(named: "template.html")
|
||||
let rendered = try template.render(context)
|
||||
print(rendered)
|
||||
} catch {
|
||||
@@ -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 the first person,
|
||||
followed by {{ people.1 }}.
|
||||
```
|
||||
|
||||
#### Filters
|
||||
|
||||
Filters allow you to transform the values of variables. For example, they look like:
|
||||
|
||||
```html+django
|
||||
{{ variable|uppercase }}
|
||||
```
|
||||
|
||||
##### Capitalize
|
||||
|
||||
The capitalize filter allows you to capitalize a string.
|
||||
For example, `stencil` to `Stencil`.
|
||||
|
||||
```html+django
|
||||
{{ "stencil"|capitalize }}
|
||||
```
|
||||
|
||||
##### Uppercase
|
||||
|
||||
The uppercase filter allows you to transform a string to uppercase.
|
||||
For example, `Stencil` to `STENCIL`.
|
||||
|
||||
```html+django
|
||||
{{ "Stencil"|uppercase }}
|
||||
```
|
||||
|
||||
##### Lowercase
|
||||
|
||||
The uppercase filter allows you to transform a string to lowercase.
|
||||
For example, `Stencil` to `stencil`.
|
||||
|
||||
```html+django
|
||||
{{ "Stencil"|lowercase }}
|
||||
```
|
||||
|
||||
### Tags
|
||||
|
||||
Tags are a mechanism to execute a piece of code, 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 were 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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// A container for template variables.
|
||||
public class Context {
|
||||
var dictionaries: [[String: Any]]
|
||||
var dictionaries: [[String: Any?]]
|
||||
let namespace: Namespace
|
||||
|
||||
/// Initialise a Context with an optional dictionary and optional namespace
|
||||
@@ -17,7 +17,7 @@ public class Context {
|
||||
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
|
||||
}
|
||||
@@ -37,19 +37,33 @@ public class Context {
|
||||
}
|
||||
|
||||
/// Push a new level into the Context
|
||||
private func push(dictionary: [String: Any]? = nil) {
|
||||
fileprivate func push(_ dictionary: [String: Any]? = nil) {
|
||||
dictionaries.append(dictionary ?? [:])
|
||||
}
|
||||
|
||||
/// Pop the last level off of the Context
|
||||
private 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)
|
||||
defer { pop() }
|
||||
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
300
Sources/Expression.swift
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
public class ForNode : NodeType {
|
||||
let variable:Variable
|
||||
class ForNode : NodeType {
|
||||
let resolvable: Resolvable
|
||||
let loopVariable:String
|
||||
let nodes:[NodeType]
|
||||
let emptyNodes: [NodeType]
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
|
||||
guard components.count == 4 && components[2] == "in" else {
|
||||
@@ -24,35 +24,36 @@ public class ForNode : NodeType {
|
||||
|
||||
if token.contents == "empty" {
|
||||
emptyNodes = try parser.parse(until(["endfor"]))
|
||||
parser.nextToken()
|
||||
_ = parser.nextToken()
|
||||
}
|
||||
|
||||
return ForNode(variable: variable, loopVariable: loopVariable, nodes: forNodes, emptyNodes:emptyNodes)
|
||||
let filter = try parser.compileFilter(variable)
|
||||
return ForNode(resolvable: filter, loopVariable: loopVariable, nodes: forNodes, emptyNodes:emptyNodes)
|
||||
}
|
||||
|
||||
public init(variable:String, loopVariable:String, nodes:[NodeType], emptyNodes:[NodeType]) {
|
||||
self.variable = Variable(variable)
|
||||
init(resolvable: Resolvable, loopVariable:String, nodes:[NodeType], emptyNodes:[NodeType]) {
|
||||
self.resolvable = resolvable
|
||||
self.loopVariable = loopVariable
|
||||
self.nodes = nodes
|
||||
self.emptyNodes = emptyNodes
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
let values = try variable.resolve(context)
|
||||
func render(_ context: Context) throws -> String {
|
||||
let values = try resolvable.resolve(context)
|
||||
|
||||
if let values = values as? [Any] where values.count > 0 {
|
||||
if let values = values as? [Any] , values.count > 0 {
|
||||
let count = values.count
|
||||
return try values.enumerate().map { index, item in
|
||||
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([loopVariable: item, "forloop": forContext]) {
|
||||
return try context.push(dictionary: [loopVariable: item, "forloop": forContext]) {
|
||||
try renderNodes(nodes, context)
|
||||
}
|
||||
}.joinWithSeparator("")
|
||||
}.joined(separator: "")
|
||||
}
|
||||
|
||||
return try context.push {
|
||||
|
||||
@@ -1,14 +1,176 @@
|
||||
public class IfNode : NodeType {
|
||||
public let variable:Variable
|
||||
public let trueNodes:[NodeType]
|
||||
public let falseNodes:[NodeType]
|
||||
enum Operator {
|
||||
case infix(String, Int, InfixOperator.Type)
|
||||
case prefix(String, Int, PrefixOperator.Type)
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
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)`.")
|
||||
}
|
||||
let variable = components[1]
|
||||
components.removeFirst()
|
||||
var trueNodes = [NodeType]()
|
||||
var falseNodes = [NodeType]()
|
||||
|
||||
@@ -20,18 +182,19 @@ public class IfNode : NodeType {
|
||||
|
||||
if token.contents == "else" {
|
||||
falseNodes = try parser.parse(until(["endif"]))
|
||||
parser.nextToken()
|
||||
_ = parser.nextToken()
|
||||
}
|
||||
|
||||
return IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)
|
||||
let expression = try parseExpression(components: components)
|
||||
return IfNode(expression: expression, trueNodes: trueNodes, falseNodes: falseNodes)
|
||||
}
|
||||
|
||||
public class func parse_ifnot(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
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 'if condition' `\(token.contents)`.")
|
||||
throw TemplateSyntaxError("'ifnot' statements should use the following 'ifnot condition' `\(token.contents)`.")
|
||||
}
|
||||
let variable = components[1]
|
||||
components.removeFirst()
|
||||
var trueNodes = [NodeType]()
|
||||
var falseNodes = [NodeType]()
|
||||
|
||||
@@ -43,29 +206,21 @@ public class IfNode : NodeType {
|
||||
|
||||
if token.contents == "else" {
|
||||
trueNodes = try parser.parse(until(["endif"]))
|
||||
parser.nextToken()
|
||||
_ = parser.nextToken()
|
||||
}
|
||||
|
||||
return IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)
|
||||
let expression = try parseExpression(components: components)
|
||||
return IfNode(expression: expression, trueNodes: trueNodes, falseNodes: falseNodes)
|
||||
}
|
||||
|
||||
public init(variable:String, trueNodes:[NodeType], falseNodes:[NodeType]) {
|
||||
self.variable = Variable(variable)
|
||||
init(expression: Expression, trueNodes: [NodeType], falseNodes: [NodeType]) {
|
||||
self.expression = expression
|
||||
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
|
||||
}
|
||||
func render(_ context: Context) throws -> String {
|
||||
let truthy = try expression.evaluate(context: context)
|
||||
|
||||
return try context.push {
|
||||
if truthy {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,13 +67,24 @@ 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)
|
||||
return try context.push([BlockContext.contextKey: blockContext]) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -85,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)
|
||||
}
|
||||
|
||||
@@ -103,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
public struct TemplateSyntaxError : ErrorType, Equatable, CustomStringConvertible {
|
||||
public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
|
||||
public let description:String
|
||||
|
||||
public init(_ description:String) {
|
||||
@@ -17,23 +17,23 @@ public func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -46,14 +46,14 @@ 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?
|
||||
}
|
||||
|
||||
|
||||
@@ -68,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 {
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
public class NowNode : NodeType {
|
||||
public let format:Variable
|
||||
class NowNode : NodeType {
|
||||
let format:Variable
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||
var format:Variable?
|
||||
|
||||
let components = token.components()
|
||||
@@ -19,25 +19,25 @@ public class NowNode : NodeType {
|
||||
return NowNode(format:format)
|
||||
}
|
||||
|
||||
public init(format:Variable?) {
|
||||
init(format:Variable?) {
|
||||
self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"")
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
let date = NSDate()
|
||||
func render(_ context: Context) throws -> String {
|
||||
let date = Date()
|
||||
let format = try self.format.resolve(context)
|
||||
var formatter:NSDateFormatter?
|
||||
var formatter:DateFormatter?
|
||||
|
||||
if let format = format as? NSDateFormatter {
|
||||
if let format = format as? DateFormatter {
|
||||
formatter = format
|
||||
} else if let format = format as? String {
|
||||
formatter = NSDateFormatter()
|
||||
formatter = DateFormatter()
|
||||
formatter!.dateFormat = format
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
|
||||
return formatter!.stringFromDate(date)
|
||||
return formatter!.string(from: date)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,23 +26,32 @@ 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) throws -> String {
|
||||
public func render(_ context: Context? = nil) throws -> String {
|
||||
let context = context ?? Context()
|
||||
let parser = TokenParser(tokens: tokens, namespace: context.namespace)
|
||||
let nodes = try parser.parse()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
public func loadTemplate(templateNames: [String]) -> Template? {
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
/// Split a string by spaces leaving quoted phrases together
|
||||
func smartSplit(value: String) -> [String] {
|
||||
extension String {
|
||||
/// Split a string by a separator leaving quoted phrases together
|
||||
func smartSplit(separator: Character = " ") -> [String] {
|
||||
var word = ""
|
||||
var separator: Character = " "
|
||||
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 {
|
||||
@@ -18,10 +19,10 @@ func smartSplit(value: String) -> [String] {
|
||||
word = ""
|
||||
}
|
||||
|
||||
separator = " "
|
||||
separate = separator
|
||||
} else {
|
||||
if separator == " " && (character == "'" || character == "\"") {
|
||||
separator = character
|
||||
if separate == separator && (character == "'" || character == "\"") {
|
||||
separate = character
|
||||
}
|
||||
word.append(character)
|
||||
}
|
||||
@@ -32,45 +33,46 @@ func smartSplit(value: String) -> [String] {
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "Stencil",
|
||||
"version": "0.6.0-beta.1",
|
||||
"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.6.0-beta.1"
|
||||
"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" ]
|
||||
}
|
||||
}
|
||||
|
||||
3
Tests/LinuxMain.swift
Normal file
3
Tests/LinuxMain.swift
Normal file
@@ -0,0 +1,3 @@
|
||||
import StencilTests
|
||||
|
||||
stencilTests()
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,10 +47,19 @@ func testContext() {
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
$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"
|
||||
}
|
||||
@@ -58,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
282
Tests/StencilTests/ExpressionSpec.swift
Normal file
282
Tests/StencilTests/ExpressionSpec.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ func testFilter() {
|
||||
let template = Template(templateString: "{{ name|repeat }}")
|
||||
|
||||
let namespace = Namespace()
|
||||
namespace.registerFilter("repeat") { value in
|
||||
namespace.registerFilter("repeat") { (value: Any?) in
|
||||
if let value = value as? String {
|
||||
return "\(value) \(value)"
|
||||
}
|
||||
@@ -22,10 +22,26 @@ func testFilter() {
|
||||
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 in
|
||||
namespace.registerFilter("repeat") { (value: Any?) in
|
||||
throw TemplateSyntaxError("No Repeat")
|
||||
}
|
||||
|
||||
@@ -37,6 +53,11 @@ func testFilter() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,4 +88,33 @@ func testFilter() {
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,7 +29,7 @@ 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(any_context)) == "123"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
85
Tests/StencilTests/IfNodeSpec.swift
Normal file
85
Tests/StencilTests/IfNodeSpec.swift
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
23
Tests/StencilTests/InheritenceSpec.swift
Normal file
23
Tests/StencilTests/InheritenceSpec.swift
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'"))
|
||||
@@ -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"),
|
||||
]
|
||||
])
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -9,6 +9,14 @@ import Stencil
|
||||
}
|
||||
#endif
|
||||
|
||||
fileprivate struct Person {
|
||||
let name: String
|
||||
}
|
||||
|
||||
fileprivate struct Article {
|
||||
let author: Person
|
||||
}
|
||||
|
||||
|
||||
func testVariable() {
|
||||
describe("Variable") {
|
||||
@@ -18,6 +26,7 @@ func testVariable() {
|
||||
"profiles": [
|
||||
"github": "kylef",
|
||||
],
|
||||
"article": Article(author: Person(name: "Kyle"))
|
||||
])
|
||||
|
||||
#if os(OSX)
|
||||
@@ -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")
|
||||
28
Tests/StencilTests/XCTest.swift
Normal file
28
Tests/StencilTests/XCTest.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
2
Tests/StencilTests/fixtures/child-child.html
Normal file
2
Tests/StencilTests/fixtures/child-child.html
Normal file
@@ -0,0 +1,2 @@
|
||||
{% extends "child.html" %}
|
||||
{% block header %}Child Child Header{% endblock %}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
testContext()
|
||||
testFilter()
|
||||
testLexer()
|
||||
testToken()
|
||||
testTokenParser()
|
||||
testTemplateLoader()
|
||||
testTemplate()
|
||||
testVariable()
|
||||
testNode()
|
||||
testForNode()
|
||||
testIfNode()
|
||||
testNowNode()
|
||||
testInclude()
|
||||
testInheritence()
|
||||
testStencil()
|
||||
225
docs/Makefile
Normal file
225
docs/Makefile
Normal 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
33
docs/_templates/sidebar_intro.html
vendored
Normal 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
51
docs/api/context.rst
Normal file
@@ -0,0 +1,51 @@
|
||||
Context
|
||||
=======
|
||||
|
||||
A Context is a structure containing any templates you would like to use in a
|
||||
template. It’s somewhat like a dictionary, however you can push and pop to
|
||||
scope variables. So that means that when iterating over a for loop, you can
|
||||
push a new scope into the context to store any variables local to the scope.
|
||||
|
||||
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
257
docs/builtins.rst
Normal 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
341
docs/conf.py
Normal 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
|
||||
68
docs/custom-template-tags-and-filters.rst
Normal file
68
docs/custom-template-tags-and-filters.rst
Normal 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
48
docs/index.rst
Normal 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
77
docs/templates.rst
Normal 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 #}
|
||||
Reference in New Issue
Block a user