feat: New extensions API (#78)

This commit is contained in:
Kyle Fuller
2016-12-07 21:27:31 +00:00
committed by GitHub
parent d3706f074d
commit d7b152089e
15 changed files with 128 additions and 287 deletions

View File

@@ -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 Tokens. Afterwards, the array of tokens are transformed into a collection of Nodes. Once we have a collection of Nodes (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 Tokens.
### 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 Tokens. 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. Tokens are converted to Nodes using the `TokenParser` class.
For some Tokens, there is a direct mapping from a Token to a Node. However block nodes 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 Tokens 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 tokens 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 tags 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 its 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 its 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.
Heres 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 tags 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 nodes further in the token array.
As an example, were 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 Nodes 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. Its somewhat like a dictionary, however you can push and pop to scope variables. So that means that when iterating over a for loop, you can push a new scope into the context to store any variables local to the scope.

View File

@@ -28,6 +28,10 @@
- `Loader`s will now throw a `TemplateDoesNotExist` error when a template - `Loader`s will now throw a `TemplateDoesNotExist` error when a template
is not found. 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 ### Enhancements
- `Environment` is a new way to load templates. You can configure an - `Environment` is a new way to load templates. You can configure an

View File

@@ -3,16 +3,14 @@ public class Context {
var dictionaries: [[String: Any?]] var dictionaries: [[String: Any?]]
public let environment: Environment 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 { if let dictionary = dictionary {
dictionaries = [dictionary] dictionaries = [dictionary]
} else { } else {
dictionaries = [] dictionaries = []
} }
self.namespace = namespace ?? environment?.namespace ?? Namespace()
self.environment = environment ?? Environment() self.environment = environment ?? Environment()
} }

View File

@@ -1,11 +1,11 @@
public struct Environment { public struct Environment {
var namespace: Namespace public let extensions: [Extension]
public var loader: Loader? public var loader: Loader?
public init(loader: Loader? = nil, namespace: Namespace? = nil) { public init(loader: Loader? = nil, extensions: [Extension]? = nil) {
self.loader = loader self.loader = loader
self.namespace = namespace ?? Namespace() self.extensions = [DefaultExtension()] + (extensions ?? [])
} }
public func loadTemplate(name: String) throws -> Template { public func loadTemplate(name: String) throws -> Template {

View File

@@ -1,33 +1,39 @@
protocol FilterType { open class Extension {
func invoke(value: Any?, arguments: [Any?]) throws -> Any? typealias TagParser = (TokenParser, Token) throws -> NodeType
}
enum Filter: FilterType {
case simple(((Any?) throws -> Any?))
case arguments(((Any?, [Any?]) throws -> Any?))
func invoke(value: Any?, arguments: [Any?]) throws -> Any? {
switch self {
case let .simple(filter):
if !arguments.isEmpty {
throw TemplateSyntaxError("cannot invoke filter with an argument")
}
return try filter(value)
case let .arguments(filter):
return try filter(value, arguments)
}
}
}
public class Namespace {
public typealias TagParser = (TokenParser, Token) throws -> NodeType
var tags = [String: TagParser]() var tags = [String: TagParser]()
var filters = [String: Filter]() var filters = [String: Filter]()
public init() { 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() registerDefaultTags()
registerDefaultFilters() registerDefaultFilters()
} }
@@ -52,26 +58,27 @@ public class Namespace {
registerFilter("lowercase", filter: lowercase) registerFilter("lowercase", filter: lowercase)
registerFilter("join", filter: joinFilter) 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) { protocol FilterType {
registerTag(name, parser: { parser, token in func invoke(value: Any?, arguments: [Any?]) throws -> Any?
return SimpleNode(handler: handler)
})
} }
/// Registers a template filter with the given name enum Filter: FilterType {
public func registerFilter(_ name: String, filter: @escaping (Any?) throws -> Any?) { case simple(((Any?) throws -> Any?))
filters[name] = .simple(filter) case arguments(((Any?, [Any?]) throws -> Any?))
func invoke(value: Any?, arguments: [Any?]) throws -> Any? {
switch self {
case let .simple(filter):
if !arguments.isEmpty {
throw TemplateSyntaxError("cannot invoke filter with an argument")
} }
/// Registers a template filter with the given name return try filter(value)
public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?]) throws -> Any?) { case let .arguments(filter):
filters[name] = .arguments(filter) return try filter(value, arguments)
}
} }
} }

View File

@@ -18,11 +18,11 @@ public class TokenParser {
public typealias TagParser = (TokenParser, Token) throws -> NodeType public typealias TagParser = (TokenParser, Token) throws -> NodeType
fileprivate var tokens: [Token] 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.tokens = tokens
self.namespace = namespace self.environment = environment
} }
/// Parse the given tokens into nodes /// Parse the given tokens into nodes
@@ -42,19 +42,14 @@ public class TokenParser {
case .variable: case .variable:
nodes.append(VariableNode(variable: try compileFilter(token.contents))) nodes.append(VariableNode(variable: try compileFilter(token.contents)))
case .block: case .block:
let tag = token.components().first
if let parse_until = parse_until , parse_until(self, token) { if let parse_until = parse_until , parse_until(self, token) {
prependToken(token) prependToken(token)
return nodes return nodes
} }
if let tag = tag { if let tag = token.components().first {
if let parser = namespace.tags[tag] { let parser = try findTag(name: tag)
nodes.append(try parser(self, token)) nodes.append(try parser(self, token))
} else {
throw TemplateSyntaxError("Unknown template tag '\(tag)'")
}
} }
case .comment: case .comment:
continue continue
@@ -76,15 +71,28 @@ public class TokenParser {
tokens.insert(token, at: 0) tokens.insert(token, at: 0)
} }
func findFilter(_ name: String) throws -> FilterType { func findTag(name: String) throws -> Extension.TagParser {
if let filter = namespace.filters[name] { for ext in environment.extensions {
if let filter = ext.tags[name] {
return filter 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 { public func compileFilter(_ token: String) throws -> Resolvable {
return try FilterExpression(token: token, parser: self) return try FilterExpression(token: token, parser: self)
} }
} }

View File

@@ -65,7 +65,7 @@ public class Template: ExpressibleByStringLiteral {
/// Render the given template with a context /// Render the given template with a context
func render(_ context: Context) throws -> String { func render(_ context: Context) throws -> String {
let context = context let context = context
let parser = TokenParser(tokens: tokens, namespace: context.namespace) let parser = TokenParser(tokens: tokens, environment: context.environment)
let nodes = try parser.parse() let nodes = try parser.parse()
return try renderNodes(nodes, context) return try renderNodes(nodes, context)
} }

View File

@@ -9,8 +9,8 @@ func testFilter() {
$0.it("allows you to register a custom filter") { $0.it("allows you to register a custom filter") {
let template = Template(templateString: "{{ name|repeat }}") let template = Template(templateString: "{{ name|repeat }}")
let namespace = Namespace() let repeatExtension = Extension()
namespace.registerFilter("repeat") { (value: Any?) in repeatExtension.registerFilter("repeat") { (value: Any?) in
if let value = value as? String { if let value = value as? String {
return "\(value) \(value)" return "\(value) \(value)"
} }
@@ -18,15 +18,15 @@ func testFilter() {
return nil 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" try expect(result) == "Kyle Kyle"
} }
$0.it("allows you to register a custom filter which accepts arguments") { $0.it("allows you to register a custom filter which accepts arguments") {
let template = Template(templateString: "{{ name|repeat:'value' }}") let template = Template(templateString: "{{ name|repeat:'value' }}")
let namespace = Namespace() let repeatExtension = Extension()
namespace.registerFilter("repeat") { value, arguments in repeatExtension.registerFilter("repeat") { value, arguments in
if !arguments.isEmpty { if !arguments.isEmpty {
return "\(value!) \(value!) with args \(arguments.first!!)" return "\(value!) \(value!) with args \(arguments.first!!)"
} }
@@ -34,18 +34,19 @@ func testFilter() {
return nil 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" try expect(result) == "Kyle Kyle with args value"
} }
$0.it("allows you to register a custom which throws") { $0.it("allows you to register a custom which throws") {
let template = Template(templateString: "{{ name|repeat }}") let template = Template(templateString: "{{ name|repeat }}")
let namespace = Namespace() let repeatExtension = Extension()
namespace.registerFilter("repeat") { (value: Any?) in repeatExtension.registerFilter("repeat") { (value: Any?) in
throw TemplateSyntaxError("No Repeat") 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") { $0.it("allows whitespace in expression") {

View File

@@ -14,7 +14,7 @@ func testIfNode() {
.block(value: "endif") .block(value: "endif")
] ]
let parser = TokenParser(tokens: tokens, namespace: Namespace()) let parser = TokenParser(tokens: tokens, environment: Environment())
let nodes = try parser.parse() let nodes = try parser.parse()
let node = nodes.first as? IfNode let node = nodes.first as? IfNode
let trueNode = node?.trueNodes.first as? TextNode let trueNode = node?.trueNodes.first as? TextNode
@@ -36,7 +36,7 @@ func testIfNode() {
.block(value: "endif") .block(value: "endif")
] ]
let parser = TokenParser(tokens: tokens, namespace: Namespace()) let parser = TokenParser(tokens: tokens, environment: Environment())
let nodes = try parser.parse() let nodes = try parser.parse()
let node = nodes.first as? IfNode let node = nodes.first as? IfNode
let trueNode = node?.trueNodes.first as? TextNode let trueNode = node?.trueNodes.first as? TextNode
@@ -58,7 +58,7 @@ func testIfNode() {
.block(value: "endif") .block(value: "endif")
] ]
let parser = TokenParser(tokens: tokens, namespace: Namespace()) let parser = TokenParser(tokens: tokens, environment: Environment())
let nodes = try parser.parse() let nodes = try parser.parse()
let node = nodes.first as? IfNode let node = nodes.first as? IfNode
let trueNode = node?.trueNodes.first as? TextNode let trueNode = node?.trueNodes.first as? TextNode
@@ -76,7 +76,7 @@ func testIfNode() {
.block(value: "if value"), .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.") let error = TemplateSyntaxError("`endif` was not found.")
try expect(try parser.parse()).toThrow(error) try expect(try parser.parse()).toThrow(error)
} }
@@ -86,7 +86,7 @@ func testIfNode() {
.block(value: "ifnot value"), .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.") let error = TemplateSyntaxError("`endif` was not found.")
try expect(try parser.parse()).toThrow(error) try expect(try parser.parse()).toThrow(error)
} }

View File

@@ -12,7 +12,7 @@ func testInclude() {
$0.describe("parsing") { $0.describe("parsing") {
$0.it("throws an error when no template is given") { $0.it("throws an error when no template is given") {
let tokens: [Token] = [ .block(value: "include") ] let tokens: [Token] = [ .block(value: "include") ]
let parser = TokenParser(tokens: tokens, namespace: Namespace()) let parser = TokenParser(tokens: tokens, environment: Environment())
let error = TemplateSyntaxError("'include' tag takes one argument, the template file to be included") let error = TemplateSyntaxError("'include' tag takes one argument, the template file to be included")
try expect(try parser.parse()).toThrow(error) try expect(try parser.parse()).toThrow(error)
@@ -20,7 +20,7 @@ func testInclude() {
$0.it("can parse a valid include block") { $0.it("can parse a valid include block") {
let tokens: [Token] = [ .block(value: "include \"test.html\"") ] let tokens: [Token] = [ .block(value: "include \"test.html\"") ]
let parser = TokenParser(tokens: tokens, namespace: Namespace()) let parser = TokenParser(tokens: tokens, environment: Environment())
let nodes = try parser.parse() let nodes = try parser.parse()
let node = nodes.first as? IncludeNode let node = nodes.first as? IncludeNode

View File

@@ -9,7 +9,7 @@ func testNowNode() {
$0.describe("parsing") { $0.describe("parsing") {
$0.it("parses default format without any now arguments") { $0.it("parses default format without any now arguments") {
let tokens: [Token] = [ .block(value: "now") ] let tokens: [Token] = [ .block(value: "now") ]
let parser = TokenParser(tokens: tokens, namespace: Namespace()) let parser = TokenParser(tokens: tokens, environment: Environment())
let nodes = try parser.parse() let nodes = try parser.parse()
let node = nodes.first as? NowNode let node = nodes.first as? NowNode
@@ -19,7 +19,7 @@ func testNowNode() {
$0.it("parses now with a format") { $0.it("parses now with a format") {
let tokens: [Token] = [ .block(value: "now \"HH:mm\"") ] let tokens: [Token] = [ .block(value: "now \"HH:mm\"") ]
let parser = TokenParser(tokens: tokens, namespace: Namespace()) let parser = TokenParser(tokens: tokens, environment: Environment())
let nodes = try parser.parse() let nodes = try parser.parse()
let node = nodes.first as? NowNode let node = nodes.first as? NowNode
try expect(nodes.count) == 1 try expect(nodes.count) == 1

View File

@@ -7,7 +7,7 @@ func testTokenParser() {
$0.it("can parse a text token") { $0.it("can parse a text token") {
let parser = TokenParser(tokens: [ let parser = TokenParser(tokens: [
.text(value: "Hello World") .text(value: "Hello World")
], namespace: Namespace()) ], environment: Environment())
let nodes = try parser.parse() let nodes = try parser.parse()
let node = nodes.first as? TextNode let node = nodes.first as? TextNode
@@ -19,7 +19,7 @@ func testTokenParser() {
$0.it("can parse a variable token") { $0.it("can parse a variable token") {
let parser = TokenParser(tokens: [ let parser = TokenParser(tokens: [
.variable(value: "'name'") .variable(value: "'name'")
], namespace: Namespace()) ], environment: Environment())
let nodes = try parser.parse() let nodes = try parser.parse()
let node = nodes.first as? VariableNode let node = nodes.first as? VariableNode
@@ -31,21 +31,21 @@ func testTokenParser() {
$0.it("can parse a comment token") { $0.it("can parse a comment token") {
let parser = TokenParser(tokens: [ let parser = TokenParser(tokens: [
.comment(value: "Secret stuff!") .comment(value: "Secret stuff!")
], namespace: Namespace()) ], environment: Environment())
let nodes = try parser.parse() let nodes = try parser.parse()
try expect(nodes.count) == 0 try expect(nodes.count) == 0
} }
$0.it("can parse a tag token") { $0.it("can parse a tag token") {
let namespace = Namespace() let simpleExtension = Extension()
namespace.registerSimpleTag("known") { _ in simpleExtension.registerSimpleTag("known") { _ in
return "" return ""
} }
let parser = TokenParser(tokens: [ let parser = TokenParser(tokens: [
.block(value: "known"), .block(value: "known"),
], namespace: namespace) ], environment: Environment(extensions: [simpleExtension]))
let nodes = try parser.parse() let nodes = try parser.parse()
try expect(nodes.count) == 1 try expect(nodes.count) == 1
@@ -54,7 +54,7 @@ func testTokenParser() {
$0.it("errors when parsing an unknown tag") { $0.it("errors when parsing an unknown tag") {
let parser = TokenParser(tokens: [ let parser = TokenParser(tokens: [
.block(value: "unknown"), .block(value: "unknown"),
], namespace: Namespace()) ], environment: Environment())
try expect(try parser.parse()).toThrow(TemplateSyntaxError("Unknown template tag 'unknown'")) try expect(try parser.parse()).toThrow(TemplateSyntaxError("Unknown template tag 'unknown'"))
} }

View File

@@ -17,6 +17,18 @@ fileprivate struct Article {
func testStencil() { func testStencil() {
describe("Stencil") { 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") { $0.it("can render the README example") {
let templateString = "There are {{ articles.count }} articles.\n" + let templateString = "There are {{ articles.count }} articles.\n" +
@@ -45,26 +57,12 @@ func testStencil() {
} }
$0.it("can render a custom template tag") { $0.it("can render a custom template tag") {
let namespace = Namespace() let result = try environment.renderTemplate(string: "{% customtag %}")
namespace.registerTag("custom") { parser, token in
return CustomNode()
}
let environment = Environment(namespace: namespace)
let result = try environment.renderTemplate(string: "{% custom %}")
try expect(result) == "Hello World" try expect(result) == "Hello World"
} }
$0.it("can render a simple custom tag") { $0.it("can render a simple custom tag") {
let namespace = Namespace() let result = try environment.renderTemplate(string: "{% simpletag %}")
namespace.registerSimpleTag("custom") { context in
return "Hello World"
}
let environment = Environment(namespace: namespace)
let result = try environment.renderTemplate(string: "{% custom %}")
try expect(result) == "Hello World" try expect(result) == "Hello World"
} }
} }

View File

@@ -15,11 +15,11 @@ along with template loaders.
let environment = Environment() 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 .. code-block:: swift
let environment = Environment(loader: ..., namespace: ...) let environment = Environment(loader: ..., extensions: [...])
Rendering a Template Rendering a Template
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~

View File

@@ -3,14 +3,14 @@ Custom Template Tags and Filters
You can build your own custom filters and tags and pass them down while 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 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 .. code-block:: swift
let namespace = Namespace() let ext = Extension()
// Register your filters and tags with the namespace // Register your filters and tags with the extension
let environment = Environment(namespace: namespace) let environment = Environment(extensions: [ext])
try environment.renderTemplate(name: "example.html") try environment.renderTemplate(name: "example.html")
Custom Filters Custom Filters
@@ -20,7 +20,7 @@ Registering custom filters:
.. code-block:: swift .. code-block:: swift
namespace.registerFilter("double") { (value: Any?) in ext.registerFilter("double") { (value: Any?) in
if let value = value as? Int { if let value = value as? Int {
return value * 2 return value * 2
} }
@@ -32,7 +32,7 @@ Registering custom filters with arguments:
.. code-block:: swift .. code-block:: swift
namespace.registerFilter("multiply") { (value: Any?, arguments: [Any?]) in ext.registerFilter("multiply") { (value: Any?, arguments: [Any?]) in
let amount: Int let amount: Int
if let value = arguments.first as? 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 .. code-block:: swift
namespace.registerSimpleTag("custom") { context in ext.registerSimpleTag("custom") { context in
return "Hello World" return "Hello World"
} }