Add Package.swift and move files around
This commit is contained in:
54
Sources/Context.swift
Normal file
54
Sources/Context.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
/// A container for template variables.
|
||||
public class Context {
|
||||
var dictionaries:[[String: Any]]
|
||||
|
||||
/// Initialise a Context with a dictionary
|
||||
public init(dictionary:[String: Any]) {
|
||||
dictionaries = [dictionary]
|
||||
}
|
||||
|
||||
/// Initialise an empty Context
|
||||
public init() {
|
||||
dictionaries = []
|
||||
}
|
||||
|
||||
public subscript(key: String) -> Any? {
|
||||
/// Retrieves a variable's value, starting at the current context and going upwards
|
||||
get {
|
||||
for dictionary in Array(dictionaries.reverse()) {
|
||||
if let value = dictionary[key] {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Set a variable in the current context, deleting the variable if it's nil
|
||||
set(value) {
|
||||
if let dictionary = dictionaries.popLast() {
|
||||
var mutable_dictionary = dictionary
|
||||
mutable_dictionary[key] = value
|
||||
dictionaries.append(mutable_dictionary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a new level into the Context
|
||||
public func push(dictionary: [String: Any]? = nil) {
|
||||
dictionaries.append(dictionary ?? [:])
|
||||
}
|
||||
|
||||
/// Pop the last level off of the Context
|
||||
public func pop() -> [String: Any]? {
|
||||
return dictionaries.popLast()
|
||||
}
|
||||
|
||||
/// Push a new level onto the context for the duration of the execution of the given closure
|
||||
public func push<Result>(dictionary: [String: Any]? = nil, @noescape closure: (() throws -> Result)) rethrows -> Result {
|
||||
push(dictionary)
|
||||
let result = try closure()
|
||||
pop()
|
||||
return result
|
||||
}
|
||||
}
|
||||
33
Sources/Filters.swift
Normal file
33
Sources/Filters.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
func toString(value: Any?) -> String? {
|
||||
if let value = value as? String {
|
||||
return value
|
||||
} else if let value = value as? CustomStringConvertible {
|
||||
return value.description
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func capitalise(value: Any?) -> Any? {
|
||||
if let value = toString(value) {
|
||||
return value.capitalizedString
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func uppercase(value: Any?) -> Any? {
|
||||
if let value = toString(value) {
|
||||
return value.uppercaseString
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func lowercase(value: Any?) -> Any? {
|
||||
if let value = toString(value) {
|
||||
return value.lowercaseString
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
38
Sources/Include.swift
Normal file
38
Sources/Include.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
import PathKit
|
||||
|
||||
|
||||
public class IncludeNode : NodeType {
|
||||
public let templateName: Variable
|
||||
|
||||
public class func parse(parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
|
||||
guard bits.count == 2 else {
|
||||
throw TemplateSyntaxError("'include' tag takes one argument, the template file to be included")
|
||||
}
|
||||
|
||||
return IncludeNode(templateName: Variable(bits[1]))
|
||||
}
|
||||
|
||||
public init(templateName: Variable) {
|
||||
self.templateName = templateName
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
guard let loader = context["loader"] as? TemplateLoader else {
|
||||
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)")
|
||||
}
|
||||
|
||||
return try template.render(context)
|
||||
}
|
||||
}
|
||||
|
||||
28
Sources/Info.plist
Normal file
28
Sources/Info.plist
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2014 Cocode. All rights reserved.</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
114
Sources/Inheritence.swift
Normal file
114
Sources/Inheritence.swift
Normal file
@@ -0,0 +1,114 @@
|
||||
class BlockContext {
|
||||
class var contextKey: String { return "block_context" }
|
||||
|
||||
var blocks: [String:BlockNode]
|
||||
|
||||
init(blocks: [String:BlockNode]) {
|
||||
self.blocks = blocks
|
||||
}
|
||||
|
||||
func pop(blockName: String) -> BlockNode? {
|
||||
return blocks.removeValueForKey(blockName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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: Variable
|
||||
let blocks: [String:BlockNode]
|
||||
|
||||
class func parse(parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
|
||||
guard bits.count == 2 else {
|
||||
throw TemplateSyntaxError("'extends' takes one argument, the template file to be extended")
|
||||
}
|
||||
|
||||
let parsedNodes = try parser.parse()
|
||||
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]()) { (accumulator, node:NodeType) -> [String:BlockNode] in
|
||||
let node = (node as! BlockNode)
|
||||
var dict = accumulator
|
||||
dict[node.name] = node
|
||||
return dict
|
||||
}
|
||||
|
||||
return ExtendsNode(templateName: Variable(bits[1]), blocks: nodes)
|
||||
}
|
||||
|
||||
init(templateName: Variable, blocks: [String: BlockNode]) {
|
||||
self.templateName = templateName
|
||||
self.blocks = blocks
|
||||
}
|
||||
|
||||
func render(context: Context) throws -> String {
|
||||
guard let loader = context["loader"] as? TemplateLoader else {
|
||||
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)")
|
||||
}
|
||||
|
||||
let blockContext = BlockContext(blocks: blocks)
|
||||
context.push([BlockContext.contextKey: blockContext])
|
||||
let result = try template.render(context)
|
||||
context.pop()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
throw TemplateSyntaxError("'block' tag takes one argument, the template file to be included")
|
||||
}
|
||||
|
||||
let blockName = bits[1]
|
||||
let nodes = try parser.parse(until(["endblock"]))
|
||||
parser.nextToken()
|
||||
return BlockNode(name:blockName, nodes:nodes)
|
||||
}
|
||||
|
||||
init(name: String, nodes: [NodeType]) {
|
||||
self.name = name
|
||||
self.nodes = nodes
|
||||
}
|
||||
|
||||
func render(context: Context) throws -> String {
|
||||
if let blockContext = context[BlockContext.contextKey] as? BlockContext, node = blockContext.pop(name) {
|
||||
return try node.render(context)
|
||||
}
|
||||
|
||||
return try renderNodes(nodes, context)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
254
Sources/Node.swift
Normal file
254
Sources/Node.swift
Normal file
@@ -0,0 +1,254 @@
|
||||
import Foundation
|
||||
|
||||
public struct TemplateSyntaxError : ErrorType, Equatable, CustomStringConvertible {
|
||||
public let description:String
|
||||
|
||||
public init(_ description:String) {
|
||||
self.description = description
|
||||
}
|
||||
}
|
||||
|
||||
public func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool {
|
||||
return lhs.description == rhs.description
|
||||
}
|
||||
|
||||
public protocol NodeType {
|
||||
/// Render the node in the given context
|
||||
func render(context:Context) throws -> String
|
||||
}
|
||||
|
||||
/// Render the collection of nodes in the given context
|
||||
public func renderNodes(nodes:[NodeType], _ context:Context) throws -> String {
|
||||
return try nodes.map { try $0.render(context) }.joinWithSeparator("")
|
||||
}
|
||||
|
||||
public class SimpleNode : NodeType {
|
||||
let handler:Context throws -> String
|
||||
|
||||
public init(handler:Context throws -> String) {
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
return try handler(context)
|
||||
}
|
||||
}
|
||||
|
||||
public class TextNode : NodeType {
|
||||
public let text:String
|
||||
|
||||
public init(text:String) {
|
||||
self.text = text
|
||||
}
|
||||
|
||||
public func render(context:Context) throws -> String {
|
||||
return self.text
|
||||
}
|
||||
}
|
||||
|
||||
public protocol Resolvable {
|
||||
func resolve(context: Context) throws -> Any?
|
||||
}
|
||||
|
||||
public class VariableNode : NodeType {
|
||||
public let variable: Resolvable
|
||||
|
||||
public init(variable: Resolvable) {
|
||||
self.variable = variable
|
||||
}
|
||||
|
||||
public init(variable: String) {
|
||||
self.variable = Variable(variable)
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
let result = try variable.resolve(context)
|
||||
|
||||
if let result = result as? String {
|
||||
return result
|
||||
} else if let result = result as? CustomStringConvertible {
|
||||
return result.description
|
||||
} else if let result = result as? NSObject {
|
||||
return result.description
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
public class NowNode : NodeType {
|
||||
public let format:Variable
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
var format:Variable?
|
||||
|
||||
let components = token.components()
|
||||
guard components.count <= 2 else {
|
||||
throw TemplateSyntaxError("'now' tags may only have one argument: the format string `\(token.contents)`.")
|
||||
}
|
||||
if components.count == 2 {
|
||||
format = Variable(components[1])
|
||||
}
|
||||
|
||||
return NowNode(format:format)
|
||||
}
|
||||
|
||||
public init(format:Variable?) {
|
||||
self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"")
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
let date = NSDate()
|
||||
let format = try self.format.resolve(context)
|
||||
var formatter:NSDateFormatter?
|
||||
|
||||
if let format = format as? NSDateFormatter {
|
||||
formatter = format
|
||||
} else if let format = format as? String {
|
||||
formatter = NSDateFormatter()
|
||||
formatter!.dateFormat = format
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
|
||||
return formatter!.stringFromDate(date)
|
||||
}
|
||||
}
|
||||
|
||||
public class ForNode : NodeType {
|
||||
let variable:Variable
|
||||
let loopVariable:String
|
||||
let nodes:[NodeType]
|
||||
let emptyNodes: [NodeType]
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
|
||||
guard components.count == 4 && components[2] == "in" else {
|
||||
throw TemplateSyntaxError("'for' statements should use the following 'for x in y' `\(token.contents)`.")
|
||||
}
|
||||
|
||||
let loopVariable = components[1]
|
||||
let variable = components[3]
|
||||
|
||||
var emptyNodes = [NodeType]()
|
||||
|
||||
let forNodes = try parser.parse(until(["endfor", "empty"]))
|
||||
|
||||
guard let token = parser.nextToken() else {
|
||||
throw TemplateSyntaxError("`endfor` was not found.")
|
||||
}
|
||||
|
||||
if token.contents == "empty" {
|
||||
emptyNodes = try parser.parse(until(["endfor"]))
|
||||
parser.nextToken()
|
||||
}
|
||||
|
||||
return ForNode(variable: variable, loopVariable: loopVariable, nodes: forNodes, emptyNodes:emptyNodes)
|
||||
}
|
||||
|
||||
public init(variable:String, loopVariable:String, nodes:[NodeType], emptyNodes:[NodeType]) {
|
||||
self.variable = Variable(variable)
|
||||
self.loopVariable = loopVariable
|
||||
self.nodes = nodes
|
||||
self.emptyNodes = emptyNodes
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
let values = try variable.resolve(context)
|
||||
|
||||
if let values = values as? [Any] where values.count > 0 {
|
||||
return try values.map { item in
|
||||
try context.push([loopVariable: item]) {
|
||||
try renderNodes(nodes, context)
|
||||
}
|
||||
}.joinWithSeparator("")
|
||||
}
|
||||
|
||||
return try context.push {
|
||||
try renderNodes(emptyNodes, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class IfNode : NodeType {
|
||||
public let variable:Variable
|
||||
public let trueNodes:[NodeType]
|
||||
public let falseNodes:[NodeType]
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
guard components.count == 2 else {
|
||||
throw TemplateSyntaxError("'if' statements should use the following 'if condition' `\(token.contents)`.")
|
||||
}
|
||||
let variable = components[1]
|
||||
var trueNodes = [NodeType]()
|
||||
var falseNodes = [NodeType]()
|
||||
|
||||
trueNodes = try parser.parse(until(["endif", "else"]))
|
||||
|
||||
guard let token = parser.nextToken() else {
|
||||
throw TemplateSyntaxError("`endif` was not found.")
|
||||
}
|
||||
|
||||
if token.contents == "else" {
|
||||
falseNodes = try parser.parse(until(["endif"]))
|
||||
parser.nextToken()
|
||||
}
|
||||
|
||||
return IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)
|
||||
}
|
||||
|
||||
public class func parse_ifnot(parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
guard components.count == 2 else {
|
||||
throw TemplateSyntaxError("'ifnot' statements should use the following 'if condition' `\(token.contents)`.")
|
||||
}
|
||||
let variable = components[1]
|
||||
var trueNodes = [NodeType]()
|
||||
var falseNodes = [NodeType]()
|
||||
|
||||
falseNodes = try parser.parse(until(["endif", "else"]))
|
||||
|
||||
guard let token = parser.nextToken() else {
|
||||
throw TemplateSyntaxError("`endif` was not found.")
|
||||
}
|
||||
|
||||
if token.contents == "else" {
|
||||
trueNodes = try parser.parse(until(["endif"]))
|
||||
parser.nextToken()
|
||||
}
|
||||
|
||||
return IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)
|
||||
}
|
||||
|
||||
public init(variable:String, trueNodes:[NodeType], falseNodes:[NodeType]) {
|
||||
self.variable = Variable(variable)
|
||||
self.trueNodes = trueNodes
|
||||
self.falseNodes = falseNodes
|
||||
}
|
||||
|
||||
public func render(context: Context) throws -> String {
|
||||
let result = try variable.resolve(context)
|
||||
var truthy = false
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
context.push()
|
||||
let output:String
|
||||
if truthy {
|
||||
output = try renderNodes(trueNodes, context)
|
||||
} else {
|
||||
output = try renderNodes(falseNodes, context)
|
||||
}
|
||||
context.pop()
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
||||
89
Sources/Parser.swift
Normal file
89
Sources/Parser.swift
Normal file
@@ -0,0 +1,89 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public typealias Filter = Any? throws -> Any?
|
||||
|
||||
/// A class for parsing an array of tokens and converts them into a collection of Node's
|
||||
public class TokenParser {
|
||||
public typealias TagParser = (TokenParser, Token) throws -> NodeType
|
||||
|
||||
private var tokens: [Token]
|
||||
private let namespace: Namespace
|
||||
|
||||
public init(tokens: [Token], namespace: Namespace) {
|
||||
self.tokens = tokens
|
||||
self.namespace = namespace
|
||||
}
|
||||
|
||||
/// Parse the given tokens into nodes
|
||||
public func parse() throws -> [NodeType] {
|
||||
return try parse(nil)
|
||||
}
|
||||
|
||||
public func parse(parse_until:((parser:TokenParser, token:Token) -> (Bool))?) throws -> [NodeType] {
|
||||
var nodes = [NodeType]()
|
||||
|
||||
while tokens.count > 0 {
|
||||
let token = nextToken()!
|
||||
|
||||
switch token {
|
||||
case .Text(let text):
|
||||
nodes.append(TextNode(text: text))
|
||||
case .Variable:
|
||||
nodes.append(VariableNode(variable: try compileFilter(token.contents)))
|
||||
case .Block:
|
||||
let tag = token.components().first
|
||||
|
||||
if let parse_until = parse_until where parse_until(parser: self, token: token) {
|
||||
prependToken(token)
|
||||
return nodes
|
||||
}
|
||||
|
||||
if let tag = tag {
|
||||
if let parser = namespace.tags[tag] {
|
||||
nodes.append(try parser(self, token))
|
||||
} else {
|
||||
throw TemplateSyntaxError("Unknown template tag '\(tag)'")
|
||||
}
|
||||
}
|
||||
case .Comment:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
public func nextToken() -> Token? {
|
||||
if tokens.count > 0 {
|
||||
return tokens.removeAtIndex(0)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public func prependToken(token:Token) {
|
||||
tokens.insert(token, atIndex: 0)
|
||||
}
|
||||
|
||||
public func findFilter(name: String) throws -> Filter {
|
||||
if let filter = namespace.filters[name] {
|
||||
return filter
|
||||
}
|
||||
|
||||
throw TemplateSyntaxError("Invalid filter '\(name)'")
|
||||
}
|
||||
|
||||
func compileFilter(token: String) throws -> Resolvable {
|
||||
return try FilterExpression(token: token, parser: self)
|
||||
}
|
||||
}
|
||||
40
Sources/Template.swift
Normal file
40
Sources/Template.swift
Normal file
@@ -0,0 +1,40 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
/// A class representing a template
|
||||
public class Template {
|
||||
let tokens: [Token]
|
||||
|
||||
/// Create a template with the given name inside the given bundle
|
||||
public convenience init(named:String, inBundle bundle:NSBundle? = nil) throws {
|
||||
let useBundle = bundle ?? NSBundle.mainBundle()
|
||||
guard let url = useBundle.URLForResource(named, withExtension: nil) else {
|
||||
throw NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError, userInfo: nil)
|
||||
}
|
||||
|
||||
try self.init(URL:url)
|
||||
}
|
||||
|
||||
/// Create a template with a file found at the given URL
|
||||
public convenience init(URL:NSURL) throws {
|
||||
try self.init(path: Path(URL.path!))
|
||||
}
|
||||
|
||||
/// Create a template with a file found at the given path
|
||||
public convenience init(path:Path) throws {
|
||||
self.init(templateString: try path.read())
|
||||
}
|
||||
|
||||
/// Create a template with a template string
|
||||
public init(templateString:String) {
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
tokens = lexer.tokenize()
|
||||
}
|
||||
|
||||
/// Render the given template
|
||||
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())
|
||||
}
|
||||
}
|
||||
38
Sources/TemplateLoader.swift
Normal file
38
Sources/TemplateLoader.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
|
||||
// A class for loading a template from disk
|
||||
public class TemplateLoader {
|
||||
public let paths: [Path]
|
||||
|
||||
public init(paths: [Path]) {
|
||||
self.paths = paths
|
||||
}
|
||||
|
||||
public init(bundle: [NSBundle]) {
|
||||
self.paths = bundle.map {
|
||||
return Path($0.bundlePath)
|
||||
}
|
||||
}
|
||||
|
||||
public func loadTemplate(templateName: String) -> Template? {
|
||||
return loadTemplate([templateName])
|
||||
}
|
||||
|
||||
public func loadTemplate(templateNames: [String]) -> Template? {
|
||||
for path in paths {
|
||||
for templateName in templateNames {
|
||||
let templatePath = path + Path(templateName)
|
||||
|
||||
if templatePath.exists {
|
||||
if let template = try? Template(path: templatePath) {
|
||||
return template
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
93
Sources/Tokenizer.swift
Normal file
93
Sources/Tokenizer.swift
Normal file
@@ -0,0 +1,93 @@
|
||||
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)
|
||||
|
||||
/// A token representing a variable.
|
||||
case Variable(value: String)
|
||||
|
||||
/// A token representing a comment.
|
||||
case Comment(value: String)
|
||||
|
||||
/// A token representing a template block.
|
||||
case Block(value: String)
|
||||
|
||||
/// Returns the underlying value as an array seperated by spaces
|
||||
public func components() -> [String] {
|
||||
switch self {
|
||||
case .Block(let value):
|
||||
return smartSplit(value)
|
||||
case .Variable(let value):
|
||||
return smartSplit(value)
|
||||
case .Text(let value):
|
||||
return smartSplit(value)
|
||||
case .Comment(let value):
|
||||
return smartSplit(value)
|
||||
}
|
||||
}
|
||||
|
||||
public var contents: String {
|
||||
switch self {
|
||||
case .Block(let value):
|
||||
return value
|
||||
case .Variable(let value):
|
||||
return value
|
||||
case .Text(let value):
|
||||
return value
|
||||
case .Comment(let value):
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func == (lhs: Token, rhs: Token) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.Text(let lhsValue), .Text(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.Variable(let lhsValue), .Variable(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.Block(let lhsValue), .Block(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.Comment(let lhsValue), .Comment(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
135
Sources/Variable.swift
Normal file
135
Sources/Variable.swift
Normal file
@@ -0,0 +1,135 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
class FilterExpression : Resolvable {
|
||||
let filters: [Filter]
|
||||
let variable: Variable
|
||||
|
||||
init(token: String, parser: TokenParser) throws {
|
||||
let bits = token.characters.split("|").map({ String($0).trim(" ") })
|
||||
if bits.isEmpty {
|
||||
filters = []
|
||||
variable = Variable("")
|
||||
throw TemplateSyntaxError("Variable tags must include at least 1 argument")
|
||||
}
|
||||
|
||||
variable = Variable(bits[0])
|
||||
let filterBits = bits[1 ..< bits.endIndex]
|
||||
|
||||
do {
|
||||
filters = try filterBits.map { try parser.findFilter($0) }
|
||||
} catch {
|
||||
filters = []
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
func resolve(context: Context) throws -> Any? {
|
||||
let result = try variable.resolve(context)
|
||||
|
||||
return try filters.reduce(result) { x, y in
|
||||
return try y(x)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
/// Create a variable with a string representing the variable
|
||||
public init(_ variable: String) {
|
||||
self.variable = variable
|
||||
}
|
||||
|
||||
private func lookup() -> [String] {
|
||||
return variable.characters.split(".").map(String.init)
|
||||
}
|
||||
|
||||
/// Resolve the variable in the given context
|
||||
public func resolve(context: Context) throws -> Any? {
|
||||
var current: Any? = context
|
||||
|
||||
if (variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\"")) {
|
||||
// String literal
|
||||
return variable[variable.startIndex.successor() ..< variable.endIndex.predecessor()]
|
||||
}
|
||||
|
||||
for bit in lookup() {
|
||||
if let context = current as? Context {
|
||||
current = context[bit]
|
||||
} else if let dictionary = resolveDictionary(current) {
|
||||
current = dictionary[bit]
|
||||
} else if let array = resolveArray(current) {
|
||||
if let index = Int(bit) {
|
||||
current = array[index]
|
||||
} else if bit == "first" {
|
||||
current = array.first
|
||||
} else if bit == "last" {
|
||||
current = array.last
|
||||
} else if bit == "count" {
|
||||
current = array.count
|
||||
}
|
||||
} else if let object = current as? NSObject { // NSKeyValueCoding
|
||||
current = object.valueForKey(bit)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return normalize(current)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user