feat: New extensions API (#78)
This commit is contained in:
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.
|
||||
@@ -28,6 +28,10 @@
|
||||
- `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
|
||||
|
||||
@@ -3,16 +3,14 @@ public class Context {
|
||||
var dictionaries: [[String: Any?]]
|
||||
|
||||
public let environment: Environment
|
||||
let namespace: Namespace
|
||||
|
||||
init(dictionary: [String: Any]? = nil, namespace: Namespace? = nil, environment: Environment? = nil) {
|
||||
init(dictionary: [String: Any]? = nil, environment: Environment? = nil) {
|
||||
if let dictionary = dictionary {
|
||||
dictionaries = [dictionary]
|
||||
} else {
|
||||
dictionaries = []
|
||||
}
|
||||
|
||||
self.namespace = namespace ?? environment?.namespace ?? Namespace()
|
||||
self.environment = environment ?? Environment()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
public struct Environment {
|
||||
var namespace: Namespace
|
||||
public let extensions: [Extension]
|
||||
|
||||
public var loader: Loader?
|
||||
|
||||
public init(loader: Loader? = nil, namespace: Namespace? = nil) {
|
||||
public init(loader: Loader? = nil, extensions: [Extension]? = nil) {
|
||||
self.loader = loader
|
||||
self.namespace = namespace ?? Namespace()
|
||||
self.extensions = [DefaultExtension()] + (extensions ?? [])
|
||||
}
|
||||
|
||||
public func loadTemplate(name: String) throws -> Template {
|
||||
|
||||
@@ -1,33 +1,39 @@
|
||||
protocol FilterType {
|
||||
func invoke(value: Any?, arguments: [Any?]) throws -> Any?
|
||||
}
|
||||
open class Extension {
|
||||
typealias TagParser = (TokenParser, Token) throws -> NodeType
|
||||
var tags = [String: TagParser]()
|
||||
|
||||
enum Filter: FilterType {
|
||||
case simple(((Any?) throws -> Any?))
|
||||
case arguments(((Any?, [Any?]) throws -> Any?))
|
||||
var filters = [String: Filter]()
|
||||
|
||||
func invoke(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
switch self {
|
||||
case let .simple(filter):
|
||||
if !arguments.isEmpty {
|
||||
throw TemplateSyntaxError("cannot invoke filter with an argument")
|
||||
}
|
||||
public init() {
|
||||
}
|
||||
|
||||
return try filter(value)
|
||||
case let .arguments(filter):
|
||||
return try filter(value, arguments)
|
||||
}
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class Namespace {
|
||||
public typealias TagParser = (TokenParser, Token) throws -> NodeType
|
||||
|
||||
var tags = [String: TagParser]()
|
||||
var filters = [String: Filter]()
|
||||
|
||||
public init() {
|
||||
class DefaultExtension: Extension {
|
||||
override init() {
|
||||
super.init()
|
||||
registerDefaultTags()
|
||||
registerDefaultFilters()
|
||||
}
|
||||
@@ -52,26 +58,27 @@ public class Namespace {
|
||||
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)
|
||||
})
|
||||
}
|
||||
protocol FilterType {
|
||||
func invoke(value: Any?, arguments: [Any?]) throws -> Any?
|
||||
}
|
||||
|
||||
/// Registers a template filter with the given name
|
||||
public func registerFilter(_ name: String, filter: @escaping (Any?) throws -> Any?) {
|
||||
filters[name] = .simple(filter)
|
||||
}
|
||||
enum Filter: FilterType {
|
||||
case simple(((Any?) throws -> Any?))
|
||||
case arguments(((Any?, [Any?]) throws -> Any?))
|
||||
|
||||
/// Registers a template filter with the given name
|
||||
public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?]) throws -> Any?) {
|
||||
filters[name] = .arguments(filter)
|
||||
func invoke(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
switch self {
|
||||
case let .simple(filter):
|
||||
if !arguments.isEmpty {
|
||||
throw TemplateSyntaxError("cannot invoke filter with an argument")
|
||||
}
|
||||
|
||||
return try filter(value)
|
||||
case let .arguments(filter):
|
||||
return try filter(value, arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ public class Template: ExpressibleByStringLiteral {
|
||||
/// Render the given template with a context
|
||||
func render(_ context: Context) throws -> String {
|
||||
let context = context
|
||||
let parser = TokenParser(tokens: tokens, namespace: context.namespace)
|
||||
let parser = TokenParser(tokens: tokens, environment: context.environment)
|
||||
let nodes = try parser.parse()
|
||||
return try renderNodes(nodes, context)
|
||||
}
|
||||
|
||||
@@ -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' }}")
|
||||
|
||||
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,19 @@ 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 with args value"
|
||||
}
|
||||
|
||||
$0.it("allows you to register a custom which throws") {
|
||||
let template = Template(templateString: "{{ name|repeat }}")
|
||||
let namespace = Namespace()
|
||||
namespace.registerFilter("repeat") { (value: Any?) in
|
||||
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 whitespace in expression") {
|
||||
|
||||
@@ -14,7 +14,7 @@ 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
|
||||
@@ -36,7 +36,7 @@ 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
|
||||
@@ -58,7 +58,7 @@ 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
|
||||
@@ -76,7 +76,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,7 +86,7 @@ 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)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ func testInclude() {
|
||||
$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)
|
||||
@@ -20,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" +
|
||||
@@ -45,26 +57,12 @@ func testStencil() {
|
||||
}
|
||||
|
||||
$0.it("can render a custom template tag") {
|
||||
let namespace = Namespace()
|
||||
namespace.registerTag("custom") { parser, token in
|
||||
return CustomNode()
|
||||
}
|
||||
|
||||
let environment = Environment(namespace: namespace)
|
||||
let result = try environment.renderTemplate(string: "{% custom %}")
|
||||
|
||||
let result = try environment.renderTemplate(string: "{% customtag %}")
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can render a simple custom tag") {
|
||||
let namespace = Namespace()
|
||||
namespace.registerSimpleTag("custom") { context in
|
||||
return "Hello World"
|
||||
}
|
||||
|
||||
let environment = Environment(namespace: namespace)
|
||||
let result = try environment.renderTemplate(string: "{% custom %}")
|
||||
|
||||
let result = try environment.renderTemplate(string: "{% simpletag %}")
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,11 @@ along with template loaders.
|
||||
|
||||
let environment = Environment()
|
||||
|
||||
You can optionally provide a loader or namespace when creating an environment:
|
||||
You can optionally provide a loader or extensions when creating an environment:
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let environment = Environment(loader: ..., namespace: ...)
|
||||
let environment = Environment(loader: ..., extensions: [...])
|
||||
|
||||
Rendering a Template
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -3,14 +3,14 @@ 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 ext = Extension()
|
||||
// Register your filters and tags with the extension
|
||||
|
||||
let environment = Environment(namespace: namespace)
|
||||
let environment = Environment(extensions: [ext])
|
||||
try environment.renderTemplate(name: "example.html")
|
||||
|
||||
Custom Filters
|
||||
@@ -20,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
|
||||
}
|
||||
@@ -32,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 {
|
||||
@@ -56,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"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user