Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19d712b4a4 | ||
|
|
201b8e263c | ||
|
|
03928721c4 | ||
|
|
07835063ed | ||
|
|
3c13d81b21 | ||
|
|
1668830d9b | ||
|
|
14195b3199 | ||
|
|
ae75ea5911 | ||
|
|
9c9ebbe559 | ||
|
|
5cdf1d326b | ||
|
|
f78562a1fd | ||
|
|
0ccd8809e0 | ||
|
|
356393088b | ||
|
|
b792cd09b9 | ||
|
|
372b2e7576 | ||
|
|
0bfd4134f9 | ||
|
|
aca0a3181d | ||
|
|
a1a268d5ac | ||
|
|
465834d89c | ||
|
|
0af879ba8a | ||
|
|
a516de51ff | ||
|
|
1f4aae1859 | ||
|
|
cba1cbe388 | ||
|
|
3722998c35 | ||
|
|
22919dc5ce | ||
|
|
89b7da2e10 | ||
|
|
3bd3aec296 | ||
|
|
48a9a65bd5 | ||
|
|
c86ab9c5b9 | ||
|
|
dc774fe43b |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
||||
.conche/
|
||||
.build/
|
||||
Packages/
|
||||
|
||||
1
.swift-version
Normal file
1
.swift-version
Normal file
@@ -0,0 +1 @@
|
||||
2.2-dev
|
||||
@@ -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 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 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
|
||||
Here’s 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 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 `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 node’s 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 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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
8
Package.swift
Normal file
8
Package.swift
Normal file
@@ -0,0 +1,8 @@
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Stencil",
|
||||
dependencies: [
|
||||
.Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 6),
|
||||
]
|
||||
)
|
||||
44
README.md
44
README.md
@@ -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.
|
||||
|
||||
|
||||
@@ -26,9 +26,10 @@ public class Context {
|
||||
|
||||
/// Set a variable in the current context, deleting the variable if it's nil
|
||||
set(value) {
|
||||
if var dictionary = dictionaries.popLast() {
|
||||
dictionary[key] = value
|
||||
dictionaries.append(dictionary)
|
||||
if let dictionary = dictionaries.popLast() {
|
||||
var mutable_dictionary = dictionary
|
||||
mutable_dictionary[key] = value
|
||||
dictionaries.append(mutable_dictionary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
|
||||
public class IncludeNode : NodeType {
|
||||
public let templateName:String
|
||||
public let templateName: Variable
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let bits = token.contents.componentsSeparatedByString("\"")
|
||||
public class func parse(parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
|
||||
guard bits.count == 3 else {
|
||||
guard bits.count == 2 else {
|
||||
throw TemplateSyntaxError("'include' tag takes one argument, the template file to be included")
|
||||
}
|
||||
|
||||
return IncludeNode(templateName: bits[1])
|
||||
return IncludeNode(templateName: Variable(bits[1]))
|
||||
}
|
||||
|
||||
public init(templateName:String) {
|
||||
public init(templateName: Variable) {
|
||||
self.templateName = templateName
|
||||
}
|
||||
|
||||
@@ -24,6 +23,10 @@ public class IncludeNode : NodeType {
|
||||
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 = loader.loadTemplate(templateName) else {
|
||||
let paths = loader.paths.map { $0.description }.joinWithSeparator(", ")
|
||||
throw TemplateSyntaxError("'\(templateName)' template not found in \(paths)")
|
||||
@@ -1,58 +1,60 @@
|
||||
import Foundation
|
||||
|
||||
class BlockContext {
|
||||
class var contextKey:String { return "block_context" }
|
||||
class var contextKey: String { return "block_context" }
|
||||
|
||||
var blocks:[String:BlockNode]
|
||||
var blocks: [String:BlockNode]
|
||||
|
||||
init(blocks:[String:BlockNode]) {
|
||||
init(blocks: [String:BlockNode]) {
|
||||
self.blocks = blocks
|
||||
}
|
||||
|
||||
func pop(blockName:String) -> BlockNode? {
|
||||
func pop(blockName: String) -> BlockNode? {
|
||||
return blocks.removeValueForKey(blockName)
|
||||
}
|
||||
}
|
||||
|
||||
func any<Element>(elements:[Element], closure:(Element -> Bool)) -> Element? {
|
||||
for element in elements {
|
||||
if closure(element) {
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
extension CollectionType {
|
||||
func any(closure: Generator.Element -> Bool) -> Generator.Element? {
|
||||
for element in self {
|
||||
if closure(element) {
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ExtendsNode : NodeType {
|
||||
let templateName:String
|
||||
let blocks:[String:BlockNode]
|
||||
let templateName: Variable
|
||||
let blocks: [String:BlockNode]
|
||||
|
||||
class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let bits = token.contents.componentsSeparatedByString("\"")
|
||||
class func parse(parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
|
||||
guard bits.count == 3 else {
|
||||
guard bits.count == 2 else {
|
||||
throw TemplateSyntaxError("'extends' takes one argument, the template file to be extended")
|
||||
}
|
||||
|
||||
let parsedNodes = try parser.parse()
|
||||
guard (any(parsedNodes) { $0 is ExtendsNode }) == nil else {
|
||||
guard (parsedNodes.any { $0 is ExtendsNode }) == nil else {
|
||||
throw TemplateSyntaxError("'extends' cannot appear more than once in the same template")
|
||||
}
|
||||
|
||||
let blockNodes = parsedNodes.filter { node in node is BlockNode }
|
||||
|
||||
let nodes = blockNodes.reduce([String:BlockNode](), combine: { (accumulator, node:NodeType) -> [String:BlockNode] in
|
||||
let nodes = blockNodes.reduce([String:BlockNode]()) { (accumulator, node:NodeType) -> [String:BlockNode] in
|
||||
let node = (node as! BlockNode)
|
||||
var dict = accumulator
|
||||
dict[node.name] = node
|
||||
return dict
|
||||
})
|
||||
}
|
||||
|
||||
return ExtendsNode(templateName: bits[1], blocks: nodes)
|
||||
return ExtendsNode(templateName: Variable(bits[1]), blocks: nodes)
|
||||
}
|
||||
|
||||
init(templateName:String, blocks:[String:BlockNode]) {
|
||||
init(templateName: Variable, blocks: [String: BlockNode]) {
|
||||
self.templateName = templateName
|
||||
self.blocks = blocks
|
||||
}
|
||||
@@ -62,6 +64,10 @@ class ExtendsNode : NodeType {
|
||||
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 = loader.loadTemplate(templateName) else {
|
||||
let paths:String = loader.paths.map { $0.description }.joinWithSeparator(", ")
|
||||
throw TemplateSyntaxError("'\(templateName)' template not found in \(paths)")
|
||||
@@ -75,11 +81,12 @@ class ExtendsNode : NodeType {
|
||||
}
|
||||
}
|
||||
|
||||
class BlockNode : NodeType {
|
||||
let name:String
|
||||
let nodes:[NodeType]
|
||||
|
||||
class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
class BlockNode : NodeType {
|
||||
let name: String
|
||||
let nodes: [NodeType]
|
||||
|
||||
class func parse(parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
|
||||
guard bits.count == 2 else {
|
||||
@@ -92,7 +99,7 @@ class BlockNode : NodeType {
|
||||
return BlockNode(name:blockName, nodes:nodes)
|
||||
}
|
||||
|
||||
init(name:String, nodes:[NodeType]) {
|
||||
init(name: String, nodes: [NodeType]) {
|
||||
self.name = name
|
||||
self.nodes = nodes
|
||||
}
|
||||
147
Sources/Lexer.swift
Normal file
147
Sources/Lexer.swift
Normal file
@@ -0,0 +1,147 @@
|
||||
public struct Lexer {
|
||||
public let templateString: String
|
||||
|
||||
public init(templateString: String) {
|
||||
self.templateString = templateString
|
||||
}
|
||||
|
||||
func createToken(string:String) -> Token {
|
||||
func strip() -> String {
|
||||
return string[string.startIndex.successor().successor()..<string.endIndex.predecessor().predecessor()].trim(" ")
|
||||
}
|
||||
|
||||
if string.hasPrefix("{{") {
|
||||
return Token.Variable(value: strip())
|
||||
} else if string.hasPrefix("{%") {
|
||||
return Token.Block(value: strip())
|
||||
} else if string.hasPrefix("{#") {
|
||||
return Token.Comment(value: strip())
|
||||
}
|
||||
|
||||
return Token.Text(value: string)
|
||||
}
|
||||
|
||||
/// Returns an array of tokens from a given template string.
|
||||
public func tokenize() -> [Token] {
|
||||
var tokens: [Token] = []
|
||||
|
||||
let scanner = Scanner(templateString)
|
||||
|
||||
let map = [
|
||||
"{{": "}}",
|
||||
"{%": "%}",
|
||||
"{#": "#}",
|
||||
]
|
||||
|
||||
while !scanner.isEmpty {
|
||||
if let text = scanner.scan(until: ["{{", "{%", "{#"]) {
|
||||
if !text.1.isEmpty {
|
||||
tokens.append(createToken(text.1))
|
||||
}
|
||||
|
||||
let end = map[text.0]!
|
||||
let result = scanner.scan(until: end, returnUntil: true)
|
||||
tokens.append(createToken(result))
|
||||
} else {
|
||||
tokens.append(createToken(scanner.content))
|
||||
scanner.content = ""
|
||||
}
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Scanner {
|
||||
var content: String
|
||||
|
||||
init(_ content: String) {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var isEmpty: Bool {
|
||||
return content.isEmpty
|
||||
}
|
||||
|
||||
func scan(until until: String, returnUntil: Bool = false) -> String {
|
||||
if until.isEmpty {
|
||||
return ""
|
||||
}
|
||||
|
||||
var index = content.startIndex
|
||||
while index != content.endIndex {
|
||||
let substring = content[index..<content.endIndex]
|
||||
if substring.hasPrefix(until) {
|
||||
let result = content[content.startIndex..<index]
|
||||
content = substring
|
||||
|
||||
if returnUntil {
|
||||
content = content[until.endIndex..<content.endIndex]
|
||||
return result + until
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
index = index.successor()
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func scan(until until: [String]) -> (String, String)? {
|
||||
if until.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
var index = content.startIndex
|
||||
while index != content.endIndex {
|
||||
let substring = content[index..<content.endIndex]
|
||||
for string in until {
|
||||
if substring.hasPrefix(string) {
|
||||
let result = content[content.startIndex..<index]
|
||||
content = substring
|
||||
return (string, result)
|
||||
}
|
||||
}
|
||||
|
||||
index = index.successor()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension String {
|
||||
func findFirstNot(character: Character) -> String.Index? {
|
||||
var index = startIndex
|
||||
while index != endIndex {
|
||||
if character != self[index] {
|
||||
return index
|
||||
}
|
||||
index = index.successor()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findLastNot(character: Character) -> String.Index? {
|
||||
var index = endIndex.predecessor()
|
||||
while index != startIndex {
|
||||
if character != self[index] {
|
||||
return index.successor()
|
||||
}
|
||||
index = index.predecessor()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func trim(character: Character) -> String {
|
||||
let first = findFirstNot(character) ?? startIndex
|
||||
let last = findLastNot(character) ?? endIndex
|
||||
return self[first..<last]
|
||||
}
|
||||
}
|
||||
44
Sources/Namespace.swift
Normal file
44
Sources/Namespace.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -157,9 +157,16 @@ public class ForNode : NodeType {
|
||||
public func render(context: Context) throws -> String {
|
||||
let values = try variable.resolve(context)
|
||||
|
||||
if let values = values as? NSArray where values.count > 0 {
|
||||
return try values.map { item in
|
||||
try context.push([loopVariable: item]) {
|
||||
if let values = values as? [Any] where values.count > 0 {
|
||||
let count = values.count
|
||||
return try values.enumerate().map { index, item in
|
||||
let forContext: [String: Any] = [
|
||||
"first": index == 0,
|
||||
"last": index == (count - 1),
|
||||
"counter": index + 1,
|
||||
]
|
||||
|
||||
return try context.push([loopVariable: item, "forloop": forContext]) {
|
||||
try renderNodes(nodes, context)
|
||||
}
|
||||
}.joinWithSeparator("")
|
||||
@@ -232,10 +239,10 @@ public class IfNode : NodeType {
|
||||
let result = try variable.resolve(context)
|
||||
var truthy = false
|
||||
|
||||
if let result = result as? NSArray {
|
||||
if result.count > 0 {
|
||||
truthy = true
|
||||
}
|
||||
if let result = result as? [Any] {
|
||||
truthy = !result.isEmpty
|
||||
} else if let result = result as? [String:Any] {
|
||||
truthy = !result.isEmpty
|
||||
} else if result != nil {
|
||||
truthy = true
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
public func until(tags:[String])(parser:TokenParser, token:Token) -> Bool {
|
||||
if let name = token.components().first {
|
||||
for tag in tags {
|
||||
if name == tag {
|
||||
return true
|
||||
public func until(tags: [String]) -> ((TokenParser, Token) -> Bool) {
|
||||
return { parser, token in
|
||||
if let name = token.components().first {
|
||||
for tag in tags {
|
||||
if name == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Filter = Any? throws -> Any?
|
||||
@@ -16,38 +18,12 @@ public typealias Filter = Any? throws -> Any?
|
||||
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 var tokens: [Token]
|
||||
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 +51,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 +78,7 @@ public class TokenParser {
|
||||
}
|
||||
|
||||
public func findFilter(name: String) throws -> Filter {
|
||||
if let filter = filters[name] {
|
||||
if let filter = namespace.filters[name] {
|
||||
return filter
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
#if os(Linux)
|
||||
let NSFileNoSuchFileError = 4
|
||||
#endif
|
||||
|
||||
/// 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 +32,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())
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,26 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
|
||||
// A class for loading a template from disk
|
||||
public class TemplateLoader {
|
||||
public let paths:[Path]
|
||||
public let paths: [Path]
|
||||
|
||||
public init(paths:[Path]) {
|
||||
public init(paths: [Path]) {
|
||||
self.paths = paths
|
||||
}
|
||||
|
||||
public init(bundle:[NSBundle]) {
|
||||
public init(bundle: [NSBundle]) {
|
||||
self.paths = bundle.map {
|
||||
return Path($0.bundlePath)
|
||||
}
|
||||
}
|
||||
|
||||
public func loadTemplate(templateName:String) -> Template? {
|
||||
public func loadTemplate(templateName: String) -> Template? {
|
||||
return loadTemplate([templateName])
|
||||
}
|
||||
|
||||
public func loadTemplate(templateNames:[String]) -> Template? {
|
||||
public func loadTemplate(templateNames: [String]) -> Template? {
|
||||
for path in paths {
|
||||
for templateName in templateNames {
|
||||
let templatePath = path + Path(templateName)
|
||||
@@ -1,41 +1,68 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
/// Split a string by spaces leaving quoted phrases together
|
||||
func smartSplit(value: String) -> [String] {
|
||||
var word = ""
|
||||
var separator: Character = " "
|
||||
var components: [String] = []
|
||||
|
||||
for character in value.characters {
|
||||
if character == separator {
|
||||
if separator != " " {
|
||||
word.append(separator)
|
||||
}
|
||||
|
||||
if !word.isEmpty {
|
||||
components.append(word)
|
||||
word = ""
|
||||
}
|
||||
|
||||
separator = " "
|
||||
} else {
|
||||
if separator == " " && (character == "'" || character == "\"") {
|
||||
separator = character
|
||||
}
|
||||
word.append(character)
|
||||
}
|
||||
}
|
||||
|
||||
if !word.isEmpty {
|
||||
components.append(word)
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
|
||||
public enum Token : Equatable {
|
||||
/// A token representing a piece of text.
|
||||
case Text(value:String)
|
||||
case Text(value: String)
|
||||
|
||||
/// A token representing a variable.
|
||||
case Variable(value:String)
|
||||
case Variable(value: String)
|
||||
|
||||
/// A token representing a comment.
|
||||
case Comment(value:String)
|
||||
case Comment(value: String)
|
||||
|
||||
/// A token representing a template block.
|
||||
case Block(value:String)
|
||||
case Block(value: String)
|
||||
|
||||
/// Returns the underlying value as an array seperated by spaces
|
||||
public func components() -> [String] {
|
||||
// TODO: Make this smarter and treat quoted strings as a single component
|
||||
let characterSet = NSCharacterSet.whitespaceAndNewlineCharacterSet()
|
||||
|
||||
func strip(value: String) -> [String] {
|
||||
return value.stringByTrimmingCharactersInSet(characterSet).componentsSeparatedByCharactersInSet(characterSet)
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .Block(let value):
|
||||
return strip(value)
|
||||
return smartSplit(value)
|
||||
case .Variable(let value):
|
||||
return strip(value)
|
||||
return smartSplit(value)
|
||||
case .Text(let value):
|
||||
return strip(value)
|
||||
return smartSplit(value)
|
||||
case .Comment(let value):
|
||||
return strip(value)
|
||||
return smartSplit(value)
|
||||
}
|
||||
}
|
||||
|
||||
public var contents:String {
|
||||
public var contents: String {
|
||||
switch self {
|
||||
case .Block(let value):
|
||||
return value
|
||||
@@ -49,7 +76,8 @@ public enum Token : Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public func ==(lhs:Token, rhs:Token) -> Bool {
|
||||
|
||||
public func == (lhs: Token, rhs: Token) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.Text(let lhsValue), .Text(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
@@ -6,7 +6,7 @@ class FilterExpression : Resolvable {
|
||||
let variable: Variable
|
||||
|
||||
init(token: String, parser: TokenParser) throws {
|
||||
let bits = token.componentsSeparatedByString("|")
|
||||
let bits = token.characters.split("|").map({ String($0).trim(" ") })
|
||||
if bits.isEmpty {
|
||||
filters = []
|
||||
variable = Variable("")
|
||||
@@ -35,19 +35,19 @@ class FilterExpression : Resolvable {
|
||||
|
||||
/// A structure used to represent a template variable, and to resolve it in a given context.
|
||||
public struct Variable : Equatable, Resolvable {
|
||||
public let variable:String
|
||||
public let variable: String
|
||||
|
||||
/// Create a variable with a string representing the variable
|
||||
public init(_ variable:String) {
|
||||
public init(_ variable: String) {
|
||||
self.variable = variable
|
||||
}
|
||||
|
||||
private func lookup() -> [String] {
|
||||
return variable.componentsSeparatedByString(".")
|
||||
return variable.characters.split(".").map(String.init)
|
||||
}
|
||||
|
||||
/// Resolve the variable in the given context
|
||||
public func resolve(context:Context) throws -> Any? {
|
||||
public func resolve(context: Context) throws -> Any? {
|
||||
var current: Any? = context
|
||||
|
||||
if (variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\"")) {
|
||||
@@ -58,11 +58,9 @@ public struct Variable : Equatable, Resolvable {
|
||||
for bit in lookup() {
|
||||
if let context = current as? Context {
|
||||
current = context[bit]
|
||||
} else if let dictionary = current as? [String: Any] {
|
||||
} else if let dictionary = resolveDictionary(current) {
|
||||
current = dictionary[bit]
|
||||
} else if let dictionary = current as? [String: AnyObject] {
|
||||
current = dictionary[bit]
|
||||
} else if let array = current as? [Any] {
|
||||
} else if let array = resolveArray(current) {
|
||||
if let index = Int(bit) {
|
||||
current = array[index]
|
||||
} else if bit == "first" {
|
||||
@@ -72,27 +70,70 @@ public struct Variable : Equatable, Resolvable {
|
||||
} else if bit == "count" {
|
||||
current = array.count
|
||||
}
|
||||
} else if let array = current as? NSArray {
|
||||
if let index = Int(bit) {
|
||||
current = array[index]
|
||||
} else if bit == "first" {
|
||||
current = array.firstObject
|
||||
} else if bit == "last" {
|
||||
current = array.lastObject
|
||||
} else if bit == "count" {
|
||||
current = array.count
|
||||
}
|
||||
} else if let object = current as? NSObject {
|
||||
} else if let object = current as? NSObject { // NSKeyValueCoding
|
||||
#if os(Linux)
|
||||
return nil
|
||||
#else
|
||||
current = object.valueForKey(bit)
|
||||
#endif
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return current
|
||||
return normalize(current)
|
||||
}
|
||||
}
|
||||
|
||||
public func ==(lhs:Variable, rhs:Variable) -> Bool {
|
||||
public func ==(lhs: Variable, rhs: Variable) -> Bool {
|
||||
return lhs.variable == rhs.variable
|
||||
}
|
||||
|
||||
|
||||
func resolveDictionary(current: Any?) -> [String: Any]? {
|
||||
switch current {
|
||||
case let dictionary as [String: Any]:
|
||||
return dictionary
|
||||
case let dictionary as [String: AnyObject]:
|
||||
var result: [String: Any] = [:]
|
||||
for (k, v) in dictionary {
|
||||
result[k] = v as Any
|
||||
}
|
||||
return result
|
||||
case let dictionary as NSDictionary:
|
||||
var result: [String: Any] = [:]
|
||||
for (k, v) in dictionary {
|
||||
if let k = k as? String {
|
||||
result[k] = v as Any
|
||||
}
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func resolveArray(current: Any?) -> [Any]? {
|
||||
switch current {
|
||||
case let array as [Any]:
|
||||
return array
|
||||
case let array as [AnyObject]:
|
||||
return array.map { $0 as Any }
|
||||
case let array as NSArray:
|
||||
return array.map { $0 as Any }
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func normalize(current: Any?) -> Any? {
|
||||
if let array = resolveArray(current) {
|
||||
return array
|
||||
}
|
||||
|
||||
if let dictionary = resolveDictionary(current) {
|
||||
return dictionary
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Stencil",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.2",
|
||||
"summary": "Stencil is a simple and powerful template language for Swift.",
|
||||
"homepage": "https://github.com/kylef/Stencil",
|
||||
"license": {
|
||||
@@ -8,16 +8,15 @@
|
||||
"file": "LICENSE"
|
||||
},
|
||||
"authors": {
|
||||
"Kyle Fuller": "inbox@kylefuller.co.uk"
|
||||
"Kyle Fuller": "kyle@fuller.li"
|
||||
},
|
||||
"social_media_url": "http://twitter.com/kylefuller",
|
||||
"source": {
|
||||
"git": "https://github.com/kylef/Stencil.git",
|
||||
"tag": "0.4.0"
|
||||
"tag": "0.5.2"
|
||||
},
|
||||
"source_files": [
|
||||
"Stencil/*.swift",
|
||||
"Stencil/TemplateLoader/*.swift"
|
||||
"Sources/*.swift"
|
||||
],
|
||||
"platforms": {
|
||||
"ios": "8.0",
|
||||
@@ -25,17 +24,17 @@
|
||||
},
|
||||
"requires_arc": true,
|
||||
"dependencies": {
|
||||
"PathKit": [ "~> 0.5.0" ]
|
||||
"PathKit": [ "~> 0.6.0" ]
|
||||
},
|
||||
"test_specification": {
|
||||
"source_files": [
|
||||
"StencilSpecs/*.swift",
|
||||
"StencilSpecs/TemplateLoader/*.swift",
|
||||
"StencilSpecs/Nodes/*.swift"
|
||||
"Tests/*.swift",
|
||||
"Tests/TemplateLoader/*.swift",
|
||||
"Tests/Nodes/*.swift"
|
||||
],
|
||||
"dependencies": {
|
||||
"Spectre": [ "~> 0.4.1" ],
|
||||
"PathKit": [ "~> 0.5.0" ]
|
||||
"Spectre": [ "~> 0.5.0" ],
|
||||
"PathKit": [ "~> 0.6.0" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public struct Lexer {
|
||||
public let templateString:String
|
||||
let regex = try! NSRegularExpression(pattern: "(\\{\\{.*?\\}\\}|\\{%.*?%\\}|\\{#.*?#\\})", options: [])
|
||||
|
||||
public init(templateString:String) {
|
||||
self.templateString = templateString
|
||||
}
|
||||
|
||||
func createToken(string:String) -> Token {
|
||||
func strip() -> String {
|
||||
return string[string.startIndex.successor().successor()..<string.endIndex.predecessor().predecessor()].stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
|
||||
}
|
||||
|
||||
if string.hasPrefix("{{") {
|
||||
return Token.Variable(value: strip())
|
||||
} else if string.hasPrefix("{%") {
|
||||
return Token.Block(value: strip())
|
||||
} else if string.hasPrefix("{#") {
|
||||
return Token.Comment(value: strip())
|
||||
}
|
||||
|
||||
return Token.Text(value: string)
|
||||
}
|
||||
|
||||
/// Returns an array of tokens from a given template string.
|
||||
public func tokenize() -> [Token] {
|
||||
// Unfortunately NSRegularExpression doesn't have a split.
|
||||
// So here's a really terrible implementation
|
||||
|
||||
var tokens = [Token]()
|
||||
|
||||
let range = NSMakeRange(0, templateString.characters.count)
|
||||
var lastIndex = 0
|
||||
let nsTemplateString = templateString as NSString
|
||||
let options = NSMatchingOptions(rawValue: 0)
|
||||
regex.enumerateMatchesInString(templateString, options: options, range: range) { (result, flags, b) in
|
||||
if let result = result {
|
||||
if result.range.location != lastIndex {
|
||||
let previousMatch = nsTemplateString.substringWithRange(NSMakeRange(lastIndex, result.range.location - lastIndex))
|
||||
tokens.append(self.createToken(previousMatch))
|
||||
}
|
||||
|
||||
let match = nsTemplateString.substringWithRange(result.range)
|
||||
tokens.append(self.createToken(match))
|
||||
|
||||
lastIndex = result.range.location + result.range.length
|
||||
}
|
||||
}
|
||||
|
||||
if lastIndex < templateString.characters.count {
|
||||
let substring = (templateString as NSString).substringFromIndex(lastIndex)
|
||||
tokens.append(Token.Text(value: substring))
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// Stencil.h
|
||||
// Stencil
|
||||
//
|
||||
// Created by Kyle Fuller on 23/10/2014.
|
||||
// Copyright (c) 2014 Cocode. All rights reserved.
|
||||
// See LICENSE for more details.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for Stencil.
|
||||
FOUNDATION_EXPORT double StencilVersionNumber;
|
||||
|
||||
//! Project version string for Stencil.
|
||||
FOUNDATION_EXPORT const unsigned char StencilVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <Stencil/PublicHeader.h>
|
||||
@@ -1,23 +0,0 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
describe("ForNode") {
|
||||
let context = Context(dictionary: [
|
||||
"items": [1, 2, 3],
|
||||
"emptyItems": [Int](),
|
||||
])
|
||||
|
||||
$0.it("renders the given nodes for each item") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "123"
|
||||
}
|
||||
|
||||
$0.it("renders the given empty nodes when no items found item") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||
let node = ForNode(variable: "emptyItems", loopVariable: "item", nodes: nodes, emptyNodes: emptyNodes)
|
||||
try expect(try node.render(context)) == "empty"
|
||||
}
|
||||
}
|
||||
@@ -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,24 @@ 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"))
|
||||
}
|
||||
|
||||
$0.it("allows whitespace in expression") {
|
||||
let template = Template(templateString: "{{ name | uppercase }}")
|
||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
||||
try expect(result) == "KYLE"
|
||||
}
|
||||
}
|
||||
|
||||
62
Tests/Nodes/ForNodeSpec.swift
Normal file
62
Tests/Nodes/ForNodeSpec.swift
Normal file
@@ -0,0 +1,62 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import Foundation
|
||||
|
||||
|
||||
describe("ForNode") {
|
||||
let context = Context(dictionary: [
|
||||
"items": [1, 2, 3],
|
||||
"emptyItems": [Int](),
|
||||
])
|
||||
|
||||
$0.it("renders the given nodes for each item") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "123"
|
||||
}
|
||||
|
||||
$0.it("renders the given empty nodes when no items found item") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||
let node = ForNode(variable: "emptyItems", loopVariable: "item", nodes: nodes, emptyNodes: emptyNodes)
|
||||
try expect(try node.render(context)) == "empty"
|
||||
}
|
||||
|
||||
$0.it("renders a context variable of type Array<Any>") {
|
||||
let any_context = Context(dictionary: [
|
||||
"items": ([1, 2, 3] as [Any])
|
||||
])
|
||||
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(any_context)) == "123"
|
||||
}
|
||||
|
||||
$0.it("renders a context variable of type NSArray") {
|
||||
let nsarray_context = Context(dictionary: [
|
||||
"items": NSArray(array: [1, 2, 3])
|
||||
])
|
||||
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(nsarray_context)) == "123"
|
||||
}
|
||||
|
||||
$0.it("renders the given nodes while providing if the item is first in the context") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.first")]
|
||||
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "1true2false3false"
|
||||
}
|
||||
|
||||
$0.it("renders the given nodes while providing if the item is last in the context") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.last")]
|
||||
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "1false2false3true"
|
||||
}
|
||||
|
||||
$0.it("renders the given nodes while providing item counter") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
|
||||
let node = ForNode(variable: "items", loopVariable: "item", nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "112233"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -96,5 +96,18 @@ describe("IfNode") {
|
||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
try expect(try node.render(arrayContext)) == "false"
|
||||
}
|
||||
|
||||
$0.it("renders the false when dictionary expression is empty") {
|
||||
let emptyItems = [String:AnyObject]()
|
||||
let arrayContext = Context(dictionary: ["items": emptyItems])
|
||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
try expect(try node.render(arrayContext)) == "false"
|
||||
}
|
||||
|
||||
$0.it("renders the false when Array<Any> variable is empty") {
|
||||
let arrayContext = Context(dictionary: ["items": ([] as [Any])])
|
||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
try expect(try node.render(arrayContext)) == "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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'"))
|
||||
}
|
||||
@@ -38,24 +38,26 @@ describe("Stencil") {
|
||||
|
||||
$0.it("can render a custom template tag") {
|
||||
let templateString = "{% custom %}"
|
||||
let template = Template(templateString:templateString)
|
||||
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"
|
||||
}
|
||||
|
||||
$0.it("can render a simple custom tag") {
|
||||
let templateString = "{% custom %}"
|
||||
let template = Template(templateString:templateString)
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,13 @@ import PathKit
|
||||
|
||||
|
||||
describe("Include") {
|
||||
let path = Path(__FILE__) + ".." + ".." + "StencilSpecs" + "fixtures"
|
||||
let path = Path(__FILE__) + ".." + ".." + "Tests" + "fixtures"
|
||||
let loader = TemplateLoader(paths: [path])
|
||||
|
||||
$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,18 +18,18 @@ 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
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.templateName) == "test.html"
|
||||
try expect(node?.templateName) == Variable("\"test.html\"")
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("rendering") {
|
||||
$0.it("throws an error when rendering without a loader") {
|
||||
let node = IncludeNode(templateName: "test.html")
|
||||
let node = IncludeNode(templateName: Variable("\"test.html\""))
|
||||
|
||||
do {
|
||||
try node.render(Context())
|
||||
@@ -39,7 +39,7 @@ describe("Include") {
|
||||
}
|
||||
|
||||
$0.it("throws an error when it cannot find the included template") {
|
||||
let node = IncludeNode(templateName: "unknown.html")
|
||||
let node = IncludeNode(templateName: Variable("\"unknown.html\""))
|
||||
|
||||
do {
|
||||
try node.render(Context(dictionary: ["loader": loader]))
|
||||
@@ -49,7 +49,7 @@ describe("Include") {
|
||||
}
|
||||
|
||||
$0.it("successfully renders a found included template") {
|
||||
let node = IncludeNode(templateName: "test.html")
|
||||
let node = IncludeNode(templateName: Variable("\"test.html\""))
|
||||
let context = Context(dictionary: ["loader":loader, "target": "World"])
|
||||
let value = try node.render(context)
|
||||
try expect(value) == "Hello World!"
|
||||
@@ -4,7 +4,7 @@ import PathKit
|
||||
|
||||
|
||||
describe("Inheritence") {
|
||||
let path = Path(__FILE__) + ".." + ".." + "StencilSpecs" + "fixtures"
|
||||
let path = Path(__FILE__) + ".." + ".." + "Tests" + "fixtures"
|
||||
let loader = TemplateLoader(paths: [path])
|
||||
|
||||
$0.it("can inherit from another template") {
|
||||
@@ -4,7 +4,7 @@ import PathKit
|
||||
|
||||
|
||||
describe("TemplateLoader") {
|
||||
let path = Path(__FILE__) + ".." + ".." + "StencilSpecs" + "fixtures"
|
||||
let path = Path(__FILE__) + ".." + ".." + "Tests" + "fixtures"
|
||||
let loader = TemplateLoader(paths: [path])
|
||||
|
||||
$0.it("returns nil when a template cannot be found") {
|
||||
32
Tests/TokenSpec.swift
Normal file
32
Tests/TokenSpec.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
describe("Token") {
|
||||
$0.it("can split the contents into components") {
|
||||
let token = Token.Text(value: "hello world")
|
||||
let components = token.components()
|
||||
|
||||
try expect(components.count) == 2
|
||||
try expect(components[0]) == "hello"
|
||||
try expect(components[1]) == "world"
|
||||
}
|
||||
|
||||
$0.it("can split the contents into components with single quoted strings") {
|
||||
let token = Token.Text(value: "hello 'kyle fuller'")
|
||||
let components = token.components()
|
||||
|
||||
try expect(components.count) == 2
|
||||
try expect(components[0]) == "hello"
|
||||
try expect(components[1]) == "'kyle fuller'"
|
||||
}
|
||||
|
||||
$0.it("can split the contents into components with double quoted strings") {
|
||||
let token = Token.Text(value: "hello \"kyle fuller\"")
|
||||
let components = token.components()
|
||||
|
||||
try expect(components.count) == 2
|
||||
try expect(components[0]) == "hello"
|
||||
try expect(components[1]) == "\"kyle fuller\""
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user