Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65a461d0a1 | ||
|
|
89256b96f4 | ||
|
|
a8d680b30e | ||
|
|
6f48fe2d91 | ||
|
|
4f9063c147 | ||
|
|
5541eae818 | ||
|
|
d1717df6ff | ||
|
|
dc8759ba34 | ||
|
|
233dcfc59a | ||
|
|
19e4f6e506 | ||
|
|
e2f33d4337 | ||
|
|
20b9476e4b | ||
|
|
cf9714ffd0 | ||
|
|
5e78d6cc46 | ||
|
|
99efba56e9 | ||
|
|
39517b7514 | ||
|
|
fdde1dec02 | ||
|
|
8f6b403aa9 | ||
|
|
2331b11a52 | ||
|
|
26f30cbd9d | ||
|
|
d7b152089e | ||
|
|
d3706f074d | ||
|
|
aa7c36296b | ||
|
|
2d507e7c11 | ||
|
|
15facd97fb | ||
|
|
d75db241ac | ||
|
|
49936c36d4 | ||
|
|
2e04a71d59 | ||
|
|
6d05832997 | ||
|
|
6871387671 | ||
|
|
46bc1242f3 | ||
|
|
24359489ce | ||
|
|
f90597fba1 | ||
|
|
9e2a061795 | ||
|
|
2be672c6a5 | ||
|
|
2ebb79df8b | ||
|
|
63c2b935f7 |
@@ -1 +1 @@
|
||||
3.0.1
|
||||
3.1
|
||||
|
||||
175
ARCHITECTURE.md
175
ARCHITECTURE.md
@@ -1,175 +0,0 @@
|
||||
Stencil Architecture
|
||||
====================
|
||||
|
||||
This document outlines the architecture of Stencil and how it works internally.
|
||||
|
||||
Stencil uses a three-step process for rendering templates. The first step is tokenising the template into an array of Token’s. Afterwards, the array of token’s are transformed into a collection of Node’s. Once we have a collection of Node’s (objects conforming to the `Node` protocol), we then call `render(context)` on each Node instructing it to render itself inside the given context.
|
||||
|
||||
## Token
|
||||
|
||||
Token is an enum which has four members. These represent a piece of text, a variable, a comment or a template block. They are parsed using the `TokenParser` which takes the template as a string as input and returns an array of Token’s.
|
||||
|
||||
### Values
|
||||
|
||||
#### Text
|
||||
|
||||
A text token represents a string which will be rendered in the template. For example, a text token with the string `Hello World` will be rendered as such in the output.
|
||||
|
||||
#### Variable
|
||||
|
||||
A variable token represents a variable inside a context. It will be evaluated and rendered in the output. It is created from the template using `{{ string }}`.
|
||||
|
||||
#### Comment
|
||||
|
||||
The comment token represents a comment inside the source. It is created using `{# This is a comment #}`.
|
||||
|
||||
#### Block
|
||||
|
||||
A block represents a template tag. It is created using `{% this is a template block %}` inside a template. The template tag in this case would be called `this`. See “Block Token” below for more information.
|
||||
|
||||
### Parsing
|
||||
|
||||
A template is parsed using the TokenParser into an array of Token’s. For example:
|
||||
|
||||
```html+django
|
||||
Hello {{ name }}
|
||||
```
|
||||
|
||||
Would be parsed into two tokens. A token representing the string, `Hello ` and a token representing the variable called `name`. So, in Swift it would be represented as follows:
|
||||
|
||||
```swift
|
||||
let tokens = [
|
||||
Token.Text("Hello "),
|
||||
Token.Variable("name"),
|
||||
]
|
||||
```
|
||||
|
||||
## Node
|
||||
|
||||
Node is a protocol with a single method, to render it inside a context. When rendering a node, it is converted into the output string, or an error if there is a failure. Token’s are converted to Node’s using the `TokenParser` class.
|
||||
|
||||
For some Token’s, there is a direct mapping from a Token to a Node. However block node’s do not have a 1:1 mapping.
|
||||
|
||||
### Token Parsing
|
||||
|
||||
#### Text Token
|
||||
|
||||
A text token is converted directly to a `TextNode` which simply returns the text when rendered.
|
||||
|
||||
#### Variable Token
|
||||
|
||||
Variable Token’s are transformed directly to a `VariableNode`, which will evaluate a variable in the given template when rendered.
|
||||
|
||||
#### Comment Token
|
||||
|
||||
A comment token is simply omitted, a comment token will be dropped when it is converted to a Node.
|
||||
|
||||
#### Block Token
|
||||
|
||||
Block token’s are slightly different from the other tokens, there is no direct mapping. A block token is made up of a string representing the token. For example `now` or `for article in articles`. The `TokenParser` will pull out the first word inside the string representation and use that to look-up a parser for the block. So, in this example, the template tag names will be `now` or `for`.
|
||||
|
||||
The template tag’s are registered with a block of code which deals with the parsing for the given tag. This allows the parser to parse a set of tokens ahead of the block tone. This is useful for control flow, such as the `for` template tag will want to parse any following tokens up until the `endblock` block token.
|
||||
|
||||
For example:
|
||||
|
||||
```html+django
|
||||
{% for article in articles %}
|
||||
An Article
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
Or as a set of tokens:
|
||||
|
||||
```swift
|
||||
let tokens = [
|
||||
Token.Block("for article in articles"),
|
||||
Token.Text(" An Article")
|
||||
Token.Block("endfor")
|
||||
]
|
||||
```
|
||||
|
||||
Will result in a single Node (a `ForNode`) which contains the sub-node containing the text. The `ForNode` class has a property called `forNodes` which contains the text node representing the text token (` An Article`).
|
||||
|
||||
When the `ForNode` is rendered in a context, it will look up the variable `articles` and if it’s an array it will loop over it. Inserting the variable `article` into the context while rendered the `forNodes` for each article.
|
||||
|
||||
### Custom Nodes
|
||||
|
||||
There are two ways to register custom template tags. A simple way which allows you to map 1:1 a block token to a Node. You can also register a more advanced template tag which has it’s own block of code for handling parsing if you want to parse up until another token such as if you are trying to provide flow-control.
|
||||
|
||||
The tags are registered with a `Namespace` passed when rendering your `Template`.
|
||||
|
||||
#### Simple Tags
|
||||
|
||||
A simple tag is registered with a string for the tag name and a block of code which is evaluated when the block is rendered in a given context.
|
||||
|
||||
Here’s an example. Registering a template tag called `custom` which just renders `Hello World` in the rendered template:
|
||||
|
||||
```swift
|
||||
namespace.registerSimpleTag("custom") { context in
|
||||
return "Hello World"
|
||||
}
|
||||
```
|
||||
|
||||
You would use it as such in a template:
|
||||
|
||||
```html+django
|
||||
{% custom %}
|
||||
```
|
||||
|
||||
#### Advanced Tags
|
||||
|
||||
If you need more control or functionality than the simple tag’s above, you can use the node based API where you can provide a block of code to deal with parsing. There are a few examples of this in use over at `Node.swift` inside Stencil. There is an implementation of `if` and `for` template tags.
|
||||
|
||||
You would register a template tag using the `registerTag` API inside a `Namespace` which accepts a name for the tag and a block of code to handle parsing. The block of code is invoked with the parser and the current token as an argument. This allows you to use the API on `TokenParser` to parse node’s further in the token array.
|
||||
|
||||
As an example, we’re going to create a template tag called `debug` which will optionally render nodes from `debug` up until `enddebug`. When rendering the `DebugNode`, it will only render the nodes inside if a variable called `debug` is set to `true` inside the template Context.
|
||||
|
||||
```html+django
|
||||
{% debug %}
|
||||
Debugging is enabled!
|
||||
{% enddebug %}
|
||||
```
|
||||
|
||||
This will be represented by a `DebugNode` which will have a property containing all of the Node’s inside the `debug`/`enddebug` block. In the above example, this will just be a TextNode containing ` Debugging is enabled!`.
|
||||
|
||||
When the `DebugNode` is rendered, it will determine if debug is enabled by introspecting the context and if it is enabled. We will render the nodes, otherwise just return an empty string to hide the debug output.
|
||||
|
||||
So, our `DebugNode` would look like as following:
|
||||
|
||||
```swift
|
||||
class DebugNode : Node {
|
||||
let nodes:[Node]
|
||||
|
||||
init(nodes:[Node]) {
|
||||
self.nodes = nodes
|
||||
}
|
||||
|
||||
func render(context: Context) throws -> String {
|
||||
// Is there a debug variable inside the context?
|
||||
if let debug = context["debug"] as? Bool {
|
||||
// Is debug set to true?
|
||||
if debug {
|
||||
// Let's render the given nodes in the context since debug is enabled.
|
||||
return renderNodes(nodes, context)
|
||||
}
|
||||
}
|
||||
|
||||
// Debug is turned off, so let's not render anything
|
||||
return ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We will need to write a parser to parse up until the `enddebug` template block and create a `DebugNode` with the nodes in-between. If there was another error form another Node inside, then we will return that error.
|
||||
|
||||
```swift
|
||||
namespace.registerTag("debug") { parser, token in
|
||||
// Use the parser to parse every token up until the `enddebug` block.
|
||||
let nodes = try until(["enddebug"]))
|
||||
return DebugNode(nodes)
|
||||
}
|
||||
```
|
||||
|
||||
## Context
|
||||
|
||||
A Context is a structure containing any templates you would like to use in a template. 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.
|
||||
112
CHANGELOG.md
112
CHANGELOG.md
@@ -1,5 +1,117 @@
|
||||
# Stencil Changelog
|
||||
|
||||
## 0.9.0
|
||||
|
||||
### Enhancements
|
||||
|
||||
- `for` block now can contain `where` expression to filter array items. For example `{% for item in items where item > 1 %}` is now supported.
|
||||
- `if` blocks may now contain else if (`elif`) conditions.
|
||||
|
||||
```html+django
|
||||
{% if one or two and not three %}
|
||||
one or two but not three
|
||||
{% elif four %}
|
||||
four
|
||||
{% else %}
|
||||
not one, two, or four
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
- `for` block now allows you to iterate over array of tuples or dictionaries.
|
||||
|
||||
```html+django
|
||||
{% for key, value in thing %}
|
||||
<li>{{ key }}: {{ value }}</li>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- You can now use literal filter arguments which contain quotes.
|
||||
[#98](https://github.com/kylef/Stencil/pull/98)
|
||||
|
||||
|
||||
## 0.8.0
|
||||
|
||||
### Breaking
|
||||
|
||||
- It is no longer possible to create `Context` objects. Instead, you can pass a
|
||||
dictionary directly to a `Template`s `render` method.
|
||||
|
||||
```diff
|
||||
- try template.render(Context(dictionary: ["name": "Kyle"]))
|
||||
+ try template.render(["name": "Kyle"])
|
||||
```
|
||||
|
||||
- Template loader are no longer passed into a `Context`, instead you will need
|
||||
to pass the `Loader` to an `Environment` and create a template from the
|
||||
`Environment`.
|
||||
|
||||
```diff
|
||||
let loader = FileSystemLoader(paths: ["templates/"])
|
||||
|
||||
- let template = loader.loadTemplate(name: "index.html")
|
||||
- try template.render(Context(dictionary: ["loader": loader]))
|
||||
+ let environment = Environment(loader: loader)
|
||||
+ try environment.renderTemplate(name: "index.html")
|
||||
```
|
||||
|
||||
- `Loader`s will now throw a `TemplateDoesNotExist` error when a template
|
||||
is not found.
|
||||
|
||||
- `Namespace` has been removed and replaced by extensions. You can create an
|
||||
extension including any custom template tags and filters. A collection of
|
||||
extensions can be passed to an `Environment`.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- `Environment` is a new way to load templates. You can configure an
|
||||
environment with custom template filters, tags and loaders and then create a
|
||||
template from an environment.
|
||||
|
||||
Environment also provides a convenience method to render a template directly.
|
||||
|
||||
- `FileSystemLoader` will now ensure that template paths are within the base
|
||||
path. Any template names that try to escape the base path will raise a
|
||||
`SuspiciousFileOperation` error.
|
||||
|
||||
- New `{% filter %}` tag allowing you to perform a filter across the contents
|
||||
of a block.
|
||||
|
||||
```html+django
|
||||
{% filter lowercase %}
|
||||
This Text Will Be Lowercased.
|
||||
{% endfilter %}
|
||||
```
|
||||
|
||||
- You can now use `{{ block.super }}` to render a super block from another `{%
|
||||
block %}`.
|
||||
|
||||
- `Environment` allows you to provide a custom `Template` subclass, allowing
|
||||
new template to use a specific subclass.
|
||||
|
||||
- If expressions may now contain filters on variables. For example
|
||||
`{% if name|uppercase == "TEST" %}` is now supported.
|
||||
|
||||
### Deprecations
|
||||
|
||||
- `Template` initialisers have been deprecated in favour of using a template
|
||||
loader such as `FileSystemLoader` inside an `Environment`.
|
||||
|
||||
- The use of whitespace inside variable filter expression is now deprecated.
|
||||
|
||||
```diff
|
||||
- {{ name | uppercase }}
|
||||
+ {{ name|uppercase }}
|
||||
```
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Restores compatibility with ARM based platforms such as iOS. Stencil 0.7
|
||||
introduced compilation errors due to using the `Float80` type which is not
|
||||
available.
|
||||
|
||||
|
||||
## 0.7.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -3,9 +3,9 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "Stencil",
|
||||
dependencies: [
|
||||
.Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 7),
|
||||
.Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 8),
|
||||
|
||||
// https://github.com/apple/swift-package-manager/pull/597
|
||||
.Package(url: "https://github.com/kylef/Spectre", majorVersion: 0, minor: 7),
|
||||
.Package(url: "https://github.com/kylef/Spectre.git", majorVersion: 0, minor: 7),
|
||||
]
|
||||
)
|
||||
|
||||
35
README.md
35
README.md
@@ -19,35 +19,24 @@ There are {{ articles.count }} articles.
|
||||
```
|
||||
|
||||
```swift
|
||||
import Stencil
|
||||
|
||||
struct Article {
|
||||
let title: String
|
||||
let author: String
|
||||
}
|
||||
|
||||
let context = Context(dictionary: [
|
||||
let context = [
|
||||
"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)")
|
||||
}
|
||||
```
|
||||
let environment = Environment(loader: FileSystemLoader(paths: ["templates/"]))
|
||||
let rendered = try environment.renderTemplate(name: context)
|
||||
|
||||
## Installation
|
||||
|
||||
Installation with Swift Package Manager is recommended.
|
||||
|
||||
### CocoaPods
|
||||
|
||||
```ruby
|
||||
pod 'Stencil'
|
||||
print(rendered)
|
||||
```
|
||||
|
||||
## Philosophy
|
||||
@@ -62,8 +51,16 @@ Stencil follows the same philosophy of Django:
|
||||
|
||||
## The User Guide
|
||||
|
||||
- [Templates](http://stencil.fuller.li/en/latest/templates.html)
|
||||
Resources for Stencil template authors to write Stencil templates:
|
||||
|
||||
- [Language overview](http://stencil.fuller.li/en/latest/templates.html)
|
||||
- [Built-in template tags and filters](http://stencil.fuller.li/en/latest/builtins.html)
|
||||
|
||||
Resources to help you integrate Stencil into a Swift project:
|
||||
|
||||
- [Installation](http://stencil.fuller.li/en/latest/installation.html)
|
||||
- [Getting Started](http://stencil.fuller.li/en/latest/getting-started.html)
|
||||
- [API Reference](http://stencil.fuller.li/en/latest/api.html)
|
||||
- [Custom Template Tags and Filters](http://stencil.fuller.li/en/latest/custom-template-tags-and-filters.html)
|
||||
|
||||
## License
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
/// A container for template variables.
|
||||
public class Context {
|
||||
var dictionaries: [[String: Any?]]
|
||||
let namespace: Namespace
|
||||
|
||||
/// Initialise a Context with an optional dictionary and optional namespace
|
||||
public init(dictionary: [String: Any]? = nil, namespace: Namespace = Namespace()) {
|
||||
public let environment: Environment
|
||||
|
||||
init(dictionary: [String: Any]? = nil, environment: Environment? = nil) {
|
||||
if let dictionary = dictionary {
|
||||
dictionaries = [dictionary]
|
||||
} else {
|
||||
dictionaries = []
|
||||
}
|
||||
|
||||
self.namespace = namespace
|
||||
self.environment = environment ?? Environment()
|
||||
}
|
||||
|
||||
public subscript(key: String) -> Any? {
|
||||
@@ -42,7 +42,7 @@ public class Context {
|
||||
}
|
||||
|
||||
/// Pop the last level off of the Context
|
||||
fileprivate func pop() -> [String: Any]? {
|
||||
fileprivate func pop() -> [String: Any?]? {
|
||||
return dictionaries.popLast()
|
||||
}
|
||||
|
||||
|
||||
38
Sources/Environment.swift
Normal file
38
Sources/Environment.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
public struct Environment {
|
||||
public let templateClass: Template.Type
|
||||
public let extensions: [Extension]
|
||||
|
||||
public var loader: Loader?
|
||||
|
||||
public init(loader: Loader? = nil, extensions: [Extension]? = nil, templateClass: Template.Type = Template.self) {
|
||||
self.templateClass = templateClass
|
||||
self.loader = loader
|
||||
self.extensions = (extensions ?? []) + [DefaultExtension()]
|
||||
}
|
||||
|
||||
public func loadTemplate(name: String) throws -> Template {
|
||||
if let loader = loader {
|
||||
return try loader.loadTemplate(name: name, environment: self)
|
||||
} else {
|
||||
throw TemplateDoesNotExist(templateNames: [name], loader: nil)
|
||||
}
|
||||
}
|
||||
|
||||
public func loadTemplate(names: [String]) throws -> Template {
|
||||
if let loader = loader {
|
||||
return try loader.loadTemplate(names: names, environment: self)
|
||||
} else {
|
||||
throw TemplateDoesNotExist(templateNames: names, loader: nil)
|
||||
}
|
||||
}
|
||||
|
||||
public func renderTemplate(name: String, context: [String: Any]? = nil) throws -> String {
|
||||
let template = try loadTemplate(name: name)
|
||||
return try template.render(context)
|
||||
}
|
||||
|
||||
public func renderTemplate(string: String, context: [String: Any]? = nil) throws -> String {
|
||||
let template = templateClass.init(templateString: string, environment: self)
|
||||
return try template.render(context)
|
||||
}
|
||||
}
|
||||
19
Sources/Errors.swift
Normal file
19
Sources/Errors.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
public class TemplateDoesNotExist: Error, CustomStringConvertible {
|
||||
let templateNames: [String]
|
||||
let loader: Loader?
|
||||
|
||||
public init(templateNames: [String], loader: Loader? = nil) {
|
||||
self.templateNames = templateNames
|
||||
self.loader = loader
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
let templates = templateNames.joined(separator: ", ")
|
||||
|
||||
if let loader = loader {
|
||||
return "Template named `\(templates)` does not exist in loader \(loader)"
|
||||
}
|
||||
|
||||
return "Template named `\(templates)` does not exist. No loaders found"
|
||||
}
|
||||
}
|
||||
@@ -31,18 +31,18 @@ final class StaticExpression: Expression, CustomStringConvertible {
|
||||
|
||||
|
||||
final class VariableExpression: Expression, CustomStringConvertible {
|
||||
let variable: Variable
|
||||
let variable: Resolvable
|
||||
|
||||
init(variable: Variable) {
|
||||
init(variable: Resolvable) {
|
||||
self.variable = variable
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "(variable: \(variable.variable))"
|
||||
return "(variable: \(variable))"
|
||||
}
|
||||
|
||||
/// Resolves a variable in the given context as boolean
|
||||
func resolve(context: Context, variable: Variable) throws -> Bool {
|
||||
func resolve(context: Context, variable: Resolvable) throws -> Bool {
|
||||
let result = try variable.resolve(context)
|
||||
var truthy = false
|
||||
|
||||
@@ -202,7 +202,7 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
return ""
|
||||
}
|
||||
|
||||
func compare(lhs: Float80, rhs: Float80) -> Bool {
|
||||
func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -213,7 +213,7 @@ class MoreThanExpression: NumericExpression {
|
||||
return ">"
|
||||
}
|
||||
|
||||
override func compare(lhs: Float80, rhs: Float80) -> Bool {
|
||||
override func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
return lhs > rhs
|
||||
}
|
||||
}
|
||||
@@ -224,7 +224,7 @@ class MoreThanEqualExpression: NumericExpression {
|
||||
return ">="
|
||||
}
|
||||
|
||||
override func compare(lhs: Float80, rhs: Float80) -> Bool {
|
||||
override func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
return lhs >= rhs
|
||||
}
|
||||
}
|
||||
@@ -235,7 +235,7 @@ class LessThanExpression: NumericExpression {
|
||||
return "<"
|
||||
}
|
||||
|
||||
override func compare(lhs: Float80, rhs: Float80) -> Bool {
|
||||
override func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
return lhs < rhs
|
||||
}
|
||||
}
|
||||
@@ -246,7 +246,7 @@ class LessThanEqualExpression: NumericExpression {
|
||||
return "<="
|
||||
}
|
||||
|
||||
override func compare(lhs: Float80, rhs: Float80) -> Bool {
|
||||
override func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
return lhs <= rhs
|
||||
}
|
||||
}
|
||||
@@ -263,37 +263,37 @@ class InequalityExpression: EqualityExpression {
|
||||
}
|
||||
|
||||
|
||||
func toNumber(value: Any) -> Float80? {
|
||||
func toNumber(value: Any) -> Number? {
|
||||
if let value = value as? Float {
|
||||
return Float80(value)
|
||||
return Number(value)
|
||||
} else if let value = value as? Double {
|
||||
return Float80(value)
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt {
|
||||
return Float80(value)
|
||||
return Number(value)
|
||||
} else if let value = value as? Int {
|
||||
return Float80(value)
|
||||
return Number(value)
|
||||
} else if let value = value as? Int8 {
|
||||
return Float80(value)
|
||||
return Number(value)
|
||||
} else if let value = value as? Int16 {
|
||||
return Float80(value)
|
||||
return Number(value)
|
||||
} else if let value = value as? Int32 {
|
||||
return Float80(value)
|
||||
return Number(value)
|
||||
} else if let value = value as? Int64 {
|
||||
return Float80(value)
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt8 {
|
||||
return Float80(value)
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt16 {
|
||||
return Float80(value)
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt32 {
|
||||
return Float80(value)
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt64 {
|
||||
return Float80(value)
|
||||
} else if let value = value as? Float80 {
|
||||
return Number(value)
|
||||
} else if let value = value as? Number {
|
||||
return value
|
||||
} else if let value = value as? Float64 {
|
||||
return Float80(value)
|
||||
return Number(value)
|
||||
} else if let value = value as? Float32 {
|
||||
return Float80(value)
|
||||
return Number(value)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,3 +1,66 @@
|
||||
open class Extension {
|
||||
typealias TagParser = (TokenParser, Token) throws -> NodeType
|
||||
var tags = [String: TagParser]()
|
||||
|
||||
var filters = [String: Filter]()
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
/// Registers a new template tag
|
||||
public func registerTag(_ name: String, parser: @escaping (TokenParser, Token) throws -> NodeType) {
|
||||
tags[name] = parser
|
||||
}
|
||||
|
||||
/// Registers a simple template tag with a name and a handler
|
||||
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: @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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class DefaultExtension: Extension {
|
||||
override init() {
|
||||
super.init()
|
||||
registerDefaultTags()
|
||||
registerDefaultFilters()
|
||||
}
|
||||
|
||||
fileprivate func registerDefaultTags() {
|
||||
registerTag("for", parser: ForNode.parse)
|
||||
registerTag("if", parser: IfNode.parse)
|
||||
registerTag("ifnot", parser: IfNode.parse_ifnot)
|
||||
#if !os(Linux)
|
||||
registerTag("now", parser: NowNode.parse)
|
||||
#endif
|
||||
registerTag("include", parser: IncludeNode.parse)
|
||||
registerTag("extends", parser: ExtendsNode.parse)
|
||||
registerTag("block", parser: BlockNode.parse)
|
||||
registerTag("filter", parser: FilterNode.parse)
|
||||
}
|
||||
|
||||
fileprivate func registerDefaultFilters() {
|
||||
registerFilter("default", filter: defaultFilter)
|
||||
registerFilter("capitalize", filter: capitalise)
|
||||
registerFilter("uppercase", filter: uppercase)
|
||||
registerFilter("lowercase", filter: lowercase)
|
||||
registerFilter("join", filter: joinFilter)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protocol FilterType {
|
||||
func invoke(value: Any?, arguments: [Any?]) throws -> Any?
|
||||
}
|
||||
@@ -19,58 +82,3 @@ enum Filter: FilterType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class Namespace {
|
||||
public typealias TagParser = (TokenParser, Token) throws -> NodeType
|
||||
|
||||
var tags = [String: TagParser]()
|
||||
var filters = [String: Filter]()
|
||||
|
||||
public init() {
|
||||
registerDefaultTags()
|
||||
registerDefaultFilters()
|
||||
}
|
||||
|
||||
fileprivate func registerDefaultTags() {
|
||||
registerTag("for", parser: ForNode.parse)
|
||||
registerTag("if", parser: IfNode.parse)
|
||||
registerTag("ifnot", parser: IfNode.parse_ifnot)
|
||||
#if !os(Linux)
|
||||
registerTag("now", parser: NowNode.parse)
|
||||
#endif
|
||||
registerTag("include", parser: IncludeNode.parse)
|
||||
registerTag("extends", parser: ExtendsNode.parse)
|
||||
registerTag("block", parser: BlockNode.parse)
|
||||
}
|
||||
|
||||
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: @escaping TagParser) {
|
||||
tags[name] = parser
|
||||
}
|
||||
|
||||
/// Registers a simple template tag with a name and a handler
|
||||
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: @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)
|
||||
}
|
||||
}
|
||||
35
Sources/FilterTag.swift
Normal file
35
Sources/FilterTag.swift
Normal file
@@ -0,0 +1,35 @@
|
||||
class FilterNode : NodeType {
|
||||
let resolvable: Resolvable
|
||||
let nodes: [NodeType]
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
|
||||
guard bits.count == 2 else {
|
||||
throw TemplateSyntaxError("'filter' tag takes one argument, the filter expression")
|
||||
}
|
||||
|
||||
let blocks = try parser.parse(until(["endfilter"]))
|
||||
|
||||
guard parser.nextToken() != nil else {
|
||||
throw TemplateSyntaxError("`endfilter` was not found.")
|
||||
}
|
||||
|
||||
let resolvable = try parser.compileFilter("filter_value|\(bits[1])")
|
||||
return FilterNode(nodes: blocks, resolvable: resolvable)
|
||||
}
|
||||
|
||||
init(nodes: [NodeType], resolvable: Resolvable) {
|
||||
self.nodes = nodes
|
||||
self.resolvable = resolvable
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
let value = try renderNodes(nodes, context)
|
||||
|
||||
return try context.push(dictionary: ["filter_value": value]) {
|
||||
return try VariableNode(variable: resolvable).render(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,13 @@
|
||||
func toString(_ value: Any?) -> String? {
|
||||
if let value = value as? String {
|
||||
return value
|
||||
} else if let value = value as? CustomStringConvertible {
|
||||
return value.description
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func capitalise(_ value: Any?) -> Any? {
|
||||
if let value = toString(value) {
|
||||
return value.capitalized
|
||||
}
|
||||
|
||||
return value
|
||||
return stringify(value).capitalized
|
||||
}
|
||||
|
||||
func uppercase(_ value: Any?) -> Any? {
|
||||
if let value = toString(value) {
|
||||
return value.uppercased()
|
||||
}
|
||||
|
||||
return value
|
||||
return stringify(value).uppercased()
|
||||
}
|
||||
|
||||
func lowercase(_ value: Any?) -> Any? {
|
||||
if let value = toString(value) {
|
||||
return value.lowercased()
|
||||
}
|
||||
|
||||
return value
|
||||
return stringify(value).lowercased()
|
||||
}
|
||||
|
||||
func defaultFilter(value: Any?, arguments: [Any?]) -> Any? {
|
||||
@@ -47,17 +25,17 @@ func defaultFilter(value: Any?, arguments: [Any?]) -> Any? {
|
||||
}
|
||||
|
||||
func joinFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
guard arguments.count == 1 else {
|
||||
guard arguments.count < 2 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")
|
||||
let separator = stringify(arguments.first ?? "")
|
||||
|
||||
if let value = value as? [Any] {
|
||||
return value
|
||||
.map(stringify)
|
||||
.joined(separator: separator)
|
||||
}
|
||||
|
||||
if let value = value as? [String] {
|
||||
return value.joined(separator: separator)
|
||||
}
|
||||
|
||||
return nil
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import Foundation
|
||||
|
||||
class ForNode : NodeType {
|
||||
let resolvable: Resolvable
|
||||
let loopVariable:String
|
||||
let loopVariables: [String]
|
||||
let nodes:[NodeType]
|
||||
let emptyNodes: [NodeType]
|
||||
let `where`: Expression?
|
||||
|
||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
|
||||
guard components.count == 4 && components[2] == "in" else {
|
||||
throw TemplateSyntaxError("'for' statements should use the following 'for x in y' `\(token.contents)`.")
|
||||
guard components.count >= 2 && components[2] == "in" &&
|
||||
(components.count == 4 || (components.count >= 6 && components[4] == "where")) else {
|
||||
throw TemplateSyntaxError("'for' statements should use the following 'for x in y where condition' `\(token.contents)`.")
|
||||
}
|
||||
|
||||
let loopVariable = components[1]
|
||||
let loopVariables = components[1].characters
|
||||
.split(separator: ",")
|
||||
.map(String.init)
|
||||
.map { $0.trimmingCharacters(in: CharacterSet.whitespaces) }
|
||||
|
||||
let variable = components[3]
|
||||
|
||||
var emptyNodes = [NodeType]()
|
||||
@@ -28,21 +36,75 @@ class ForNode : NodeType {
|
||||
}
|
||||
|
||||
let filter = try parser.compileFilter(variable)
|
||||
return ForNode(resolvable: filter, loopVariable: loopVariable, nodes: forNodes, emptyNodes:emptyNodes)
|
||||
let `where`: Expression?
|
||||
if components.count >= 6 {
|
||||
`where` = try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser)
|
||||
} else {
|
||||
`where` = nil
|
||||
}
|
||||
return ForNode(resolvable: filter, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`)
|
||||
}
|
||||
|
||||
init(resolvable: Resolvable, loopVariable:String, nodes:[NodeType], emptyNodes:[NodeType]) {
|
||||
init(resolvable: Resolvable, loopVariables: [String], nodes:[NodeType], emptyNodes:[NodeType], where: Expression? = nil) {
|
||||
self.resolvable = resolvable
|
||||
self.loopVariable = loopVariable
|
||||
self.loopVariables = loopVariables
|
||||
self.nodes = nodes
|
||||
self.emptyNodes = emptyNodes
|
||||
self.where = `where`
|
||||
}
|
||||
|
||||
func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) rethrows -> Result {
|
||||
if loopVariables.isEmpty {
|
||||
return try context.push() {
|
||||
return try closure()
|
||||
}
|
||||
}
|
||||
|
||||
if let value = value as? (Any, Any) {
|
||||
let first = loopVariables[0]
|
||||
|
||||
if loopVariables.count == 2 {
|
||||
let second = loopVariables[1]
|
||||
|
||||
return try context.push(dictionary: [first: value.0, second: value.1]) {
|
||||
return try closure()
|
||||
}
|
||||
}
|
||||
|
||||
return try context.push(dictionary: [first: value.0]) {
|
||||
return try closure()
|
||||
}
|
||||
}
|
||||
|
||||
return try context.push(dictionary: [loopVariables.first!: value]) {
|
||||
return try closure()
|
||||
}
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
let values = try resolvable.resolve(context)
|
||||
let resolved = try resolvable.resolve(context)
|
||||
|
||||
if let values = values as? [Any] , values.count > 0 {
|
||||
var values: [Any]
|
||||
|
||||
if let dictionary = resolved as? [String: Any], !dictionary.isEmpty {
|
||||
values = dictionary.map { ($0.key, $0.value) }
|
||||
} else if let array = resolved as? [Any] {
|
||||
values = array
|
||||
} else {
|
||||
values = []
|
||||
}
|
||||
|
||||
if let `where` = self.where {
|
||||
values = try values.filter({ item -> Bool in
|
||||
return try push(value: item, context: context) {
|
||||
try `where`.evaluate(context: context)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if !values.isEmpty {
|
||||
let count = values.count
|
||||
|
||||
return try values.enumerated().map { index, item in
|
||||
let forContext: [String: Any] = [
|
||||
"first": index == 0,
|
||||
@@ -50,8 +112,10 @@ class ForNode : NodeType {
|
||||
"counter": index + 1,
|
||||
]
|
||||
|
||||
return try context.push(dictionary: [loopVariable: item, "forloop": forContext]) {
|
||||
try renderNodes(nodes, context)
|
||||
return try context.push(dictionary: ["forloop": forContext]) {
|
||||
return try push(value: item, context: context) {
|
||||
try renderNodes(nodes, context)
|
||||
}
|
||||
}
|
||||
}.joined(separator: "")
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func findOperator(name: String) -> Operator? {
|
||||
enum IfToken {
|
||||
case infix(name: String, bindingPower: Int, op: InfixOperator.Type)
|
||||
case prefix(name: String, bindingPower: Int, op: PrefixOperator.Type)
|
||||
case variable(Variable)
|
||||
case variable(Resolvable)
|
||||
case end
|
||||
|
||||
var bindingPower: Int {
|
||||
@@ -99,8 +99,8 @@ final class IfExpressionParser {
|
||||
let tokens: [IfToken]
|
||||
var position: Int = 0
|
||||
|
||||
init(components: [String]) {
|
||||
self.tokens = components.map { component in
|
||||
init(components: [String], tokenParser: TokenParser) throws {
|
||||
self.tokens = try components.map { component in
|
||||
if let op = findOperator(name: component) {
|
||||
switch op {
|
||||
case .infix(let name, let bindingPower, let cls):
|
||||
@@ -110,7 +110,7 @@ final class IfExpressionParser {
|
||||
}
|
||||
}
|
||||
|
||||
return .variable(Variable(component))
|
||||
return .variable(try tokenParser.compileFilter(component))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,36 +154,65 @@ final class IfExpressionParser {
|
||||
}
|
||||
|
||||
|
||||
func parseExpression(components: [String]) throws -> Expression {
|
||||
let parser = IfExpressionParser(components: components)
|
||||
func parseExpression(components: [String], tokenParser: TokenParser) throws -> Expression {
|
||||
let parser = try IfExpressionParser(components: components, tokenParser: tokenParser)
|
||||
return try parser.parse()
|
||||
}
|
||||
|
||||
|
||||
/// Represents an if condition and the associated nodes when the condition
|
||||
/// evaluates
|
||||
final class IfCondition {
|
||||
let expression: Expression?
|
||||
let nodes: [NodeType]
|
||||
|
||||
init(expression: Expression?, nodes: [NodeType]) {
|
||||
self.expression = expression
|
||||
self.nodes = nodes
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
return try context.push {
|
||||
return try renderNodes(nodes, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class IfNode : NodeType {
|
||||
let expression: Expression
|
||||
let trueNodes: [NodeType]
|
||||
let falseNodes: [NodeType]
|
||||
let conditions: [IfCondition]
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
var components = token.components()
|
||||
components.removeFirst()
|
||||
var trueNodes = [NodeType]()
|
||||
var falseNodes = [NodeType]()
|
||||
|
||||
trueNodes = try parser.parse(until(["endif", "else"]))
|
||||
let expression = try parseExpression(components: components, tokenParser: parser)
|
||||
let nodes = try parser.parse(until(["endif", "elif", "else"]))
|
||||
var conditions: [IfCondition] = [
|
||||
IfCondition(expression: expression, nodes: nodes)
|
||||
]
|
||||
|
||||
guard let token = parser.nextToken() else {
|
||||
var token = parser.nextToken()
|
||||
while let current = token, current.contents.hasPrefix("elif") {
|
||||
var components = current.components()
|
||||
components.removeFirst()
|
||||
let expression = try parseExpression(components: components, tokenParser: parser)
|
||||
|
||||
let nodes = try parser.parse(until(["endif", "elif", "else"]))
|
||||
token = parser.nextToken()
|
||||
conditions.append(IfCondition(expression: expression, nodes: nodes))
|
||||
}
|
||||
|
||||
if let current = token, current.contents == "else" {
|
||||
conditions.append(IfCondition(expression: nil, nodes: try parser.parse(until(["endif"]))))
|
||||
token = parser.nextToken()
|
||||
}
|
||||
|
||||
guard let current = token, current.contents == "endif" else {
|
||||
throw TemplateSyntaxError("`endif` was not found.")
|
||||
}
|
||||
|
||||
if token.contents == "else" {
|
||||
falseNodes = try parser.parse(until(["endif"]))
|
||||
_ = parser.nextToken()
|
||||
}
|
||||
|
||||
let expression = try parseExpression(components: components)
|
||||
return IfNode(expression: expression, trueNodes: trueNodes, falseNodes: falseNodes)
|
||||
return IfNode(conditions: conditions)
|
||||
}
|
||||
|
||||
class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
@@ -206,25 +235,30 @@ class IfNode : NodeType {
|
||||
_ = parser.nextToken()
|
||||
}
|
||||
|
||||
let expression = try parseExpression(components: components)
|
||||
return IfNode(expression: expression, trueNodes: trueNodes, falseNodes: falseNodes)
|
||||
let expression = try parseExpression(components: components, tokenParser: parser)
|
||||
return IfNode(conditions: [
|
||||
IfCondition(expression: expression, nodes: trueNodes),
|
||||
IfCondition(expression: nil, nodes: falseNodes),
|
||||
])
|
||||
}
|
||||
|
||||
init(expression: Expression, trueNodes: [NodeType], falseNodes: [NodeType]) {
|
||||
self.expression = expression
|
||||
self.trueNodes = trueNodes
|
||||
self.falseNodes = falseNodes
|
||||
init(conditions: [IfCondition]) {
|
||||
self.conditions = conditions
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
let truthy = try expression.evaluate(context: context)
|
||||
for condition in conditions {
|
||||
if let expression = condition.expression {
|
||||
let truthy = try expression.evaluate(context: context)
|
||||
|
||||
return try context.push {
|
||||
if truthy {
|
||||
return try renderNodes(trueNodes, context)
|
||||
if truthy {
|
||||
return try condition.render(context)
|
||||
}
|
||||
} else {
|
||||
return try renderNodes(falseNodes, context)
|
||||
return try condition.render(context)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,19 +19,15 @@ class IncludeNode : NodeType {
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
guard let loader = context["loader"] as? Loader else {
|
||||
throw TemplateSyntaxError("Template loader not in context")
|
||||
}
|
||||
|
||||
guard let templateName = try self.templateName.resolve(context) as? String else {
|
||||
throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string")
|
||||
}
|
||||
|
||||
guard let template = try loader.loadTemplate(name: templateName) else {
|
||||
throw TemplateSyntaxError("'\(templateName)' template not found")
|
||||
}
|
||||
let template = try context.environment.loadTemplate(name: templateName)
|
||||
|
||||
return try template.render(context)
|
||||
return try context.push {
|
||||
return try template.render(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,17 +59,11 @@ class ExtendsNode : NodeType {
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
guard let loader = context["loader"] as? Loader else {
|
||||
throw TemplateSyntaxError("Template loader not in context")
|
||||
}
|
||||
|
||||
guard let templateName = try self.templateName.resolve(context) as? String else {
|
||||
throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string")
|
||||
}
|
||||
|
||||
guard let template = try loader.loadTemplate(name: templateName) else {
|
||||
throw TemplateSyntaxError("'\(templateName)' template not found")
|
||||
}
|
||||
let template = try context.environment.loadTemplate(name: templateName)
|
||||
|
||||
let blockContext: BlockContext
|
||||
if let context = context[BlockContext.contextKey] as? BlockContext {
|
||||
@@ -115,7 +109,9 @@ class BlockNode : NodeType {
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
if let blockContext = context[BlockContext.contextKey] as? BlockContext, let node = blockContext.pop(name) {
|
||||
return try node.render(context)
|
||||
return try context.push(dictionary: ["block": ["super": self]]) {
|
||||
return try node.render(context)
|
||||
}
|
||||
}
|
||||
|
||||
return try renderNodes(nodes, context)
|
||||
|
||||
103
Sources/Loader.swift
Normal file
103
Sources/Loader.swift
Normal file
@@ -0,0 +1,103 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
|
||||
public protocol Loader {
|
||||
func loadTemplate(name: String, environment: Environment) throws -> Template
|
||||
func loadTemplate(names: [String], environment: Environment) throws -> Template
|
||||
}
|
||||
|
||||
|
||||
extension Loader {
|
||||
public func loadTemplate(names: [String], environment: Environment) throws -> Template {
|
||||
for name in names {
|
||||
do {
|
||||
return try loadTemplate(name: name, environment: environment)
|
||||
} catch is TemplateDoesNotExist {
|
||||
continue
|
||||
} catch {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
throw TemplateDoesNotExist(templateNames: names, loader: self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// A class for loading a template from disk
|
||||
public class FileSystemLoader: Loader, CustomStringConvertible {
|
||||
public let paths: [Path]
|
||||
|
||||
public init(paths: [Path]) {
|
||||
self.paths = paths
|
||||
}
|
||||
|
||||
public init(bundle: [Bundle]) {
|
||||
self.paths = bundle.map {
|
||||
return Path($0.bundlePath)
|
||||
}
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
return "FileSystemLoader(\(paths))"
|
||||
}
|
||||
|
||||
public func loadTemplate(name: String, environment: Environment) throws -> Template {
|
||||
for path in paths {
|
||||
let templatePath = try path.safeJoin(path: Path(name))
|
||||
|
||||
if !templatePath.exists {
|
||||
continue
|
||||
}
|
||||
|
||||
let content: String = try templatePath.read()
|
||||
return environment.templateClass.init(templateString: content, environment: environment, name: name)
|
||||
}
|
||||
|
||||
throw TemplateDoesNotExist(templateNames: [name], loader: self)
|
||||
}
|
||||
|
||||
public func loadTemplate(names: [String], environment: Environment) throws -> Template {
|
||||
for path in paths {
|
||||
for templateName in names {
|
||||
let templatePath = try path.safeJoin(path: Path(templateName))
|
||||
|
||||
if templatePath.exists {
|
||||
let content: String = try templatePath.read()
|
||||
return environment.templateClass.init(templateString: content, environment: environment, name: templateName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw TemplateDoesNotExist(templateNames: names, loader: self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Path {
|
||||
func safeJoin(path: Path) throws -> Path {
|
||||
let newPath = self + path
|
||||
|
||||
if !newPath.absolute().description.hasPrefix(absolute().description) {
|
||||
throw SuspiciousFileOperation(basePath: self, path: newPath)
|
||||
}
|
||||
|
||||
return newPath
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SuspiciousFileOperation: Error {
|
||||
let basePath: Path
|
||||
let path: Path
|
||||
|
||||
init(basePath: Path, path: Path) {
|
||||
self.basePath = basePath
|
||||
self.path = path
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "Path `\(path)` is located outside of base path `\(basePath)`"
|
||||
}
|
||||
}
|
||||
@@ -70,15 +70,19 @@ public class VariableNode : NodeType {
|
||||
|
||||
public func render(_ context: Context) throws -> String {
|
||||
let result = try variable.resolve(context)
|
||||
|
||||
if let result = result as? String {
|
||||
return result
|
||||
} else if let result = result as? CustomStringConvertible {
|
||||
return result.description
|
||||
} else if let result = result as? NSObject {
|
||||
return result.description
|
||||
}
|
||||
|
||||
return ""
|
||||
return stringify(result)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func stringify(_ result: Any?) -> String {
|
||||
if let result = result as? String {
|
||||
return result
|
||||
} else if let result = result as? CustomStringConvertible {
|
||||
return result.description
|
||||
} else if let result = result as? NSObject {
|
||||
return result.description
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ public class TokenParser {
|
||||
public typealias TagParser = (TokenParser, Token) throws -> NodeType
|
||||
|
||||
fileprivate var tokens: [Token]
|
||||
fileprivate let namespace: Namespace
|
||||
fileprivate let environment: Environment
|
||||
|
||||
public init(tokens: [Token], namespace: Namespace) {
|
||||
public init(tokens: [Token], environment: Environment) {
|
||||
self.tokens = tokens
|
||||
self.namespace = namespace
|
||||
self.environment = environment
|
||||
}
|
||||
|
||||
/// Parse the given tokens into nodes
|
||||
@@ -42,19 +42,14 @@ public class TokenParser {
|
||||
case .variable:
|
||||
nodes.append(VariableNode(variable: try compileFilter(token.contents)))
|
||||
case .block:
|
||||
let tag = token.components().first
|
||||
|
||||
if let parse_until = parse_until , parse_until(self, token) {
|
||||
prependToken(token)
|
||||
return nodes
|
||||
}
|
||||
|
||||
if let tag = tag {
|
||||
if let parser = namespace.tags[tag] {
|
||||
nodes.append(try parser(self, token))
|
||||
} else {
|
||||
throw TemplateSyntaxError("Unknown template tag '\(tag)'")
|
||||
}
|
||||
if let tag = token.components().first {
|
||||
let parser = try findTag(name: tag)
|
||||
nodes.append(try parser(self, token))
|
||||
}
|
||||
case .comment:
|
||||
continue
|
||||
@@ -76,15 +71,28 @@ public class TokenParser {
|
||||
tokens.insert(token, at: 0)
|
||||
}
|
||||
|
||||
func findFilter(_ name: String) throws -> FilterType {
|
||||
if let filter = namespace.filters[name] {
|
||||
return filter
|
||||
func findTag(name: String) throws -> Extension.TagParser {
|
||||
for ext in environment.extensions {
|
||||
if let filter = ext.tags[name] {
|
||||
return filter
|
||||
}
|
||||
}
|
||||
|
||||
throw TemplateSyntaxError("Invalid filter '\(name)'")
|
||||
throw TemplateSyntaxError("Unknown template tag '\(name)'")
|
||||
}
|
||||
|
||||
func findFilter(_ name: String) throws -> FilterType {
|
||||
for ext in environment.extensions {
|
||||
if let filter = ext.filters[name] {
|
||||
return filter
|
||||
}
|
||||
}
|
||||
|
||||
throw TemplateSyntaxError("Unknown filter '\(name)'")
|
||||
}
|
||||
|
||||
public func compileFilter(_ token: String) throws -> Resolvable {
|
||||
return try FilterExpression(token: token, parser: self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,16 +6,24 @@ let NSFileNoSuchFileError = 4
|
||||
#endif
|
||||
|
||||
/// A class representing a template
|
||||
public class Template: ExpressibleByStringLiteral {
|
||||
open class Template: ExpressibleByStringLiteral {
|
||||
let environment: Environment
|
||||
let tokens: [Token]
|
||||
|
||||
/// The name of the loaded Template if the Template was loaded from a Loader
|
||||
public let name: String?
|
||||
|
||||
/// Create a template with a template string
|
||||
public init(templateString: String) {
|
||||
public required init(templateString: String, environment: Environment? = nil, name: String? = nil) {
|
||||
self.environment = environment ?? Environment()
|
||||
self.name = name
|
||||
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
tokens = lexer.tokenize()
|
||||
}
|
||||
|
||||
/// Create a template with the given name inside the given bundle
|
||||
@available(*, deprecated, message: "Use Environment/FileSystemLoader instead")
|
||||
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 {
|
||||
@@ -26,16 +34,20 @@ public class Template: ExpressibleByStringLiteral {
|
||||
}
|
||||
|
||||
/// Create a template with a file found at the given URL
|
||||
@available(*, deprecated, message: "Use Environment/FileSystemLoader instead")
|
||||
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 {
|
||||
self.init(templateString: try path.read())
|
||||
@available(*, deprecated, message: "Use Environment/FileSystemLoader instead")
|
||||
public convenience init(path: Path, environment: Environment? = nil, name: String? = nil) throws {
|
||||
self.init(templateString: try path.read(), environment: environment, name: name)
|
||||
}
|
||||
|
||||
// Create a template with a template string literal
|
||||
// MARK: ExpressibleByStringLiteral
|
||||
|
||||
// Create a templaVte with a template string literal
|
||||
public convenience required init(stringLiteral value: String) {
|
||||
self.init(templateString: value)
|
||||
}
|
||||
@@ -50,11 +62,16 @@ public class Template: ExpressibleByStringLiteral {
|
||||
self.init(stringLiteral: value)
|
||||
}
|
||||
|
||||
/// Render the given template
|
||||
public func render(_ context: Context? = nil) throws -> String {
|
||||
let context = context ?? Context()
|
||||
let parser = TokenParser(tokens: tokens, namespace: context.namespace)
|
||||
/// Render the given template with a context
|
||||
func render(_ context: Context) throws -> String {
|
||||
let context = context
|
||||
let parser = TokenParser(tokens: tokens, environment: context.environment)
|
||||
let nodes = try parser.parse()
|
||||
return try renderNodes(nodes, context)
|
||||
}
|
||||
|
||||
/// Render the given template
|
||||
open func render(_ dictionary: [String: Any]? = nil) throws -> String {
|
||||
return try render(Context(dictionary: dictionary, environment: environment))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
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 FileSystemLoader: Loader {
|
||||
public let paths: [Path]
|
||||
|
||||
public init(paths: [Path]) {
|
||||
self.paths = paths
|
||||
}
|
||||
|
||||
public init(bundle: [Bundle]) {
|
||||
self.paths = bundle.map {
|
||||
return Path($0.bundlePath)
|
||||
}
|
||||
}
|
||||
|
||||
public func loadTemplate(name: String) throws -> Template? {
|
||||
for path in paths {
|
||||
let templatePath = path + Path(name)
|
||||
|
||||
if templatePath.exists {
|
||||
return try Template(path: templatePath)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public func loadTemplate(names: [String]) throws -> Template? {
|
||||
for path in paths {
|
||||
for templateName in names {
|
||||
let templatePath = path + Path(templateName)
|
||||
|
||||
if templatePath.exists {
|
||||
return try Template(path: templatePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,18 @@ extension String {
|
||||
var word = ""
|
||||
var components: [String] = []
|
||||
var separate: Character = separator
|
||||
var singleQuoteCount = 0
|
||||
var doubleQuoteCount = 0
|
||||
|
||||
for character in self.characters {
|
||||
if character == "'" { singleQuoteCount += 1 }
|
||||
else if character == "\"" { doubleQuoteCount += 1 }
|
||||
|
||||
if character == separate {
|
||||
|
||||
if separate != separator {
|
||||
word.append(separate)
|
||||
}
|
||||
|
||||
if !word.isEmpty {
|
||||
} else if singleQuoteCount % 2 == 0 && doubleQuoteCount % 2 == 0 && !word.isEmpty {
|
||||
components.append(word)
|
||||
word = ""
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
typealias Number = Float
|
||||
|
||||
|
||||
class FilterExpression : Resolvable {
|
||||
let filters: [(FilterType, [Variable])]
|
||||
let variable: Variable
|
||||
@@ -60,6 +63,11 @@ public struct Variable : Equatable, Resolvable {
|
||||
return variable[variable.characters.index(after: variable.startIndex) ..< variable.characters.index(before: variable.endIndex)]
|
||||
}
|
||||
|
||||
if let number = Number(variable) {
|
||||
// Number literal
|
||||
return number
|
||||
}
|
||||
|
||||
for bit in lookup() {
|
||||
current = normalize(current)
|
||||
|
||||
@@ -99,6 +107,12 @@ public struct Variable : Equatable, Resolvable {
|
||||
}
|
||||
}
|
||||
|
||||
if let resolvable = current as? Resolvable {
|
||||
current = try resolvable.resolve(context)
|
||||
} else if let node = current as? NodeType {
|
||||
current = try node.render(context)
|
||||
}
|
||||
|
||||
return normalize(current)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Stencil",
|
||||
"version": "0.7.1",
|
||||
"version": "0.9.0",
|
||||
"summary": "Stencil is a simple and powerful template language for Swift.",
|
||||
"homepage": "https://stencil.fuller.li",
|
||||
"license": {
|
||||
@@ -13,7 +13,7 @@
|
||||
"social_media_url": "https://twitter.com/kylefuller",
|
||||
"source": {
|
||||
"git": "https://github.com/kylef/Stencil.git",
|
||||
"tag": "0.7.1"
|
||||
"tag": "0.9.0"
|
||||
},
|
||||
"source_files": [
|
||||
"Sources/*.swift"
|
||||
@@ -25,6 +25,6 @@
|
||||
},
|
||||
"requires_arc": true,
|
||||
"dependencies": {
|
||||
"PathKit": [ "~> 0.7.0" ]
|
||||
"PathKit": [ "~> 0.8.0" ]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
@testable import Stencil
|
||||
|
||||
|
||||
func testContext() {
|
||||
|
||||
54
Tests/StencilTests/EnvironmentSpec.swift
Normal file
54
Tests/StencilTests/EnvironmentSpec.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
func testEnvironment() {
|
||||
describe("Environment") {
|
||||
let environment = Environment(loader: ExampleLoader())
|
||||
|
||||
$0.it("can load a template from a name") {
|
||||
let template = try environment.loadTemplate(name: "example.html")
|
||||
try expect(template.name) == "example.html"
|
||||
}
|
||||
|
||||
$0.it("can load a template from a names") {
|
||||
let template = try environment.loadTemplate(names: ["first.html", "example.html"])
|
||||
try expect(template.name) == "example.html"
|
||||
}
|
||||
|
||||
$0.it("can render a template from a string") {
|
||||
let result = try environment.renderTemplate(string: "Hello World")
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can render a template from a file") {
|
||||
let result = try environment.renderTemplate(name: "example.html")
|
||||
try expect(result) == "Hello World!"
|
||||
}
|
||||
|
||||
$0.it("allows you to provide a custom template class") {
|
||||
let environment = Environment(loader: ExampleLoader(), templateClass: CustomTemplate.self)
|
||||
let result = try environment.renderTemplate(string: "Hello World")
|
||||
|
||||
try expect(result) == "here"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fileprivate class ExampleLoader: Loader {
|
||||
func loadTemplate(name: String, environment: Environment) throws -> Template {
|
||||
if name == "example.html" {
|
||||
return Template(templateString: "Hello World!", environment: environment, name: name)
|
||||
}
|
||||
|
||||
throw TemplateDoesNotExist(templateNames: [name], loader: self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class CustomTemplate: Template {
|
||||
override func render(_ dictionary: [String: Any]? = nil) throws -> String {
|
||||
return "here"
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import Spectre
|
||||
|
||||
func testExpressions() {
|
||||
describe("Expression") {
|
||||
let parser = TokenParser(tokens: [], environment: Environment())
|
||||
|
||||
$0.describe("VariableExpression") {
|
||||
let expression = VariableExpression(variable: Variable("value"))
|
||||
|
||||
@@ -103,19 +105,19 @@ func testExpressions() {
|
||||
|
||||
$0.describe("expression parsing") {
|
||||
$0.it("can parse a variable expression") {
|
||||
let expression = try parseExpression(components: ["value"])
|
||||
let expression = try parseExpression(components: ["value"], tokenParser: parser)
|
||||
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"])
|
||||
let expression = try parseExpression(components: ["not", "value"], tokenParser: parser)
|
||||
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"])
|
||||
let expression = try! parseExpression(components: ["lhs", "and", "rhs"], tokenParser: parser)
|
||||
|
||||
$0.it("evaluates to false with lhs false") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse()
|
||||
@@ -135,7 +137,7 @@ func testExpressions() {
|
||||
}
|
||||
|
||||
$0.describe("or expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "or", "rhs"])
|
||||
let expression = try! parseExpression(components: ["lhs", "or", "rhs"], tokenParser: parser)
|
||||
|
||||
$0.it("evaluates to true with lhs true") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue()
|
||||
@@ -155,7 +157,7 @@ func testExpressions() {
|
||||
}
|
||||
|
||||
$0.describe("equality expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "==", "rhs"])
|
||||
let expression = try! parseExpression(components: ["lhs", "==", "rhs"], tokenParser: parser)
|
||||
|
||||
$0.it("evaluates to true with equal lhs/rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue()
|
||||
@@ -191,7 +193,7 @@ func testExpressions() {
|
||||
}
|
||||
|
||||
$0.describe("inequality expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "!=", "rhs"])
|
||||
let expression = try! parseExpression(components: ["lhs", "!=", "rhs"], tokenParser: parser)
|
||||
|
||||
$0.it("evaluates to true with inequal lhs/rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue()
|
||||
@@ -203,7 +205,7 @@ func testExpressions() {
|
||||
}
|
||||
|
||||
$0.describe("more than expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", ">", "rhs"])
|
||||
let expression = try! parseExpression(components: ["lhs", ">", "rhs"], tokenParser: parser)
|
||||
|
||||
$0.it("evaluates to true with lhs > rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue()
|
||||
@@ -215,7 +217,7 @@ func testExpressions() {
|
||||
}
|
||||
|
||||
$0.describe("more than equal expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", ">=", "rhs"])
|
||||
let expression = try! parseExpression(components: ["lhs", ">=", "rhs"], tokenParser: parser)
|
||||
|
||||
$0.it("evaluates to true with lhs == rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
|
||||
@@ -227,7 +229,7 @@ func testExpressions() {
|
||||
}
|
||||
|
||||
$0.describe("less than expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "<", "rhs"])
|
||||
let expression = try! parseExpression(components: ["lhs", "<", "rhs"], tokenParser: parser)
|
||||
|
||||
$0.it("evaluates to true with lhs < rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue()
|
||||
@@ -239,7 +241,7 @@ func testExpressions() {
|
||||
}
|
||||
|
||||
$0.describe("less than equal expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "<=", "rhs"])
|
||||
let expression = try! parseExpression(components: ["lhs", "<=", "rhs"], tokenParser: parser)
|
||||
|
||||
$0.it("evaluates to true with lhs == rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
|
||||
@@ -251,7 +253,7 @@ func testExpressions() {
|
||||
}
|
||||
|
||||
$0.describe("multiple expression") {
|
||||
let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"])
|
||||
let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"], tokenParser: parser)
|
||||
|
||||
$0.it("evaluates to true with one") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
@testable import Stencil
|
||||
|
||||
|
||||
func testFilter() {
|
||||
@@ -9,8 +9,8 @@ func testFilter() {
|
||||
$0.it("allows you to register a custom filter") {
|
||||
let template = Template(templateString: "{{ name|repeat }}")
|
||||
|
||||
let namespace = Namespace()
|
||||
namespace.registerFilter("repeat") { (value: Any?) in
|
||||
let repeatExtension = Extension()
|
||||
repeatExtension.registerFilter("repeat") { (value: Any?) in
|
||||
if let value = value as? String {
|
||||
return "\(value) \(value)"
|
||||
}
|
||||
@@ -18,15 +18,15 @@ func testFilter() {
|
||||
return nil
|
||||
}
|
||||
|
||||
let result = try template.render(Context(dictionary: context, namespace: namespace))
|
||||
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
||||
try expect(result) == "Kyle Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to register a custom filter which accepts arguments") {
|
||||
let template = Template(templateString: "{{ name|repeat:'value' }}")
|
||||
$0.it("allows you to register a custom filter which accepts single argument") {
|
||||
let template = Template(templateString: "{{ name|repeat:'value1, \"value2\"' }}")
|
||||
|
||||
let namespace = Namespace()
|
||||
namespace.registerFilter("repeat") { value, arguments in
|
||||
let repeatExtension = Extension()
|
||||
repeatExtension.registerFilter("repeat") { value, arguments in
|
||||
if !arguments.isEmpty {
|
||||
return "\(value!) \(value!) with args \(arguments.first!!)"
|
||||
}
|
||||
@@ -34,18 +34,47 @@ func testFilter() {
|
||||
return nil
|
||||
}
|
||||
|
||||
let result = try template.render(Context(dictionary: context, namespace: namespace))
|
||||
try expect(result) == "Kyle Kyle with args value"
|
||||
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
||||
try expect(result) == "Kyle Kyle with args value1, \"value2\""
|
||||
}
|
||||
|
||||
$0.it("allows you to register a custom filter which accepts several arguments") {
|
||||
let template = Template(templateString: "{{ name|repeat:'value\"1\"',\"value'2'\",'(key, value)' }}")
|
||||
|
||||
let repeatExtension = Extension()
|
||||
repeatExtension.registerFilter("repeat") { value, arguments in
|
||||
if !arguments.isEmpty {
|
||||
return "\(value!) \(value!) with args 0: \(arguments[0]!), 1: \(arguments[1]!), 2: \(arguments[2]!)"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
||||
try expect(result) == "Kyle Kyle with args 0: value\"1\", 1: value'2', 2: (key, value)"
|
||||
}
|
||||
|
||||
$0.it("allows you to register a custom which throws") {
|
||||
let template = Template(templateString: "{{ name|repeat }}")
|
||||
let namespace = Namespace()
|
||||
namespace.registerFilter("repeat") { (value: Any?) in
|
||||
let repeatExtension = Extension()
|
||||
repeatExtension.registerFilter("repeat") { (value: Any?) in
|
||||
throw TemplateSyntaxError("No Repeat")
|
||||
}
|
||||
|
||||
try expect(try template.render(Context(dictionary: context, namespace: namespace))).toThrow(TemplateSyntaxError("No Repeat"))
|
||||
let context = Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))
|
||||
try expect(try template.render(context)).toThrow(TemplateSyntaxError("No Repeat"))
|
||||
}
|
||||
|
||||
$0.it("allows you to override a default filter") {
|
||||
let template = Template(templateString: "{{ name|join }}")
|
||||
|
||||
let repeatExtension = Extension()
|
||||
repeatExtension.registerFilter("join") { (value: Any?) in
|
||||
return "joined"
|
||||
}
|
||||
|
||||
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
||||
try expect(result) == "joined"
|
||||
}
|
||||
|
||||
$0.it("allows whitespace in expression") {
|
||||
@@ -112,9 +141,26 @@ func testFilter() {
|
||||
describe("join filter") {
|
||||
let template = Template(templateString: "{{ value|join:\", \" }}")
|
||||
|
||||
$0.it("transforms a string to be lowercase") {
|
||||
$0.it("joins a collection of strings") {
|
||||
let result = try template.render(Context(dictionary: ["value": ["One", "Two"]]))
|
||||
try expect(result) == "One, Two"
|
||||
}
|
||||
|
||||
$0.it("joins a mixed-type collection") {
|
||||
let result = try template.render(Context(dictionary: ["value": ["One", 2, true, 10.5, "Five"]]))
|
||||
try expect(result) == "One, 2, true, 10.5, Five"
|
||||
}
|
||||
|
||||
$0.it("can join by non string") {
|
||||
let template = Template(templateString: "{{ value|join:separator }}")
|
||||
let result = try template.render(Context(dictionary: ["value": ["One", "Two"], "separator": true]))
|
||||
try expect(result) == "OnetrueTwo"
|
||||
}
|
||||
|
||||
$0.it("can join without arguments") {
|
||||
let template = Template(templateString: "{{ value|join }}")
|
||||
let result = try template.render(Context(dictionary: ["value": ["One", "Two"]]))
|
||||
try expect(result) == "OneTwo"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
Tests/StencilTests/FilterTagSpec.swift
Normal file
25
Tests/StencilTests/FilterTagSpec.swift
Normal file
@@ -0,0 +1,25 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
func testFilterTag() {
|
||||
describe("Filter Tag") {
|
||||
$0.it("allows you to use a filter") {
|
||||
let template = Template(templateString: "{% filter uppercase %}Test{% endfilter %}")
|
||||
let result = try template.render()
|
||||
try expect(result) == "TEST"
|
||||
}
|
||||
|
||||
$0.it("allows you to chain filters") {
|
||||
let template = Template(templateString: "{% filter lowercase|capitalize %}TEST{% endfilter %}")
|
||||
let result = try template.render()
|
||||
try expect(result) == "Test"
|
||||
}
|
||||
|
||||
$0.it("errors without a filter") {
|
||||
let template = Template(templateString: "{% filter %}Test{% endfilter %}")
|
||||
try expect(try template.render()).toThrow()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -8,18 +8,22 @@ func testForNode() {
|
||||
let context = Context(dictionary: [
|
||||
"items": [1, 2, 3],
|
||||
"emptyItems": [Int](),
|
||||
"dict": [
|
||||
"one": "I",
|
||||
"two": "II",
|
||||
]
|
||||
])
|
||||
|
||||
$0.it("renders the given nodes for each item") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["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(resolvable: Variable("emptyItems"), loopVariable: "item", nodes: nodes, emptyNodes: emptyNodes)
|
||||
let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes)
|
||||
try expect(try node.render(context)) == "empty"
|
||||
}
|
||||
|
||||
@@ -29,7 +33,7 @@ func testForNode() {
|
||||
])
|
||||
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(any_context)) == "123"
|
||||
}
|
||||
|
||||
@@ -40,29 +44,44 @@ func testForNode() {
|
||||
])
|
||||
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["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(resolvable: Variable("items"), loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["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(resolvable: Variable("items"), loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["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(resolvable: Variable("items"), loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "112233"
|
||||
}
|
||||
|
||||
$0.it("renders the given nodes while filtering items using where expression") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
|
||||
let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment()))
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`)
|
||||
try expect(try node.render(context)) == "2132"
|
||||
}
|
||||
|
||||
$0.it("renders the given empty nodes when all items filtered out with where expression") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||
let `where` = try parseExpression(components: ["item", "==", "0"], tokenParser: TokenParser(tokens: [], environment: Environment()))
|
||||
let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`)
|
||||
try expect(try node.render(context)) == "empty"
|
||||
}
|
||||
|
||||
$0.it("can render a filter") {
|
||||
let templateString = "{% for article in ars|default:articles %}" +
|
||||
"- {{ article.title }} by {{ article.author }}.\n" +
|
||||
@@ -85,6 +104,20 @@ func testForNode() {
|
||||
|
||||
try expect(result) == fixture
|
||||
}
|
||||
|
||||
$0.it("renders supports iterating over dictionary") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "key")]
|
||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||
let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key"], nodes: nodes, emptyNodes: emptyNodes, where: nil)
|
||||
try expect(try node.render(context)) == "onetwo"
|
||||
}
|
||||
|
||||
$0.it("renders supports iterating over dictionary") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "key"), VariableNode(variable: "value")]
|
||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||
let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key", "value"], nodes: nodes, emptyNodes: emptyNodes, where: nil)
|
||||
try expect(try node.render(context)) == "oneItwoII"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,46 +9,151 @@ func testIfNode() {
|
||||
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 parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
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
|
||||
let conditions = node?.conditions
|
||||
try expect(conditions?.count) == 1
|
||||
try expect(conditions?[0].nodes.count) == 1
|
||||
let trueNode = conditions?[0].nodes.first as? TextNode
|
||||
try expect(trueNode?.text) == "true"
|
||||
try expect(node?.falseNodes.count) == 1
|
||||
try expect(falseNode?.text) == "false"
|
||||
}
|
||||
|
||||
$0.it("can parse an if with complex expression") {
|
||||
$0.it("can parse an if with else block") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value == \"test\" and not name"),
|
||||
.block(value: "if value"),
|
||||
.text(value: "true"),
|
||||
.block(value: "else"),
|
||||
.text(value: "false"),
|
||||
.block(value: "endif")
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, namespace: Namespace())
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
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
|
||||
let conditions = node?.conditions
|
||||
try expect(conditions?.count) == 2
|
||||
|
||||
try expect(conditions?[0].nodes.count) == 1
|
||||
let trueNode = conditions?[0].nodes.first as? TextNode
|
||||
try expect(trueNode?.text) == "true"
|
||||
try expect(node?.falseNodes.count) == 1
|
||||
|
||||
try expect(conditions?[1].nodes.count) == 1
|
||||
let falseNode = conditions?[1].nodes.first as? TextNode
|
||||
try expect(falseNode?.text) == "false"
|
||||
}
|
||||
|
||||
$0.it("can parse an if with elif block") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value"),
|
||||
.text(value: "true"),
|
||||
.block(value: "elif something"),
|
||||
.text(value: "some"),
|
||||
.block(value: "else"),
|
||||
.text(value: "false"),
|
||||
.block(value: "endif")
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
|
||||
let conditions = node?.conditions
|
||||
try expect(conditions?.count) == 3
|
||||
|
||||
try expect(conditions?[0].nodes.count) == 1
|
||||
let trueNode = conditions?[0].nodes.first as? TextNode
|
||||
try expect(trueNode?.text) == "true"
|
||||
|
||||
try expect(conditions?[1].nodes.count) == 1
|
||||
let elifNode = conditions?[1].nodes.first as? TextNode
|
||||
try expect(elifNode?.text) == "some"
|
||||
|
||||
try expect(conditions?[2].nodes.count) == 1
|
||||
let falseNode = conditions?[2].nodes.first as? TextNode
|
||||
try expect(falseNode?.text) == "false"
|
||||
}
|
||||
|
||||
$0.it("can parse an if with elif block without else") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value"),
|
||||
.text(value: "true"),
|
||||
.block(value: "elif something"),
|
||||
.text(value: "some"),
|
||||
.block(value: "endif")
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
|
||||
let conditions = node?.conditions
|
||||
try expect(conditions?.count) == 2
|
||||
|
||||
try expect(conditions?[0].nodes.count) == 1
|
||||
let trueNode = conditions?[0].nodes.first as? TextNode
|
||||
try expect(trueNode?.text) == "true"
|
||||
|
||||
try expect(conditions?[1].nodes.count) == 1
|
||||
let elifNode = conditions?[1].nodes.first as? TextNode
|
||||
try expect(elifNode?.text) == "some"
|
||||
}
|
||||
|
||||
$0.it("can parse an if with multiple elif block") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value"),
|
||||
.text(value: "true"),
|
||||
.block(value: "elif something1"),
|
||||
.text(value: "some1"),
|
||||
.block(value: "elif something2"),
|
||||
.text(value: "some2"),
|
||||
.block(value: "else"),
|
||||
.text(value: "false"),
|
||||
.block(value: "endif")
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
|
||||
let conditions = node?.conditions
|
||||
try expect(conditions?.count) == 4
|
||||
|
||||
try expect(conditions?[0].nodes.count) == 1
|
||||
let trueNode = conditions?[0].nodes.first as? TextNode
|
||||
try expect(trueNode?.text) == "true"
|
||||
|
||||
try expect(conditions?[1].nodes.count) == 1
|
||||
let elifNode = conditions?[1].nodes.first as? TextNode
|
||||
try expect(elifNode?.text) == "some1"
|
||||
|
||||
try expect(conditions?[2].nodes.count) == 1
|
||||
let elif2Node = conditions?[2].nodes.first as? TextNode
|
||||
try expect(elif2Node?.text) == "some2"
|
||||
|
||||
try expect(conditions?[3].nodes.count) == 1
|
||||
let falseNode = conditions?[3].nodes.first as? TextNode
|
||||
try expect(falseNode?.text) == "false"
|
||||
}
|
||||
|
||||
|
||||
$0.it("can parse an if with complex expression") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value == \"test\" and not name"),
|
||||
.text(value: "true"),
|
||||
.block(value: "endif")
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.first is IfNode).beTrue()
|
||||
}
|
||||
|
||||
$0.it("can parse an ifnot block") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "ifnot value"),
|
||||
@@ -58,16 +163,18 @@ func testIfNode() {
|
||||
.block(value: "endif")
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, namespace: Namespace())
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
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
|
||||
let conditions = node?.conditions
|
||||
try expect(conditions?.count) == 2
|
||||
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.trueNodes.count) == 1
|
||||
try expect(conditions?[0].nodes.count) == 1
|
||||
let trueNode = conditions?[0].nodes.first as? TextNode
|
||||
try expect(trueNode?.text) == "true"
|
||||
try expect(node?.falseNodes.count) == 1
|
||||
|
||||
try expect(conditions?[1].nodes.count) == 1
|
||||
let falseNode = conditions?[1].nodes.first as? TextNode
|
||||
try expect(falseNode?.text) == "false"
|
||||
}
|
||||
|
||||
@@ -76,7 +183,7 @@ func testIfNode() {
|
||||
.block(value: "if value"),
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, namespace: Namespace())
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let error = TemplateSyntaxError("`endif` was not found.")
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
@@ -86,22 +193,65 @@ func testIfNode() {
|
||||
.block(value: "ifnot value"),
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, namespace: Namespace())
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
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 a true expression") {
|
||||
let node = IfNode(conditions: [
|
||||
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]),
|
||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")]),
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == "1"
|
||||
}
|
||||
|
||||
$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"
|
||||
$0.it("renders the first true expression") {
|
||||
let node = IfNode(conditions: [
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]),
|
||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")]),
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == "2"
|
||||
}
|
||||
|
||||
$0.it("renders the empty expression when other conditions are falsy") {
|
||||
let node = IfNode(conditions: [
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]),
|
||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")]),
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == "3"
|
||||
}
|
||||
|
||||
$0.it("renders empty when no truthy conditions") {
|
||||
let node = IfNode(conditions: [
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]),
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == ""
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("supports variable filters in the if expression") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value|uppercase == \"TEST\""),
|
||||
.text(value: "true"),
|
||||
.block(value: "endif")
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
|
||||
let result = try renderNodes(nodes, Context(dictionary: ["value": "test"]))
|
||||
try expect(result) == "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ func testInclude() {
|
||||
describe("Include") {
|
||||
let path = Path(#file) + ".." + "fixtures"
|
||||
let loader = FileSystemLoader(paths: [path])
|
||||
let environment = Environment(loader: loader)
|
||||
|
||||
$0.describe("parsing") {
|
||||
$0.it("throws an error when no template is given") {
|
||||
let tokens: [Token] = [ .block(value: "include") ]
|
||||
let parser = TokenParser(tokens: tokens, namespace: Namespace())
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
|
||||
let error = TemplateSyntaxError("'include' tag takes one argument, the template file to be included")
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
@@ -19,7 +20,7 @@ func testInclude() {
|
||||
|
||||
$0.it("can parse a valid include block") {
|
||||
let tokens: [Token] = [ .block(value: "include \"test.html\"") ]
|
||||
let parser = TokenParser(tokens: tokens, namespace: Namespace())
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IncludeNode
|
||||
@@ -35,7 +36,7 @@ func testInclude() {
|
||||
do {
|
||||
_ = try node.render(Context())
|
||||
} catch {
|
||||
try expect("\(error)") == "Template loader not in context"
|
||||
try expect("\(error)") == "Template named `test.html` does not exist. No loaders found"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,15 +44,15 @@ func testInclude() {
|
||||
let node = IncludeNode(templateName: Variable("\"unknown.html\""))
|
||||
|
||||
do {
|
||||
_ = try node.render(Context(dictionary: ["loader": loader]))
|
||||
_ = try node.render(Context(environment: environment))
|
||||
} catch {
|
||||
try expect("\(error)".hasPrefix("'unknown.html' template not found")).to.beTrue()
|
||||
try expect("\(error)".hasPrefix("Template named `unknown.html` does not exist in loader")).to.beTrue()
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("successfully renders a found included template") {
|
||||
let node = IncludeNode(templateName: Variable("\"test.html\""))
|
||||
let context = Context(dictionary: ["loader":loader, "target": "World"])
|
||||
let context = Context(dictionary: ["target": "World"], environment: environment)
|
||||
let value = try node.render(context)
|
||||
try expect(value) == "Hello World!"
|
||||
}
|
||||
|
||||
@@ -7,17 +7,21 @@ func testInheritence() {
|
||||
describe("Inheritence") {
|
||||
let path = Path(#file) + ".." + "fixtures"
|
||||
let loader = FileSystemLoader(paths: [path])
|
||||
let environment = Environment(loader: loader)
|
||||
|
||||
$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"
|
||||
let template = try environment.loadTemplate(name: "child.html")
|
||||
try expect(try template.render()) == "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"
|
||||
let template = try environment.loadTemplate(name: "child-child.html")
|
||||
try expect(try template.render()) == "Child Child Header\nChild"
|
||||
}
|
||||
|
||||
$0.it("can inherit from a template that calls a super block") {
|
||||
let template = try environment.loadTemplate(name: "child-super.html")
|
||||
try expect(try template.render()) == "Header\nChild Body"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
Tests/StencilTests/LoaderSpec.swift
Normal file
32
Tests/StencilTests/LoaderSpec.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import PathKit
|
||||
|
||||
|
||||
func testTemplateLoader() {
|
||||
describe("FileSystemLoader") {
|
||||
let path = Path(#file) + ".." + "fixtures"
|
||||
let loader = FileSystemLoader(paths: [path])
|
||||
let environment = Environment(loader: loader)
|
||||
|
||||
$0.it("errors when a template cannot be found") {
|
||||
try expect(try environment.loadTemplate(name: "unknown.html")).toThrow()
|
||||
}
|
||||
|
||||
$0.it("errors when an array of templates cannot be found") {
|
||||
try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow()
|
||||
}
|
||||
|
||||
$0.it("can load a template from a file") {
|
||||
_ = try environment.loadTemplate(name: "test.html")
|
||||
}
|
||||
|
||||
$0.it("errors when loading absolute file outside of the selected path") {
|
||||
try expect(try environment.loadTemplate(name: "/etc/hosts")).toThrow()
|
||||
}
|
||||
|
||||
$0.it("errors when loading relative file outside of the selected path") {
|
||||
try expect(try environment.loadTemplate(name: "../LoaderSpec.swift")).toThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
@testable import Stencil
|
||||
|
||||
|
||||
class ErrorNode : NodeType {
|
||||
|
||||
@@ -9,7 +9,7 @@ func testNowNode() {
|
||||
$0.describe("parsing") {
|
||||
$0.it("parses default format without any now arguments") {
|
||||
let tokens: [Token] = [ .block(value: "now") ]
|
||||
let parser = TokenParser(tokens: tokens, namespace: Namespace())
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? NowNode
|
||||
@@ -19,7 +19,7 @@ func testNowNode() {
|
||||
|
||||
$0.it("parses now with a format") {
|
||||
let tokens: [Token] = [ .block(value: "now \"HH:mm\"") ]
|
||||
let parser = TokenParser(tokens: tokens, namespace: Namespace())
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? NowNode
|
||||
try expect(nodes.count) == 1
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
@testable import Stencil
|
||||
|
||||
|
||||
func testTokenParser() {
|
||||
@@ -7,7 +7,7 @@ func testTokenParser() {
|
||||
$0.it("can parse a text token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
.text(value: "Hello World")
|
||||
], namespace: Namespace())
|
||||
], environment: Environment())
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? TextNode
|
||||
@@ -19,7 +19,7 @@ func testTokenParser() {
|
||||
$0.it("can parse a variable token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
.variable(value: "'name'")
|
||||
], namespace: Namespace())
|
||||
], environment: Environment())
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? VariableNode
|
||||
@@ -31,21 +31,21 @@ func testTokenParser() {
|
||||
$0.it("can parse a comment token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
.comment(value: "Secret stuff!")
|
||||
], namespace: Namespace())
|
||||
], environment: Environment())
|
||||
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.count) == 0
|
||||
}
|
||||
|
||||
$0.it("can parse a tag token") {
|
||||
let namespace = Namespace()
|
||||
namespace.registerSimpleTag("known") { _ in
|
||||
let simpleExtension = Extension()
|
||||
simpleExtension.registerSimpleTag("known") { _ in
|
||||
return ""
|
||||
}
|
||||
|
||||
let parser = TokenParser(tokens: [
|
||||
.block(value: "known"),
|
||||
], namespace: namespace)
|
||||
], environment: Environment(extensions: [simpleExtension]))
|
||||
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.count) == 1
|
||||
@@ -54,7 +54,7 @@ func testTokenParser() {
|
||||
$0.it("errors when parsing an unknown tag") {
|
||||
let parser = TokenParser(tokens: [
|
||||
.block(value: "unknown"),
|
||||
], namespace: Namespace())
|
||||
], environment: Environment())
|
||||
|
||||
try expect(try parser.parse()).toThrow(TemplateSyntaxError("Unknown template tag 'unknown'"))
|
||||
}
|
||||
|
||||
@@ -17,6 +17,18 @@ fileprivate struct Article {
|
||||
|
||||
func testStencil() {
|
||||
describe("Stencil") {
|
||||
let exampleExtension = Extension()
|
||||
|
||||
exampleExtension.registerSimpleTag("simpletag") { context in
|
||||
return "Hello World"
|
||||
}
|
||||
|
||||
exampleExtension.registerTag("customtag") { parser, token in
|
||||
return CustomNode()
|
||||
}
|
||||
|
||||
let environment = Environment(extensions: [exampleExtension])
|
||||
|
||||
$0.it("can render the README example") {
|
||||
|
||||
let templateString = "There are {{ articles.count }} articles.\n" +
|
||||
@@ -25,12 +37,12 @@ func testStencil() {
|
||||
" - {{ article.title }} by {{ article.author }}.\n" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let context = Context(dictionary: [
|
||||
let context = [
|
||||
"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)
|
||||
@@ -45,28 +57,13 @@ func testStencil() {
|
||||
}
|
||||
|
||||
$0.it("can render a custom template tag") {
|
||||
let templateString = "{% custom %}"
|
||||
let template = Template(templateString: templateString)
|
||||
|
||||
let namespace = Namespace()
|
||||
namespace.registerTag("custom") { parser, token in
|
||||
return CustomNode()
|
||||
}
|
||||
|
||||
let result = try template.render(Context(namespace: namespace))
|
||||
let result = try environment.renderTemplate(string: "{% customtag %}")
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can render a simple custom tag") {
|
||||
let templateString = "{% custom %}"
|
||||
let template = Template(templateString: templateString)
|
||||
|
||||
let namespace = Namespace()
|
||||
namespace.registerSimpleTag("custom") { context in
|
||||
return "Hello World"
|
||||
}
|
||||
|
||||
try expect(try template.render(Context(namespace: namespace))) == "Hello World"
|
||||
let result = try environment.renderTemplate(string: "{% simpletag %}")
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import PathKit
|
||||
|
||||
|
||||
func testTemplateLoader() {
|
||||
describe("TemplateLoader") {
|
||||
let path = Path(#file) + ".." + "fixtures"
|
||||
let loader = FileSystemLoader(paths: [path])
|
||||
|
||||
$0.it("returns nil when a template cannot be found") {
|
||||
try expect(try loader.loadTemplate(name: "unknown.html")).to.beNil()
|
||||
}
|
||||
|
||||
$0.it("returns nil when an array of templates cannot be found") {
|
||||
try expect(try loader.loadTemplate(names: ["unknown.html", "unknown2.html"])).to.beNil()
|
||||
}
|
||||
|
||||
$0.it("can load a template from a file") {
|
||||
if try loader.loadTemplate(name: "test.html") == nil {
|
||||
throw failure("didn't find the template")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,16 +5,14 @@ import Stencil
|
||||
func testTemplate() {
|
||||
describe("Template") {
|
||||
$0.it("can render a template from a string") {
|
||||
let context = Context(dictionary: [ "name": "Kyle" ])
|
||||
let template = Template(templateString: "Hello World")
|
||||
let result = try template.render(context)
|
||||
let result = try template.render([ "name": "Kyle" ])
|
||||
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)
|
||||
let result = try template.render([ "name": "Kyle" ])
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
import Spectre
|
||||
import Stencil
|
||||
@testable import Stencil
|
||||
|
||||
|
||||
#if os(OSX)
|
||||
@@ -45,6 +45,18 @@ func testVariable() {
|
||||
try expect(result) == "name"
|
||||
}
|
||||
|
||||
$0.it("can resolve an integer literal") {
|
||||
let variable = Variable("5")
|
||||
let result = try variable.resolve(context) as? Number
|
||||
try expect(result) == 5
|
||||
}
|
||||
|
||||
$0.it("can resolve an float literal") {
|
||||
let variable = Variable("3.14")
|
||||
let result = try variable.resolve(context) as? Number
|
||||
try expect(result) == 3.14
|
||||
}
|
||||
|
||||
$0.it("can resolve a string variable") {
|
||||
let variable = Variable("name")
|
||||
let result = try variable.resolve(context) as? String
|
||||
|
||||
@@ -17,6 +17,8 @@ public func stencilTests() {
|
||||
testNowNode()
|
||||
testInclude()
|
||||
testInheritence()
|
||||
testFilterTag()
|
||||
testEnvironment()
|
||||
testStencil()
|
||||
}
|
||||
|
||||
|
||||
3
Tests/StencilTests/fixtures/child-super.html
Normal file
3
Tests/StencilTests/fixtures/child-super.html
Normal file
@@ -0,0 +1,3 @@
|
||||
{% extends "base.html" %}
|
||||
{% block body %}Child {{ block.super }}{% endblock %}
|
||||
|
||||
140
docs/api.rst
Normal file
140
docs/api.rst
Normal file
@@ -0,0 +1,140 @@
|
||||
Template API
|
||||
============
|
||||
|
||||
This document describes Stencils Swift API, and not the Swift template language.
|
||||
|
||||
.. contents:: :depth: 2
|
||||
|
||||
Environment
|
||||
-----------
|
||||
|
||||
An environment contains shared configuration such as custom filters and tags
|
||||
along with template loaders.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let environment = Environment()
|
||||
|
||||
You can optionally provide a loader or extensions when creating an environment:
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let environment = Environment(loader: ..., extensions: [...])
|
||||
|
||||
Rendering a Template
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Environment provides convinience methods to render a template either from a
|
||||
string or a template loader.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let template = "Hello {{ name }}"
|
||||
let context = ["name": "Kyle"]
|
||||
let rendered = environment.renderTemplate(string: template, context: context)
|
||||
|
||||
Rendering a template from the configured loader:
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let context = ["name": "Kyle"]
|
||||
let rendered = environment.renderTemplate(name: "example.html", context: context)
|
||||
|
||||
Loading a Template
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Environment provides an API to load a template from the configured loader.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let template = try environment.loadTemplate(name: "example.html")
|
||||
|
||||
Loader
|
||||
------
|
||||
|
||||
Loaders are responsible for loading templates from a resource such as the file
|
||||
system.
|
||||
|
||||
Stencil provides a ``FileSytemLoader`` which allows you to load a template
|
||||
directly from the file system.
|
||||
|
||||
FileSystemLoader
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Loads templates from the file system. This loader can find templates in folders
|
||||
on the file system.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
FileSystemLoader(paths: ["./templates"])
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
FileSystemLoader(bundle: [Bundle.main])
|
||||
|
||||
|
||||
Custom Loaders
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
``Loader`` is a protocol, so you can implement your own compatible loaders. You
|
||||
will need to implement a ``loadTemplate`` method to load the template,
|
||||
throwing a ``TemplateDoesNotExist`` when the template is not found.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
class ExampleMemoryLoader: Loader {
|
||||
func loadTemplate(name: String, environment: Environment) throws -> Template {
|
||||
if name == "index.html" {
|
||||
return Template(templateString: "Hello", environment: environment)
|
||||
}
|
||||
|
||||
throw TemplateDoesNotExist(name: name, loader: self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 would normally only access the ``Context`` within a custom template tag or
|
||||
filter.
|
||||
|
||||
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()
|
||||
@@ -1,51 +0,0 @@
|
||||
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()
|
||||
@@ -19,6 +19,27 @@ A for loop allows you to iterate over an array found by variable lookup.
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
The ``for`` tag can iterate over dictionaries.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
<ul>
|
||||
{% for key, value in dict %}
|
||||
<li>{{ key }}: {{ value }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
The ``for`` tag can contain optional ``where`` expression to filter out
|
||||
elements on which this expression evaluates to false.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
<ul>
|
||||
{% for user in users where user.name != "Kyle" %}
|
||||
<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.
|
||||
|
||||
@@ -52,10 +73,12 @@ true the contents of the block are processed. Being true is defined as:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% if variable %}
|
||||
The variable was found in the current context.
|
||||
{% if admin %}
|
||||
The user is an administrator.
|
||||
{% elif user %}
|
||||
A user is logged in.
|
||||
{% else %}
|
||||
The variable was not found.
|
||||
No user was found.
|
||||
{% endif %}
|
||||
|
||||
Operators
|
||||
@@ -177,6 +200,26 @@ Will be treated as:
|
||||
``now``
|
||||
~~~~~~~
|
||||
|
||||
``filter``
|
||||
~~~~~~~~~~
|
||||
|
||||
Filters the contents of the block.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% filter lowercase %}
|
||||
This Text Will Be Lowercased.
|
||||
{% endfilter %}
|
||||
|
||||
You can chain multiple filters with a pipe (`|`).
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% filter lowercase|capitalize %}
|
||||
This Text Will First Be Lowercased, Then The First Character Will BE
|
||||
Capitalised.
|
||||
{% endfilter %}
|
||||
|
||||
``include``
|
||||
~~~~~~~~~~~
|
||||
|
||||
@@ -186,20 +229,31 @@ You can include another template using the `include` tag.
|
||||
|
||||
{% 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.
|
||||
The `include` tag requires you to provide a loader which will be used to lookup
|
||||
the template.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let context = Context(dictionary: [
|
||||
"loader": FileSystemLoader(bundle: [NSBundle.mainBundle()])
|
||||
])
|
||||
let environment = Environment(bundle: [Bundle.main])
|
||||
let template = environment.loadTemplate(name: "index.html")
|
||||
|
||||
``extends``
|
||||
~~~~~~~~~~~
|
||||
|
||||
Extends the template from a parent template.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
See :ref:`template-inheritance` for more information.
|
||||
|
||||
``block``
|
||||
~~~~~~~~~
|
||||
|
||||
Defines a block that can be overridden by child templates. See
|
||||
:ref:`template-inheritance` for more information.
|
||||
|
||||
.. _built-in-filters:
|
||||
|
||||
Built-in Filters
|
||||
@@ -248,10 +302,10 @@ value of the variable. For example:
|
||||
``join``
|
||||
~~~~~~~~
|
||||
|
||||
Join an array with a string.
|
||||
Join an array of items.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{{ value|join:", " }}
|
||||
|
||||
.. note:: The value MUST be an array of Strngs and the separator must be a string.
|
||||
.. note:: The value MUST be an array.
|
||||
|
||||
@@ -3,13 +3,15 @@ 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.
|
||||
extension 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)
|
||||
let ext = Extension()
|
||||
// Register your filters and tags with the extension
|
||||
|
||||
let environment = Environment(extensions: [ext])
|
||||
try environment.renderTemplate(name: "example.html")
|
||||
|
||||
Custom Filters
|
||||
--------------
|
||||
@@ -18,7 +20,7 @@ Registering custom filters:
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
namespace.registerFilter("double") { (value: Any?) in
|
||||
ext.registerFilter("double") { (value: Any?) in
|
||||
if let value = value as? Int {
|
||||
return value * 2
|
||||
}
|
||||
@@ -30,7 +32,7 @@ Registering custom filters with arguments:
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
namespace.registerFilter("multiply") { (value: Any?, arguments: [Any?]) in
|
||||
ext.registerFilter("multiply") { (value: Any?, arguments: [Any?]) in
|
||||
let amount: Int
|
||||
|
||||
if let value = arguments.first as? Int {
|
||||
@@ -54,7 +56,7 @@ write your own custom tags. The following is the simplest form:
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
namespace.registerSimpleTag("custom") { context in
|
||||
ext.registerSimpleTag("custom") { context in
|
||||
return "Hello World"
|
||||
}
|
||||
|
||||
|
||||
37
docs/getting-started.rst
Normal file
37
docs/getting-started.rst
Normal file
@@ -0,0 +1,37 @@
|
||||
Getting Started
|
||||
===============
|
||||
|
||||
The easiest way to render a template using Stencil is to create a template and
|
||||
call render on it providing a context.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let template = Template(templateString: "Hello {{ name }}")
|
||||
try template.render(["name": "kyle"])
|
||||
|
||||
For more advanced uses, you would normally create an ``Environment`` and call
|
||||
the ``renderTemplate`` convinience method.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let environment = Environment()
|
||||
|
||||
let context = ["name": "kyle"]
|
||||
try template.renderTemplate(string: "Hello {{ name }}", context: context)
|
||||
|
||||
Template Loaders
|
||||
----------------
|
||||
|
||||
A template loader allows you to load files from disk or elsewhere. Using a
|
||||
``FileSystemLoader`` we can easily render a template from disk.
|
||||
|
||||
For example, to render a template called ``index.html`` inside the
|
||||
``templates/`` directory we can use the following:
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let fsLoader = FileSystemLoader(paths: ["templates/"])
|
||||
let environment = Environment(loader: fsLoader)
|
||||
|
||||
let context = ["name": "kyle"]
|
||||
try template.renderTemplate(name: "index.html", context: context)
|
||||
@@ -17,32 +17,48 @@ feel right at home with Stencil.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
struct Article {
|
||||
let title: String
|
||||
let author: String
|
||||
}
|
||||
import Stencil
|
||||
|
||||
let context = Context(dictionary: [
|
||||
struct Article {
|
||||
let title: String
|
||||
let author: String
|
||||
}
|
||||
|
||||
let context = [
|
||||
"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)")
|
||||
}
|
||||
let environment = Environment(loader: FileSystemLoader(paths: ["templates/"])
|
||||
let rendered = try environment.renderTemplate(name: context)
|
||||
|
||||
Contents:
|
||||
print(rendered)
|
||||
|
||||
The User Guide
|
||||
--------------
|
||||
|
||||
For Template Writers
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Resources for Stencil template authors to write Stencil templates.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
templates
|
||||
builtins
|
||||
api/context
|
||||
|
||||
For Developers
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Resources to help you integrate Stencil into a Swift project.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
installation
|
||||
getting-started
|
||||
api
|
||||
custom-template-tags-and-filters
|
||||
|
||||
52
docs/installation.rst
Normal file
52
docs/installation.rst
Normal file
@@ -0,0 +1,52 @@
|
||||
Installation
|
||||
============
|
||||
|
||||
Swift Package Mangaer
|
||||
---------------------
|
||||
|
||||
If you're using the Swift Package Manager, you can add ``Stencil`` to your
|
||||
dependencies inside ``Package.swift``.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "MyApplication",
|
||||
dependencies: [
|
||||
.Package(url: "https://github.com/kylef/Stencil.git", majorVersion: 0, minor: 8),
|
||||
]
|
||||
)
|
||||
|
||||
CocoaPods
|
||||
---------
|
||||
|
||||
If you're using CocoaPods, you can add Stencil to your ``Podfile`` and then run
|
||||
``pod install``.
|
||||
|
||||
.. code-block:: ruby
|
||||
|
||||
pod 'Stencil', '~> 0.8.0'
|
||||
|
||||
Carthage
|
||||
--------
|
||||
|
||||
.. note:: Use at your own risk. We don't offer support for Carthage and instead recommend you use Swift Package Manager.
|
||||
|
||||
1) Add ``Stencil`` to your ``Cartfile``:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
github "kylef/Stencil" ~> 0.8.0
|
||||
|
||||
2) Checkout your dependencies, generate the Stencil Xcode project, and then use Carthage to build Stencil:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ carthage update
|
||||
$ (cd Carthage/Checkouts/Stencil && swift package generate-xcodeproj)
|
||||
$ carthage build
|
||||
|
||||
3) Follow the Carthage steps to add the built frameworks to your project.
|
||||
|
||||
To learn more about this approach see `Using Swift Package Manager with Carthage <https://fuller.li/posts/using-swift-package-manager-with-carthage/>`_.
|
||||
@@ -1,5 +1,5 @@
|
||||
Templates
|
||||
=========
|
||||
Language overview
|
||||
==================
|
||||
|
||||
- ``{{ ... }}`` for variables to print to the template output
|
||||
- ``{% ... %}`` for tags
|
||||
@@ -75,3 +75,93 @@ To comment out part of your template, you can use the following syntax:
|
||||
.. code-block:: html+django
|
||||
|
||||
{# My comment is completely hidden #}
|
||||
|
||||
.. _template-inheritance:
|
||||
|
||||
Template inheritance
|
||||
--------------------
|
||||
|
||||
Template inheritance allows the common components surrounding individual pages
|
||||
to be shared across other templates. You can define blocks which can be
|
||||
overidden in any child template.
|
||||
|
||||
Let's take a look at an example. Here is our base template (``base.html``):
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>{% block title %}Example{% endblock %}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<aside>
|
||||
{% block sidebar %}
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/notes/">Notes</a></li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
</aside>
|
||||
|
||||
<section>
|
||||
{% block content %}{% endblock %}
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
This example declares three blocks, ``title``, ``sidebar`` and ``content``. We
|
||||
can use the ``{% extends %}`` template tag to inherit from out base template
|
||||
and then use ``{% block %}`` to override any blocks from our base template.
|
||||
|
||||
A child template might look like the following:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Notes{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% for note in notes %}
|
||||
<h2>{{ note }}</h2>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
.. note:: You can use ``{{ block.super }}` inside a block to render the contents of the parent block inline.
|
||||
|
||||
Since our child template doesn't declare a sidebar block. The original sidebar
|
||||
from our base template will be used. Depending on the content of ``notes`` our
|
||||
template might be rendered like the following:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Notes</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<aside>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/notes/">Notes</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<section>
|
||||
<h2>Pick up food</h2>
|
||||
<h2>Do laundry</h2>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
You can use as many levels of inheritance as needed. One common way of using
|
||||
inheritance is the following three-level approach:
|
||||
|
||||
* Create a ``base.html`` template that holds the main look-and-feel of your site.
|
||||
* Create a ``base_SECTIONNAME.html`` template for each “section” of your site.
|
||||
For example, ``base_news.html``, ``base_news.html``. These templates all
|
||||
extend ``base.html`` and include section-specific styles/design.
|
||||
* Create individual templates for each type of page, such as a news article or
|
||||
blog entry. These templates extend the appropriate section template.
|
||||
|
||||
Reference in New Issue
Block a user