Add 'Namespace' a container for tags and filters

This commit is contained in:
Kyle Fuller
2015-11-18 16:08:18 +03:00
parent 226becb258
commit dc774fe43b
11 changed files with 115 additions and 86 deletions

View File

@@ -96,7 +96,7 @@ When the `ForNode` is rendered in a context, it will look up the variable `artic
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 onto the `TokenParser` which you can access from your `Template`.
The tags are registered with a `Namespace` passed when rendering your `Template`.
#### Simple Tags
@@ -105,7 +105,7 @@ A simple tag is registered with a string for the tag name and a block of code wh
Heres an example. Registering a template tag called `custom` which just renders `Hello World` in the rendered template:
```swift
parser.registerSimpleTag("custom") { context in
namespace.registerSimpleTag("custom") { context in
return "Hello World"
}
```
@@ -120,7 +120,7 @@ You would use it as such in a template:
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 `TokenParser` 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.
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.
@@ -163,7 +163,7 @@ class DebugNode : Node {
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
parser.registerTag("debug") { parser, token in
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)

View File

@@ -112,18 +112,6 @@ For example, `Stencil` to `stencil`.
{{ "Stencil"|lowercase }}
```
#### Registering custom filters
```swift
template.parser.registerFilter("double") { value in
if let value = value as? Int {
return value * 2
}
return value
}
```
### Tags
Tags are a mechanism to execute a piece of code, allowing you to have
@@ -194,13 +182,37 @@ let context = Context(dictionary: [
])
```
### Customisation
You can build your own custom filters and tags and pass them down while
rendering your template. Any custom filters or tags must be registered
with a namespace which contains all filters and tags available to the template.
```swift
let namespace = Namespace()
// Register your filters and tags with the namespace
let rendered = try template.render(context, namespace: namespace)
```
#### Registering custom filters
```swift
namespace.registerFilter("double") { value in
if let value = value as? Int {
return value * 2
}
return value
}
```
#### Building custom tags
You can build a custom template tag. There are a couple of APIs to allow
you to write your own custom tags. The following is the simplest form:
```swift
template.parser.registerSimpleTag("custom") { context in
namespace.registerSimpleTag("custom") { context in
return "Hello World"
}
```
@@ -214,9 +226,8 @@ of template tags. You will need to call the `registerTag` API which accepts a
closure to handle the parsing. You can find examples of the `now`, `if` and
`for` tags found inside `Node.swift`.
Custom template tags must be registered prior to calling `Template.render` the first time.
The architecture of Stencil along with how to build advanced plugins can be found in the [architecture](ARCHITECTURE.md) document.
The architecture of Stencil along with how to build advanced plugins can be
found in the [architecture](ARCHITECTURE.md) document.
### Comments
@@ -230,4 +241,3 @@ To comment out part of your template, you can use the following syntax:
Stencil is licensed under the BSD license. See [LICENSE](LICENSE) for more
info.

44
Stencil/Namespace.swift Normal file
View File

@@ -0,0 +1,44 @@
public class Namespace {
public typealias TagParser = (TokenParser, Token) throws -> NodeType
var tags = [String: TagParser]()
var filters = [String: Filter]()
public init() {
registerDefaultTags()
registerDefaultFilters()
}
private func registerDefaultTags() {
registerTag("for", parser: ForNode.parse)
registerTag("if", parser: IfNode.parse)
registerTag("ifnot", parser: IfNode.parse_ifnot)
registerTag("now", parser: NowNode.parse)
registerTag("include", parser: IncludeNode.parse)
registerTag("extends", parser: ExtendsNode.parse)
registerTag("block", parser: BlockNode.parse)
}
private func registerDefaultFilters() {
registerFilter("capitalize", filter: capitalise)
registerFilter("uppercase", filter: uppercase)
registerFilter("lowercase", filter: lowercase)
}
/// Registers a new template tag
public func registerTag(name: String, parser: TagParser) {
tags[name] = parser
}
/// Registers a simple template tag with a name and a handler
public func registerSimpleTag(name: String, handler: Context throws -> String) {
registerTag(name, parser: { parser, token in
return SimpleNode(handler: handler)
})
}
/// Registers a template filter with the given name
public func registerFilter(name: String, filter: Filter) {
filters[name] = filter
}
}

View File

@@ -17,37 +17,11 @@ public class TokenParser {
public typealias TagParser = (TokenParser, Token) throws -> NodeType
private var tokens: [Token]
private var tags = [String:TagParser]()
private var filters = [String: Filter]()
private let namespace: Namespace
public init(tokens:[Token]) {
public init(tokens: [Token], namespace: Namespace) {
self.tokens = tokens
registerTag("for", parser: ForNode.parse)
registerTag("if", parser: IfNode.parse)
registerTag("ifnot", parser: IfNode.parse_ifnot)
registerTag("now", parser: NowNode.parse)
registerTag("include", parser: IncludeNode.parse)
registerTag("extends", parser: ExtendsNode.parse)
registerTag("block", parser: BlockNode.parse)
registerFilter("capitalize", filter: capitalise)
registerFilter("uppercase", filter: uppercase)
registerFilter("lowercase", filter: lowercase)
}
/// Registers a new template tag
public func registerTag(name:String, parser:TagParser) {
tags[name] = parser
}
/// Registers a simple template tag with a name and a handler
public func registerSimpleTag(name:String, handler:(Context throws -> String)) {
registerTag(name, parser: { parser, token in
return SimpleNode(handler: handler)
})
}
public func registerFilter(name: String, filter: Filter) {
filters[name] = filter
self.namespace = namespace
}
/// Parse the given tokens into nodes
@@ -75,7 +49,7 @@ public class TokenParser {
}
if let tag = tag {
if let parser = self.tags[tag] {
if let parser = namespace.tags[tag] {
nodes.append(try parser(self, token))
} else {
throw TemplateSyntaxError("Unknown template tag '\(tag)'")
@@ -102,7 +76,7 @@ public class TokenParser {
}
public func findFilter(name: String) throws -> Filter {
if let filter = filters[name] {
if let filter = namespace.filters[name] {
return filter
}

View File

@@ -3,8 +3,7 @@ import PathKit
/// A class representing a template
public class Template {
public let parser:TokenParser
private var nodes:[NodeType]? = nil
let tokens: [Token]
/// Create a template with the given name inside the given bundle
public convenience init(named:String, inBundle bundle:NSBundle? = nil) throws {
@@ -29,16 +28,13 @@ public class Template {
/// Create a template with a template string
public init(templateString:String) {
let lexer = Lexer(templateString: templateString)
let tokens = lexer.tokenize()
parser = TokenParser(tokens: tokens)
tokens = lexer.tokenize()
}
/// Render the given template
public func render(context:Context? = nil) throws -> String {
if nodes == nil {
nodes = try parser.parse()
}
return try renderNodes(nodes!, context ?? Context())
public func render(context: Context? = nil, namespace: Namespace? = nil) throws -> String {
let parser = TokenParser(tokens: tokens, namespace: namespace ?? Namespace())
let nodes = try parser.parse()
return try renderNodes(nodes, context ?? Context())
}
}

View File

@@ -7,7 +7,9 @@ describe("template filters") {
$0.it("allows you to register a custom filter") {
let template = Template(templateString: "{{ name|repeat }}")
template.parser.registerFilter("repeat") { value in
let namespace = Namespace()
namespace.registerFilter("repeat") { value in
if let value = value as? String {
return "\(value) \(value)"
}
@@ -15,17 +17,18 @@ describe("template filters") {
return nil
}
let result = try template.render(context)
let result = try template.render(context, namespace: namespace)
try expect(result) == "Kyle Kyle"
}
$0.it("allows you to register a custom filter") {
let template = Template(templateString: "{{ name|repeat }}")
template.parser.registerFilter("repeat") { value in
let namespace = Namespace()
namespace.registerFilter("repeat") { value in
throw TemplateSyntaxError("No Repeat")
}
try expect(try template.render(context)).toThrow(TemplateSyntaxError("No Repeat"))
try expect(try template.render(context, namespace: namespace)).toThrow(TemplateSyntaxError("No Repeat"))
}
}

View File

@@ -12,7 +12,7 @@ describe("IfNode") {
Token.Block(value: "endif")
]
let parser = TokenParser(tokens: tokens)
let parser = TokenParser(tokens: tokens, namespace: Namespace())
let nodes = try parser.parse()
let node = nodes.first as? IfNode
let trueNode = node?.trueNodes.first as? TextNode
@@ -35,7 +35,7 @@ describe("IfNode") {
Token.Block(value: "endif")
]
let parser = TokenParser(tokens: tokens)
let parser = TokenParser(tokens: tokens, namespace: Namespace())
let nodes = try parser.parse()
let node = nodes.first as? IfNode
let trueNode = node?.trueNodes.first as? TextNode
@@ -54,7 +54,7 @@ describe("IfNode") {
Token.Block(value: "if value"),
]
let parser = TokenParser(tokens: tokens)
let parser = TokenParser(tokens: tokens, namespace: Namespace())
let error = TemplateSyntaxError("`endif` was not found.")
try expect(try parser.parse()).toThrow(error)
}
@@ -64,7 +64,7 @@ describe("IfNode") {
Token.Block(value: "ifnot value"),
]
let parser = TokenParser(tokens: tokens)
let parser = TokenParser(tokens: tokens, namespace: Namespace())
let error = TemplateSyntaxError("`endif` was not found.")
try expect(try parser.parse()).toThrow(error)
}

View File

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

View File

@@ -6,7 +6,7 @@ describe("TokenParser") {
$0.it("can parse a text token") {
let parser = TokenParser(tokens: [
Token.Text(value: "Hello World")
])
], namespace: Namespace())
let nodes = try parser.parse()
let node = nodes.first as? TextNode
@@ -18,7 +18,7 @@ describe("TokenParser") {
$0.it("can parse a variable token") {
let parser = TokenParser(tokens: [
Token.Variable(value: "'name'")
])
], namespace: Namespace())
let nodes = try parser.parse()
let node = nodes.first as? VariableNode
@@ -30,7 +30,7 @@ describe("TokenParser") {
$0.it("can parse a comment token") {
let parser = TokenParser(tokens: [
Token.Comment(value: "Secret stuff!")
])
], namespace: Namespace())
let nodes = try parser.parse()
try expect(nodes.count) == 0
@@ -39,7 +39,7 @@ describe("TokenParser") {
$0.it("can parse a tag token") {
let parser = TokenParser(tokens: [
Token.Block(value: "now"),
])
], namespace: Namespace())
let nodes = try parser.parse()
try expect(nodes.count) == 1
@@ -48,7 +48,7 @@ describe("TokenParser") {
$0.it("errors when parsing an unknown tag") {
let parser = TokenParser(tokens: [
Token.Block(value: "unknown"),
])
], namespace: Namespace())
try expect(try parser.parse()).toThrow(TemplateSyntaxError("Unknown template tag 'unknown'"))
}

View File

@@ -40,11 +40,12 @@ describe("Stencil") {
let templateString = "{% custom %}"
let template = Template(templateString: templateString)
template.parser.registerTag("custom") { parser, token in
let namespace = Namespace()
namespace.registerTag("custom") { parser, token in
return CustomNode()
}
let result = try template.render()
let result = try template.render(namespace: namespace)
try expect(result) == "Hello World"
}
@@ -52,10 +53,11 @@ describe("Stencil") {
let templateString = "{% custom %}"
let template = Template(templateString: templateString)
template.parser.registerSimpleTag("custom") { context in
let namespace = Namespace()
namespace.registerSimpleTag("custom") { context in
return "Hello World"
}
try expect(try template.render()) == "Hello World"
try expect(try template.render(namespace: namespace)) == "Hello World"
}
}

View File

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