30 Commits
0.4.0 ... 0.5.2

Author SHA1 Message Date
Kyle Fuller
19d712b4a4 Release 0.5.2 2016-01-30 14:57:26 +01:00
Kyle Fuller
201b8e263c Fix failing for node tests
These we're broken in commit 0783506
2015-12-14 16:03:48 +00:00
Kyle Fuller
03928721c4 Move away from deprecated curry syntax 2015-12-14 09:04:18 -05:00
Kyle Fuller
07835063ed Fix an ambiguous array literal warning 2015-12-14 09:04:01 -05:00
Kyle Fuller
3c13d81b21 Whoops, Tests not Specs 2015-12-09 19:20:05 +00:00
Kyle Fuller
1668830d9b [for] Provide forloop context, first, last and counter 2015-12-09 19:18:16 +00:00
Kyle Fuller
14195b3199 Include missing specs 2015-12-09 19:18:08 +00:00
Kyle Fuller
ae75ea5911 [podspec] Correct PathKit dependency 2015-12-09 19:16:47 +00:00
Kyle Fuller
9c9ebbe559 Release 0.5.1 2015-12-08 18:08:44 +00:00
Kyle Fuller
5cdf1d326b Merge pull request #47 from neonichu/fix-manifest
Needs to depend on PathKit 0.6.x
2015-12-08 16:32:39 +00:00
Boris Bügling
f78562a1fd Needs to depend on PathKit 0.6.x
Seems like this is a bug in the current "stable" SPM which has been
fixed upstream in the meantime.
2015-12-08 17:31:12 +01:00
Kyle Fuller
0ccd8809e0 Merge pull request #46 from neonichu/linux-support
Support for Linux
2015-12-08 15:59:36 +00:00
Boris Bügling
356393088b Support for Linux 2015-12-08 16:54:58 +01:00
Kyle Fuller
b792cd09b9 Merge pull request #45 from neonichu/spm-support
Support for SPM
2015-12-08 12:28:55 +00:00
Boris Bügling
372b2e7576 Add Package.swift and move files around 2015-12-08 11:45:03 +01:00
Kyle Fuller
0bfd4134f9 Merge pull request #43 from njdehoog/filter_whitespace
Allow whitespace in filter expression
2015-11-24 14:33:17 +00:00
Niels de Hoog
aca0a3181d Allow whitespace in filter expression 2015-11-23 15:27:51 +01:00
Kyle Fuller
a1a268d5ac Merge pull request #41 from kylef/scanner
Replace NSRegularExpression with string scanning
2015-11-23 11:02:54 +00:00
Kyle Fuller
465834d89c Merge pull request #40 from njdehoog/array_any
Cast ForNode values to Array<Any>
2015-11-23 11:02:29 +00:00
Niels de Hoog
0af879ba8a Use switch syntax in resolve functions 2015-11-23 11:44:32 +01:00
Niels de Hoog
a516de51ff Update spec name to conform to style 2015-11-23 11:30:30 +01:00
Niels de Hoog
1f4aae1859 Added IfNode spec for Array<Any> value 2015-11-23 11:26:10 +01:00
Niels de Hoog
cba1cbe388 Updated specs for ForNode 2015-11-23 11:24:13 +01:00
Kyle Fuller
3722998c35 Replace NSRegularExpressions with string scanning 2015-11-21 16:27:24 +00:00
Kyle Fuller
22919dc5ce [Variable] Normalize resolved types into Swift types 2015-11-21 15:25:55 +00:00
Kyle Fuller
89b7da2e10 [Variable] Use Swift split over Foundation 2015-11-21 14:42:51 +00:00
Kyle Fuller
3bd3aec296 Resolve extends and include arguments as variables 2015-11-21 14:42:23 +00:00
Kyle Fuller
48a9a65bd5 [Token] Correctly split quoted components 2015-11-21 14:27:23 +00:00
Kyle Fuller
c86ab9c5b9 Remove unnessecary uses of Foundation 2015-11-21 14:06:15 +00:00
Kyle Fuller
dc774fe43b Add 'Namespace' a container for tags and filters 2015-11-18 16:10:27 +03:00
40 changed files with 593 additions and 300 deletions

2
.gitignore vendored
View File

@@ -1 +1,3 @@
.conche/
.build/
Packages/

1
.swift-version Normal file
View File

@@ -0,0 +1 @@
2.2-dev

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)

8
Package.swift Normal file
View 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),
]
)

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.

View File

@@ -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)
}
}
}

View File

@@ -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)")

View File

@@ -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
View 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
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

@@ -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
}

View File

@@ -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
}

View File

@@ -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())
}
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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
}

View File

@@ -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" ]
}
}
}

View File

@@ -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
}
}

View File

@@ -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>

View File

@@ -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"
}
}

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,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"
}
}

View 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"
}
}

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)
}
@@ -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"
}
}
}

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

@@ -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"
}
}

View File

@@ -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!"

View File

@@ -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") {

View File

@@ -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
View 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\""
}
}