Merge pull request #167 from stencilproject/errors-logs-improvements
Errors logs improvements
This commit is contained in:
@@ -12,8 +12,11 @@
|
|||||||
[David Jennes](https://github.com/djbe)
|
[David Jennes](https://github.com/djbe)
|
||||||
[#215](https://github.com/stencilproject/Stencil/pull/215)
|
[#215](https://github.com/stencilproject/Stencil/pull/215)
|
||||||
- Adds support for using spaces in filter expression.
|
- Adds support for using spaces in filter expression.
|
||||||
[Ilya Puchka](https://github.com/yonaskolb)
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
[#178](https://github.com/stencilproject/Stencil/pull/178)
|
[#178](https://github.com/stencilproject/Stencil/pull/178)
|
||||||
|
- Improvements in error reporting.
|
||||||
|
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||||
|
[#167](https://github.com/stencilproject/Stencil/pull/167)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ public class Context {
|
|||||||
var dictionaries: [[String: Any?]]
|
var dictionaries: [[String: Any?]]
|
||||||
|
|
||||||
public let environment: Environment
|
public let environment: Environment
|
||||||
|
|
||||||
init(dictionary: [String: Any]? = nil, environment: Environment? = nil) {
|
init(dictionary: [String: Any]? = nil, environment: Environment? = nil) {
|
||||||
if let dictionary = dictionary {
|
if let dictionary = dictionary {
|
||||||
dictionaries = [dictionary]
|
dictionaries = [dictionary]
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ public struct Environment {
|
|||||||
|
|
||||||
public var loader: Loader?
|
public var loader: Loader?
|
||||||
|
|
||||||
public init(loader: Loader? = nil, extensions: [Extension]? = nil, templateClass: Template.Type = Template.self) {
|
public init(loader: Loader? = nil,
|
||||||
|
extensions: [Extension]? = nil,
|
||||||
|
templateClass: Template.Type = Template.self) {
|
||||||
|
|
||||||
self.templateClass = templateClass
|
self.templateClass = templateClass
|
||||||
self.loader = loader
|
self.loader = loader
|
||||||
self.extensions = (extensions ?? []) + [DefaultExtension()]
|
self.extensions = (extensions ?? []) + [DefaultExtension()]
|
||||||
@@ -28,11 +31,18 @@ public struct Environment {
|
|||||||
|
|
||||||
public func renderTemplate(name: String, context: [String: Any]? = nil) throws -> String {
|
public func renderTemplate(name: String, context: [String: Any]? = nil) throws -> String {
|
||||||
let template = try loadTemplate(name: name)
|
let template = try loadTemplate(name: name)
|
||||||
return try template.render(context)
|
return try render(template: template, context: context)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func renderTemplate(string: String, context: [String: Any]? = nil) throws -> String {
|
public func renderTemplate(string: String, context: [String: Any]? = nil) throws -> String {
|
||||||
let template = templateClass.init(templateString: string, environment: self)
|
let template = templateClass.init(templateString: string, environment: self)
|
||||||
|
return try render(template: template, context: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
func render(template: Template, context: [String: Any]?) throws -> String {
|
||||||
|
// update template environment as it can be created from string literal with default environment
|
||||||
|
template.environment = self
|
||||||
return try template.render(context)
|
return try template.render(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,3 +17,67 @@ public class TemplateDoesNotExist: Error, CustomStringConvertible {
|
|||||||
return "Template named `\(templates)` does not exist. No loaders found"
|
return "Template named `\(templates)` does not exist. No loaders found"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
|
||||||
|
public let reason: String
|
||||||
|
public var description: String { return reason }
|
||||||
|
public internal(set) var token: Token?
|
||||||
|
public internal(set) var stackTrace: [Token]
|
||||||
|
public var templateName: String? { return token?.sourceMap.filename }
|
||||||
|
var allTokens: [Token] {
|
||||||
|
return stackTrace + (token.map({ [$0] }) ?? [])
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) {
|
||||||
|
self.reason = reason
|
||||||
|
self.stackTrace = stackTrace
|
||||||
|
self.token = token
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(_ description: String) {
|
||||||
|
self.init(reason: description)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool {
|
||||||
|
return lhs.description == rhs.description && lhs.token == rhs.token && lhs.stackTrace == rhs.stackTrace
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Error {
|
||||||
|
func withToken(_ token: Token?) -> Error {
|
||||||
|
if var error = self as? TemplateSyntaxError {
|
||||||
|
error.token = error.token ?? token
|
||||||
|
return error
|
||||||
|
} else {
|
||||||
|
return TemplateSyntaxError(reason: "\(self)", token: token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol ErrorReporter: class {
|
||||||
|
func renderError(_ error: Error) -> String
|
||||||
|
}
|
||||||
|
|
||||||
|
open class SimpleErrorReporter: ErrorReporter {
|
||||||
|
|
||||||
|
open func renderError(_ error: Error) -> String {
|
||||||
|
guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription }
|
||||||
|
|
||||||
|
func describe(token: Token) -> String {
|
||||||
|
let templateName = token.sourceMap.filename ?? ""
|
||||||
|
let line = token.sourceMap.line
|
||||||
|
let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(token.contents.characters.count - 1, 0))))"
|
||||||
|
|
||||||
|
return "\(templateName)\(line.number):\(line.offset): error: \(templateError.reason)\n"
|
||||||
|
+ "\(line.content)\n"
|
||||||
|
+ "\(highlight)\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
var descriptions = templateError.stackTrace.reduce([]) { $0 + [describe(token: $1)] }
|
||||||
|
let description = templateError.token.map(describe(token:)) ?? templateError.reason
|
||||||
|
descriptions.append(description)
|
||||||
|
return descriptions.joined(separator: "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,21 +88,21 @@ final class NotExpression: Expression, PrefixOperator, CustomStringConvertible {
|
|||||||
final class InExpression: Expression, InfixOperator, CustomStringConvertible {
|
final class InExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||||
let lhs: Expression
|
let lhs: Expression
|
||||||
let rhs: Expression
|
let rhs: Expression
|
||||||
|
|
||||||
init(lhs: Expression, rhs: Expression) {
|
init(lhs: Expression, rhs: Expression) {
|
||||||
self.lhs = lhs
|
self.lhs = lhs
|
||||||
self.rhs = rhs
|
self.rhs = rhs
|
||||||
}
|
}
|
||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
return "(\(lhs) in \(rhs))"
|
return "(\(lhs) in \(rhs))"
|
||||||
}
|
}
|
||||||
|
|
||||||
func evaluate(context: Context) throws -> Bool {
|
func evaluate(context: Context) throws -> Bool {
|
||||||
if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression {
|
if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression {
|
||||||
let lhsValue = try lhs.variable.resolve(context)
|
let lhsValue = try lhs.variable.resolve(context)
|
||||||
let rhsValue = try rhs.variable.resolve(context)
|
let rhsValue = try rhs.variable.resolve(context)
|
||||||
|
|
||||||
if let lhs = lhsValue as? AnyHashable, let rhs = rhsValue as? [AnyHashable] {
|
if let lhs = lhsValue as? AnyHashable, let rhs = rhsValue as? [AnyHashable] {
|
||||||
return rhs.contains(lhs)
|
return rhs.contains(lhs)
|
||||||
} else if let lhs = lhsValue as? Int, let rhs = rhsValue as? CountableClosedRange<Int> {
|
} else if let lhs = lhsValue as? Int, let rhs = rhsValue as? CountableClosedRange<Int> {
|
||||||
@@ -115,10 +115,10 @@ final class InExpression: Expression, InfixOperator, CustomStringConvertible {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final class OrExpression: Expression, InfixOperator, CustomStringConvertible {
|
final class OrExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ open class Extension {
|
|||||||
/// Registers a simple template tag with a name and a handler
|
/// Registers a simple template tag with a name and a handler
|
||||||
public func registerSimpleTag(_ name: String, handler: @escaping (Context) throws -> String) {
|
public func registerSimpleTag(_ name: String, handler: @escaping (Context) throws -> String) {
|
||||||
registerTag(name, parser: { parser, token in
|
registerTag(name, parser: { parser, token in
|
||||||
return SimpleNode(handler: handler)
|
return SimpleNode(token: token, handler: handler)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,9 +42,9 @@ class DefaultExtension: Extension {
|
|||||||
registerTag("for", parser: ForNode.parse)
|
registerTag("for", parser: ForNode.parse)
|
||||||
registerTag("if", parser: IfNode.parse)
|
registerTag("if", parser: IfNode.parse)
|
||||||
registerTag("ifnot", parser: IfNode.parse_ifnot)
|
registerTag("ifnot", parser: IfNode.parse_ifnot)
|
||||||
#if !os(Linux)
|
#if !os(Linux)
|
||||||
registerTag("now", parser: NowNode.parse)
|
registerTag("now", parser: NowNode.parse)
|
||||||
#endif
|
#endif
|
||||||
registerTag("include", parser: IncludeNode.parse)
|
registerTag("include", parser: IncludeNode.parse)
|
||||||
registerTag("extends", parser: ExtendsNode.parse)
|
registerTag("extends", parser: ExtendsNode.parse)
|
||||||
registerTag("block", parser: BlockNode.parse)
|
registerTag("block", parser: BlockNode.parse)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class FilterNode : NodeType {
|
class FilterNode : NodeType {
|
||||||
let resolvable: Resolvable
|
let resolvable: Resolvable
|
||||||
let nodes: [NodeType]
|
let nodes: [NodeType]
|
||||||
|
let token: Token?
|
||||||
|
|
||||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||||
let bits = token.components()
|
let bits = token.components()
|
||||||
@@ -15,20 +16,21 @@ class FilterNode : NodeType {
|
|||||||
throw TemplateSyntaxError("`endfilter` was not found.")
|
throw TemplateSyntaxError("`endfilter` was not found.")
|
||||||
}
|
}
|
||||||
|
|
||||||
let resolvable = try parser.compileFilter("filter_value|\(bits[1])")
|
let resolvable = try parser.compileFilter("filter_value|\(bits[1])", containedIn: token)
|
||||||
return FilterNode(nodes: blocks, resolvable: resolvable)
|
return FilterNode(nodes: blocks, resolvable: resolvable, token: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(nodes: [NodeType], resolvable: Resolvable) {
|
init(nodes: [NodeType], resolvable: Resolvable, token: Token) {
|
||||||
self.nodes = nodes
|
self.nodes = nodes
|
||||||
self.resolvable = resolvable
|
self.resolvable = resolvable
|
||||||
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
let value = try renderNodes(nodes, context)
|
let value = try renderNodes(nodes, context)
|
||||||
|
|
||||||
return try context.push(dictionary: ["filter_value": value]) {
|
return try context.push(dictionary: ["filter_value": value]) {
|
||||||
return try VariableNode(variable: resolvable).render(context)
|
return try VariableNode(variable: resolvable, token: token).render(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class ForNode : NodeType {
|
|||||||
let nodes:[NodeType]
|
let nodes:[NodeType]
|
||||||
let emptyNodes: [NodeType]
|
let emptyNodes: [NodeType]
|
||||||
let `where`: Expression?
|
let `where`: Expression?
|
||||||
|
let token: Token?
|
||||||
|
|
||||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||||
let components = token.components()
|
let components = token.components()
|
||||||
@@ -13,12 +14,13 @@ class ForNode : NodeType {
|
|||||||
func hasToken(_ token: String, at index: Int) -> Bool {
|
func hasToken(_ token: String, at index: Int) -> Bool {
|
||||||
return components.count > (index + 1) && components[index] == token
|
return components.count > (index + 1) && components[index] == token
|
||||||
}
|
}
|
||||||
|
|
||||||
func endsOrHasToken(_ token: String, at index: Int) -> Bool {
|
func endsOrHasToken(_ token: String, at index: Int) -> Bool {
|
||||||
return components.count == index || hasToken(token, at: index)
|
return components.count == index || hasToken(token, at: index)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard hasToken("in", at: 2) && endsOrHasToken("where", at: 4) else {
|
guard hasToken("in", at: 2) && endsOrHasToken("where", at: 4) else {
|
||||||
throw TemplateSyntaxError("'for' statements should use the syntax: `for <x> in <y> [where <condition>]")
|
throw TemplateSyntaxError("'for' statements should use the syntax: `for <x> in <y> [where <condition>]`.")
|
||||||
}
|
}
|
||||||
|
|
||||||
let loopVariables = components[1].characters
|
let loopVariables = components[1].characters
|
||||||
@@ -26,7 +28,11 @@ class ForNode : NodeType {
|
|||||||
.map(String.init)
|
.map(String.init)
|
||||||
.map { $0.trim(character: " ") }
|
.map { $0.trim(character: " ") }
|
||||||
|
|
||||||
var emptyNodes = [NodeType]()
|
let resolvable = try parser.compileResolvable(components[3], containedIn: token)
|
||||||
|
|
||||||
|
let `where` = hasToken("where", at: 4)
|
||||||
|
? try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser, token: token)
|
||||||
|
: nil
|
||||||
|
|
||||||
let forNodes = try parser.parse(until(["endfor", "empty"]))
|
let forNodes = try parser.parse(until(["endfor", "empty"]))
|
||||||
|
|
||||||
@@ -34,26 +40,22 @@ class ForNode : NodeType {
|
|||||||
throw TemplateSyntaxError("`endfor` was not found.")
|
throw TemplateSyntaxError("`endfor` was not found.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var emptyNodes = [NodeType]()
|
||||||
if token.contents == "empty" {
|
if token.contents == "empty" {
|
||||||
emptyNodes = try parser.parse(until(["endfor"]))
|
emptyNodes = try parser.parse(until(["endfor"]))
|
||||||
_ = parser.nextToken()
|
_ = parser.nextToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
let resolvable = try parser.compileResolvable(components[3])
|
return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes: emptyNodes, where: `where`, token: token)
|
||||||
|
|
||||||
let `where` = hasToken("where", at: 4)
|
|
||||||
? try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser)
|
|
||||||
: nil
|
|
||||||
|
|
||||||
return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init(resolvable: Resolvable, loopVariables: [String], nodes:[NodeType], emptyNodes:[NodeType], where: Expression? = nil) {
|
init(resolvable: Resolvable, loopVariables: [String], nodes: [NodeType], emptyNodes: [NodeType], where: Expression? = nil, token: Token? = nil) {
|
||||||
self.resolvable = resolvable
|
self.resolvable = resolvable
|
||||||
self.loopVariables = loopVariables
|
self.loopVariables = loopVariables
|
||||||
self.nodes = nodes
|
self.nodes = nodes
|
||||||
self.emptyNodes = emptyNodes
|
self.emptyNodes = emptyNodes
|
||||||
self.where = `where`
|
self.where = `where`
|
||||||
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result {
|
func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result {
|
||||||
@@ -143,7 +145,7 @@ class ForNode : NodeType {
|
|||||||
try renderNodes(nodes, context)
|
try renderNodes(nodes, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.joined(separator: "")
|
}.joined(separator: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
return try context.push {
|
return try context.push {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ enum IfToken {
|
|||||||
case .variable(_):
|
case .variable(_):
|
||||||
return 0
|
return 0
|
||||||
case .end:
|
case .end:
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ final class IfExpressionParser {
|
|||||||
let tokens: [IfToken]
|
let tokens: [IfToken]
|
||||||
var position: Int = 0
|
var position: Int = 0
|
||||||
|
|
||||||
init(components: [String], tokenParser: TokenParser) throws {
|
init(components: [String], tokenParser: TokenParser, token: Token) throws {
|
||||||
self.tokens = try components.map { component in
|
self.tokens = try components.map { component in
|
||||||
if let op = findOperator(name: component) {
|
if let op = findOperator(name: component) {
|
||||||
switch op {
|
switch op {
|
||||||
@@ -111,7 +111,7 @@ final class IfExpressionParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return .variable(try tokenParser.compileResolvable(component))
|
return .variable(try tokenParser.compileResolvable(component, containedIn: token))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,8 +155,8 @@ final class IfExpressionParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func parseExpression(components: [String], tokenParser: TokenParser) throws -> Expression {
|
func parseExpression(components: [String], tokenParser: TokenParser, token: Token) throws -> Expression {
|
||||||
let parser = try IfExpressionParser(components: components, tokenParser: tokenParser)
|
let parser = try IfExpressionParser(components: components, tokenParser: tokenParser, token: token)
|
||||||
return try parser.parse()
|
return try parser.parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,49 +182,51 @@ final class IfCondition {
|
|||||||
|
|
||||||
class IfNode : NodeType {
|
class IfNode : NodeType {
|
||||||
let conditions: [IfCondition]
|
let conditions: [IfCondition]
|
||||||
|
let token: Token?
|
||||||
|
|
||||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||||
var components = token.components()
|
var components = token.components()
|
||||||
components.removeFirst()
|
components.removeFirst()
|
||||||
|
|
||||||
let expression = try parseExpression(components: components, tokenParser: parser)
|
let expression = try parseExpression(components: components, tokenParser: parser, token: token)
|
||||||
let nodes = try parser.parse(until(["endif", "elif", "else"]))
|
let nodes = try parser.parse(until(["endif", "elif", "else"]))
|
||||||
var conditions: [IfCondition] = [
|
var conditions: [IfCondition] = [
|
||||||
IfCondition(expression: expression, nodes: nodes)
|
IfCondition(expression: expression, nodes: nodes)
|
||||||
]
|
]
|
||||||
|
|
||||||
var token = parser.nextToken()
|
var nextToken = parser.nextToken()
|
||||||
while let current = token, current.contents.hasPrefix("elif") {
|
while let current = nextToken, current.contents.hasPrefix("elif") {
|
||||||
var components = current.components()
|
var components = current.components()
|
||||||
components.removeFirst()
|
components.removeFirst()
|
||||||
let expression = try parseExpression(components: components, tokenParser: parser)
|
let expression = try parseExpression(components: components, tokenParser: parser, token: current)
|
||||||
|
|
||||||
let nodes = try parser.parse(until(["endif", "elif", "else"]))
|
let nodes = try parser.parse(until(["endif", "elif", "else"]))
|
||||||
token = parser.nextToken()
|
nextToken = parser.nextToken()
|
||||||
conditions.append(IfCondition(expression: expression, nodes: nodes))
|
conditions.append(IfCondition(expression: expression, nodes: nodes))
|
||||||
}
|
}
|
||||||
|
|
||||||
if let current = token, current.contents == "else" {
|
if let current = nextToken, current.contents == "else" {
|
||||||
conditions.append(IfCondition(expression: nil, nodes: try parser.parse(until(["endif"]))))
|
conditions.append(IfCondition(expression: nil, nodes: try parser.parse(until(["endif"]))))
|
||||||
token = parser.nextToken()
|
nextToken = parser.nextToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let current = token, current.contents == "endif" else {
|
guard let current = nextToken, current.contents == "endif" else {
|
||||||
throw TemplateSyntaxError("`endif` was not found.")
|
throw TemplateSyntaxError("`endif` was not found.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return IfNode(conditions: conditions)
|
return IfNode(conditions: conditions, token: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType {
|
class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||||
var components = token.components()
|
var components = token.components()
|
||||||
guard components.count == 2 else {
|
guard components.count == 2 else {
|
||||||
throw TemplateSyntaxError("'ifnot' statements should use the following 'ifnot condition' `\(token.contents)`.")
|
throw TemplateSyntaxError("'ifnot' statements should use the following syntax 'ifnot condition'.")
|
||||||
}
|
}
|
||||||
components.removeFirst()
|
components.removeFirst()
|
||||||
var trueNodes = [NodeType]()
|
var trueNodes = [NodeType]()
|
||||||
var falseNodes = [NodeType]()
|
var falseNodes = [NodeType]()
|
||||||
|
|
||||||
|
let expression = try parseExpression(components: components, tokenParser: parser, token: token)
|
||||||
falseNodes = try parser.parse(until(["endif", "else"]))
|
falseNodes = try parser.parse(until(["endif", "else"]))
|
||||||
|
|
||||||
guard let token = parser.nextToken() else {
|
guard let token = parser.nextToken() else {
|
||||||
@@ -236,15 +238,15 @@ class IfNode : NodeType {
|
|||||||
_ = parser.nextToken()
|
_ = parser.nextToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
let expression = try parseExpression(components: components, tokenParser: parser)
|
|
||||||
return IfNode(conditions: [
|
return IfNode(conditions: [
|
||||||
IfCondition(expression: expression, nodes: trueNodes),
|
IfCondition(expression: expression, nodes: trueNodes),
|
||||||
IfCondition(expression: nil, nodes: falseNodes),
|
IfCondition(expression: nil, nodes: falseNodes),
|
||||||
])
|
], token: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(conditions: [IfCondition]) {
|
init(conditions: [IfCondition], token: Token? = nil) {
|
||||||
self.conditions = conditions
|
self.conditions = conditions
|
||||||
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import PathKit
|
|||||||
class IncludeNode : NodeType {
|
class IncludeNode : NodeType {
|
||||||
let templateName: Variable
|
let templateName: Variable
|
||||||
let includeContext: String?
|
let includeContext: String?
|
||||||
|
let token: Token?
|
||||||
|
|
||||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||||
let bits = token.components()
|
let bits = token.components()
|
||||||
@@ -12,12 +13,13 @@ class IncludeNode : NodeType {
|
|||||||
throw TemplateSyntaxError("'include' tag requires one argument, the template file to be included. A second optional argument can be used to specify the context that will be passed to the included file")
|
throw TemplateSyntaxError("'include' tag requires one argument, the template file to be included. A second optional argument can be used to specify the context that will be passed to the included file")
|
||||||
}
|
}
|
||||||
|
|
||||||
return IncludeNode(templateName: Variable(bits[1]), includeContext: bits.count == 3 ? bits[2] : nil)
|
return IncludeNode(templateName: Variable(bits[1]), includeContext: bits.count == 3 ? bits[2] : nil, token: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(templateName: Variable, includeContext: String? = nil) {
|
init(templateName: Variable, includeContext: String? = nil, token: Token) {
|
||||||
self.templateName = templateName
|
self.templateName = templateName
|
||||||
self.includeContext = includeContext
|
self.includeContext = includeContext
|
||||||
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
@@ -27,9 +29,17 @@ class IncludeNode : NodeType {
|
|||||||
|
|
||||||
let template = try context.environment.loadTemplate(name: templateName)
|
let template = try context.environment.loadTemplate(name: templateName)
|
||||||
|
|
||||||
let subContext = includeContext.flatMap { context[$0] as? [String: Any] }
|
do {
|
||||||
return try context.push(dictionary: subContext) {
|
let subContext = includeContext.flatMap { context[$0] as? [String: Any] }
|
||||||
return try template.render(context)
|
return try context.push(dictionary: subContext) {
|
||||||
|
return try template.render(context)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if let error = error as? TemplateSyntaxError {
|
||||||
|
throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens)
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
class BlockContext {
|
class BlockContext {
|
||||||
class var contextKey: String { return "block_context" }
|
class var contextKey: String { return "block_context" }
|
||||||
|
|
||||||
|
// contains mapping of block names to their nodes and templates where they are defined
|
||||||
var blocks: [String: [BlockNode]]
|
var blocks: [String: [BlockNode]]
|
||||||
|
|
||||||
init(blocks: [String: BlockNode]) {
|
init(blocks: [String: BlockNode]) {
|
||||||
self.blocks = [:]
|
self.blocks = [:]
|
||||||
blocks.forEach { (key, value) in
|
blocks.forEach { self.blocks[$0.key] = [$0.value] }
|
||||||
self.blocks[key] = [value]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func push(_ block: BlockNode, forKey blockName: String) {
|
func push(_ block: BlockNode, forKey blockName: String) {
|
||||||
@@ -18,7 +17,7 @@ class BlockContext {
|
|||||||
self.blocks[blockName] = [block]
|
self.blocks[blockName] = [block]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func pop(_ blockName: String) -> BlockNode? {
|
func pop(_ blockName: String) -> BlockNode? {
|
||||||
if var blocks = blocks[blockName] {
|
if var blocks = blocks[blockName] {
|
||||||
let block = blocks.removeFirst()
|
let block = blocks.removeFirst()
|
||||||
@@ -51,6 +50,7 @@ extension Collection {
|
|||||||
class ExtendsNode : NodeType {
|
class ExtendsNode : NodeType {
|
||||||
let templateName: Variable
|
let templateName: Variable
|
||||||
let blocks: [String:BlockNode]
|
let blocks: [String:BlockNode]
|
||||||
|
let token: Token?
|
||||||
|
|
||||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||||
let bits = token.components()
|
let bits = token.components()
|
||||||
@@ -72,12 +72,13 @@ class ExtendsNode : NodeType {
|
|||||||
return dict
|
return dict
|
||||||
}
|
}
|
||||||
|
|
||||||
return ExtendsNode(templateName: Variable(bits[1]), blocks: nodes)
|
return ExtendsNode(templateName: Variable(bits[1]), blocks: nodes, token: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(templateName: Variable, blocks: [String: BlockNode]) {
|
init(templateName: Variable, blocks: [String: BlockNode], token: Token) {
|
||||||
self.templateName = templateName
|
self.templateName = templateName
|
||||||
self.blocks = blocks
|
self.blocks = blocks
|
||||||
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
@@ -85,21 +86,33 @@ class ExtendsNode : NodeType {
|
|||||||
throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string")
|
throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string")
|
||||||
}
|
}
|
||||||
|
|
||||||
let template = try context.environment.loadTemplate(name: templateName)
|
let baseTemplate = try context.environment.loadTemplate(name: templateName)
|
||||||
|
|
||||||
let blockContext: BlockContext
|
let blockContext: BlockContext
|
||||||
if let context = context[BlockContext.contextKey] as? BlockContext {
|
if let currentBlockContext = context[BlockContext.contextKey] as? BlockContext {
|
||||||
blockContext = context
|
blockContext = currentBlockContext
|
||||||
|
for (name, block) in blocks {
|
||||||
for (key, value) in blocks {
|
blockContext.push(block, forKey: name)
|
||||||
blockContext.push(value, forKey: key)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blockContext = BlockContext(blocks: blocks)
|
blockContext = BlockContext(blocks: blocks)
|
||||||
}
|
}
|
||||||
|
|
||||||
return try context.push(dictionary: [BlockContext.contextKey: blockContext]) {
|
do {
|
||||||
return try template.render(context)
|
// pushes base template and renders it's content
|
||||||
|
// block_context contains all blocks from child templates
|
||||||
|
return try context.push(dictionary: [BlockContext.contextKey: blockContext]) {
|
||||||
|
return try baseTemplate.render(context)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// if error template is already set (see catch in BlockNode)
|
||||||
|
// and it happend in the same template as current template
|
||||||
|
// there is no need to wrap it in another error
|
||||||
|
if let error = error as? TemplateSyntaxError, error.templateName != token?.sourceMap.filename {
|
||||||
|
throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens)
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,6 +121,7 @@ class ExtendsNode : NodeType {
|
|||||||
class BlockNode : NodeType {
|
class BlockNode : NodeType {
|
||||||
let name: String
|
let name: String
|
||||||
let nodes: [NodeType]
|
let nodes: [NodeType]
|
||||||
|
let token: Token?
|
||||||
|
|
||||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||||
let bits = token.components()
|
let bits = token.components()
|
||||||
@@ -119,25 +133,57 @@ class BlockNode : NodeType {
|
|||||||
let blockName = bits[1]
|
let blockName = bits[1]
|
||||||
let nodes = try parser.parse(until(["endblock"]))
|
let nodes = try parser.parse(until(["endblock"]))
|
||||||
_ = parser.nextToken()
|
_ = parser.nextToken()
|
||||||
return BlockNode(name:blockName, nodes:nodes)
|
return BlockNode(name:blockName, nodes:nodes, token: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(name: String, nodes: [NodeType]) {
|
init(name: String, nodes: [NodeType], token: Token) {
|
||||||
self.name = name
|
self.name = name
|
||||||
self.nodes = nodes
|
self.nodes = nodes
|
||||||
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
if let blockContext = context[BlockContext.contextKey] as? BlockContext, let node = blockContext.pop(name) {
|
if let blockContext = context[BlockContext.contextKey] as? BlockContext, let child = blockContext.pop(name) {
|
||||||
let newContext: [String: Any] = [
|
let childContext = try self.childContext(child, blockContext: blockContext, context: context)
|
||||||
BlockContext.contextKey: blockContext,
|
// render extension node
|
||||||
"block": ["super": try self.render(context)]
|
do {
|
||||||
]
|
return try context.push(dictionary: childContext) {
|
||||||
return try context.push(dictionary: newContext) {
|
return try child.render(context)
|
||||||
return try node.render(context)
|
}
|
||||||
|
} catch {
|
||||||
|
throw error.withToken(child.token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return try renderNodes(nodes, context)
|
return try renderNodes(nodes, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// child node is a block node from child template that extends this node (has the same name)
|
||||||
|
func childContext(_ child: BlockNode, blockContext: BlockContext, context: Context) throws -> [String: Any?] {
|
||||||
|
var childContext: [String: Any?] = [BlockContext.contextKey: blockContext]
|
||||||
|
|
||||||
|
if let blockSuperNode = child.nodes.first(where: {
|
||||||
|
if case .variable(let variable, _)? = $0.token, variable == "block.super" { return true }
|
||||||
|
else { return false}
|
||||||
|
}) {
|
||||||
|
do {
|
||||||
|
// render base node so that its content can be used as part of child node that extends it
|
||||||
|
childContext["block"] = ["super": try self.render(context)]
|
||||||
|
} catch {
|
||||||
|
if let error = error as? TemplateSyntaxError {
|
||||||
|
throw TemplateSyntaxError(
|
||||||
|
reason: error.reason,
|
||||||
|
token: blockSuperNode.token,
|
||||||
|
stackTrace: error.allTokens)
|
||||||
|
} else {
|
||||||
|
throw TemplateSyntaxError(
|
||||||
|
reason: "\(error)",
|
||||||
|
token: blockSuperNode.token,
|
||||||
|
stackTrace: [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return childContext
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
struct Lexer {
|
struct Lexer {
|
||||||
|
let templateName: String?
|
||||||
let templateString: String
|
let templateString: String
|
||||||
|
|
||||||
init(templateString: String) {
|
init(templateName: String? = nil, templateString: String) {
|
||||||
|
self.templateName = templateName
|
||||||
self.templateString = templateString
|
self.templateString = templateString
|
||||||
}
|
}
|
||||||
|
|
||||||
func createToken(string: String) -> Token {
|
func createToken(string: String, at range: Range<String.Index>) -> Token {
|
||||||
func strip() -> String {
|
func strip() -> String {
|
||||||
guard string.characters.count > 4 else { return "" }
|
guard string.characters.count > 4 else { return "" }
|
||||||
let start = string.index(string.startIndex, offsetBy: 2)
|
let start = string.index(string.startIndex, offsetBy: 2)
|
||||||
@@ -18,15 +22,24 @@ struct Lexer {
|
|||||||
return trimmed
|
return trimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
if string.hasPrefix("{{") {
|
if string.hasPrefix("{{") || string.hasPrefix("{%") || string.hasPrefix("{#") {
|
||||||
return .variable(value: strip())
|
let value = strip()
|
||||||
} else if string.hasPrefix("{%") {
|
let range = templateString.range(of: value, range: range) ?? range
|
||||||
return .block(value: strip())
|
let line = templateString.rangeLine(range)
|
||||||
} else if string.hasPrefix("{#") {
|
let sourceMap = SourceMap(filename: templateName, line: line)
|
||||||
return .comment(value: strip())
|
|
||||||
|
if string.hasPrefix("{{") {
|
||||||
|
return .variable(value: value, at: sourceMap)
|
||||||
|
} else if string.hasPrefix("{%") {
|
||||||
|
return .block(value: value, at: sourceMap)
|
||||||
|
} else if string.hasPrefix("{#") {
|
||||||
|
return .comment(value: value, at: sourceMap)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return .text(value: string)
|
let line = templateString.rangeLine(range)
|
||||||
|
let sourceMap = SourceMap(filename: templateName, line: line)
|
||||||
|
return .text(value: string, at: sourceMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns an array of tokens from a given template string.
|
/// Returns an array of tokens from a given template string.
|
||||||
@@ -39,33 +52,37 @@ struct Lexer {
|
|||||||
"{{": "}}",
|
"{{": "}}",
|
||||||
"{%": "%}",
|
"{%": "%}",
|
||||||
"{#": "#}",
|
"{#": "#}",
|
||||||
]
|
]
|
||||||
|
|
||||||
while !scanner.isEmpty {
|
while !scanner.isEmpty {
|
||||||
if let text = scanner.scan(until: ["{{", "{%", "{#"]) {
|
if let text = scanner.scan(until: ["{{", "{%", "{#"]) {
|
||||||
if !text.1.isEmpty {
|
if !text.1.isEmpty {
|
||||||
tokens.append(createToken(string: text.1))
|
tokens.append(createToken(string: text.1, at: scanner.range))
|
||||||
}
|
}
|
||||||
|
|
||||||
let end = map[text.0]!
|
let end = map[text.0]!
|
||||||
let result = scanner.scan(until: end, returnUntil: true)
|
let result = scanner.scan(until: end, returnUntil: true)
|
||||||
tokens.append(createToken(string: result))
|
tokens.append(createToken(string: result, at: scanner.range))
|
||||||
} else {
|
} else {
|
||||||
tokens.append(createToken(string: scanner.content))
|
tokens.append(createToken(string: scanner.content, at: scanner.range))
|
||||||
scanner.content = ""
|
scanner.content = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tokens
|
return tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Scanner {
|
class Scanner {
|
||||||
|
let originalContent: String
|
||||||
var content: String
|
var content: String
|
||||||
|
var range: Range<String.Index>
|
||||||
|
|
||||||
init(_ content: String) {
|
init(_ content: String) {
|
||||||
|
self.originalContent = content
|
||||||
self.content = content
|
self.content = content
|
||||||
|
range = content.startIndex..<content.startIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
var isEmpty: Bool {
|
var isEmpty: Bool {
|
||||||
@@ -73,27 +90,31 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func scan(until: String, returnUntil: Bool = false) -> String {
|
func scan(until: String, returnUntil: Bool = false) -> String {
|
||||||
|
var index = content.startIndex
|
||||||
|
|
||||||
if until.isEmpty {
|
if until.isEmpty {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var index = content.startIndex
|
range = range.upperBound..<range.upperBound
|
||||||
while index != content.endIndex {
|
while index != content.endIndex {
|
||||||
let substring = content.substring(from: index)
|
let substring = content.substring(from: index)
|
||||||
|
|
||||||
if substring.hasPrefix(until) {
|
if substring.hasPrefix(until) {
|
||||||
let result = content.substring(to: index)
|
let result = content.substring(to: index)
|
||||||
content = substring
|
|
||||||
|
|
||||||
if returnUntil {
|
if returnUntil {
|
||||||
content = content.substring(from: until.endIndex)
|
range = range.lowerBound..<originalContent.index(range.upperBound, offsetBy: until.characters.count)
|
||||||
|
content = substring.substring(from: until.endIndex)
|
||||||
return result + until
|
return result + until
|
||||||
}
|
}
|
||||||
|
|
||||||
|
content = substring
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
index = content.index(after: index)
|
index = content.index(after: index)
|
||||||
|
range = range.lowerBound..<originalContent.index(after: range.upperBound)
|
||||||
}
|
}
|
||||||
|
|
||||||
content = ""
|
content = ""
|
||||||
@@ -106,6 +127,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var index = content.startIndex
|
var index = content.startIndex
|
||||||
|
range = range.upperBound..<range.upperBound
|
||||||
while index != content.endIndex {
|
while index != content.endIndex {
|
||||||
let substring = content.substring(from: index)
|
let substring = content.substring(from: index)
|
||||||
for string in until {
|
for string in until {
|
||||||
@@ -117,6 +139,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
index = content.index(after: index)
|
index = content.index(after: index)
|
||||||
|
range = range.lowerBound..<originalContent.index(after: range.upperBound)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -156,4 +179,23 @@ extension String {
|
|||||||
let last = findLastNot(character: character) ?? endIndex
|
let last = findLastNot(character: character) ?? endIndex
|
||||||
return String(self[first..<last])
|
return String(self[first..<last])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func rangeLine(_ range: Range<String.Index>) -> RangeLine {
|
||||||
|
var lineNumber: UInt = 0
|
||||||
|
var offset: Int = 0
|
||||||
|
var lineContent = ""
|
||||||
|
|
||||||
|
for line in components(separatedBy: CharacterSet.newlines) {
|
||||||
|
lineNumber += 1
|
||||||
|
lineContent = line
|
||||||
|
if let rangeOfLine = self.range(of: line), rangeOfLine.contains(range.lowerBound) {
|
||||||
|
offset = distance(from: rangeOfLine.lowerBound, to: range.lowerBound)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (lineContent, lineNumber, offset)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public typealias RangeLine = (content: String, number: UInt, offset: Int)
|
||||||
|
|||||||
@@ -1,35 +1,31 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
public struct TemplateSyntaxError : Error, 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 {
|
public protocol NodeType {
|
||||||
/// Render the node in the given context
|
/// Render the node in the given context
|
||||||
func render(_ context:Context) throws -> String
|
func render(_ context:Context) throws -> String
|
||||||
|
|
||||||
|
/// Reference to this node's token
|
||||||
|
var token: Token? { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Render the collection of nodes in the given context
|
/// Render the collection of nodes in the given context
|
||||||
public func renderNodes(_ nodes:[NodeType], _ context:Context) throws -> String {
|
public func renderNodes(_ nodes:[NodeType], _ context:Context) throws -> String {
|
||||||
return try nodes.map { try $0.render(context) }.joined(separator: "")
|
return try nodes.map {
|
||||||
|
do {
|
||||||
|
return try $0.render(context)
|
||||||
|
} catch {
|
||||||
|
throw error.withToken($0.token)
|
||||||
|
}
|
||||||
|
}.joined(separator: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SimpleNode : NodeType {
|
public class SimpleNode : NodeType {
|
||||||
public let handler:(Context) throws -> String
|
public let handler:(Context) throws -> String
|
||||||
|
public let token: Token?
|
||||||
|
|
||||||
public init(handler: @escaping (Context) throws -> String) {
|
public init(token: Token, handler: @escaping (Context) throws -> String) {
|
||||||
|
self.token = token
|
||||||
self.handler = handler
|
self.handler = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,9 +37,11 @@ public class SimpleNode : NodeType {
|
|||||||
|
|
||||||
public class TextNode : NodeType {
|
public class TextNode : NodeType {
|
||||||
public let text:String
|
public let text:String
|
||||||
|
public let token: Token?
|
||||||
|
|
||||||
public init(text:String) {
|
public init(text:String) {
|
||||||
self.text = text
|
self.text = text
|
||||||
|
self.token = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public func render(_ context:Context) throws -> String {
|
public func render(_ context:Context) throws -> String {
|
||||||
@@ -59,13 +57,16 @@ public protocol Resolvable {
|
|||||||
|
|
||||||
public class VariableNode : NodeType {
|
public class VariableNode : NodeType {
|
||||||
public let variable: Resolvable
|
public let variable: Resolvable
|
||||||
|
public var token: Token?
|
||||||
|
|
||||||
public init(variable: Resolvable) {
|
public init(variable: Resolvable, token: Token? = nil) {
|
||||||
self.variable = variable
|
self.variable = variable
|
||||||
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(variable: String) {
|
public init(variable: String, token: Token? = nil) {
|
||||||
self.variable = Variable(variable)
|
self.variable = Variable(variable)
|
||||||
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
public func render(_ context: Context) throws -> String {
|
public func render(_ context: Context) throws -> String {
|
||||||
|
|||||||
@@ -4,23 +4,25 @@ import Foundation
|
|||||||
|
|
||||||
class NowNode : NodeType {
|
class NowNode : NodeType {
|
||||||
let format:Variable
|
let format:Variable
|
||||||
|
let token: Token?
|
||||||
|
|
||||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||||
var format:Variable?
|
var format:Variable?
|
||||||
|
|
||||||
let components = token.components()
|
let components = token.components()
|
||||||
guard components.count <= 2 else {
|
guard components.count <= 2 else {
|
||||||
throw TemplateSyntaxError("'now' tags may only have one argument: the format string `\(token.contents)`.")
|
throw TemplateSyntaxError("'now' tags may only have one argument: the format string.")
|
||||||
}
|
}
|
||||||
if components.count == 2 {
|
if components.count == 2 {
|
||||||
format = Variable(components[1])
|
format = Variable(components[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
return NowNode(format:format)
|
return NowNode(format:format, token: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(format:Variable?) {
|
init(format:Variable?, token: Token? = nil) {
|
||||||
self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"")
|
self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"")
|
||||||
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
|
|||||||
@@ -37,10 +37,11 @@ public class TokenParser {
|
|||||||
let token = nextToken()!
|
let token = nextToken()!
|
||||||
|
|
||||||
switch token {
|
switch token {
|
||||||
case .text(let text):
|
case .text(let text, _):
|
||||||
nodes.append(TextNode(text: text))
|
nodes.append(TextNode(text: text))
|
||||||
case .variable:
|
case .variable:
|
||||||
nodes.append(VariableNode(variable: try compileResolvable(token.contents)))
|
let filter = try compileResolvable(token.contents, containedIn: token)
|
||||||
|
nodes.append(VariableNode(variable: filter, token: token))
|
||||||
case .block:
|
case .block:
|
||||||
if let parse_until = parse_until , parse_until(self, token) {
|
if let parse_until = parse_until , parse_until(self, token) {
|
||||||
prependToken(token)
|
prependToken(token)
|
||||||
@@ -48,8 +49,13 @@ public class TokenParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let tag = token.components().first {
|
if let tag = token.components().first {
|
||||||
let parser = try findTag(name: tag)
|
do {
|
||||||
nodes.append(try parser(self, token))
|
let parser = try findTag(name: tag)
|
||||||
|
let node = try parser(self, token)
|
||||||
|
nodes.append(node)
|
||||||
|
} catch {
|
||||||
|
throw error.withToken(token)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case .comment:
|
case .comment:
|
||||||
continue
|
continue
|
||||||
@@ -92,7 +98,7 @@ public class TokenParser {
|
|||||||
if suggestedFilters.isEmpty {
|
if suggestedFilters.isEmpty {
|
||||||
throw TemplateSyntaxError("Unknown filter '\(name)'.")
|
throw TemplateSyntaxError("Unknown filter '\(name)'.")
|
||||||
} else {
|
} else {
|
||||||
throw TemplateSyntaxError("Unknown filter '\(name)'. Found similar filters: \(suggestedFilters.map({ "'\($0)'" }).joined(separator: ", "))")
|
throw TemplateSyntaxError("Unknown filter '\(name)'. Found similar filters: \(suggestedFilters.map({ "'\($0)'" }).joined(separator: ", ")).")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,15 +116,41 @@ public class TokenParser {
|
|||||||
return filtersWithDistance.filter({ $0.distance == minDistance }).map({ $0.filterName })
|
return filtersWithDistance.filter({ $0.distance == minDistance }).map({ $0.filterName })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func compileFilter(_ filterToken: String, containedIn containingToken: Token) throws -> Resolvable {
|
||||||
|
do {
|
||||||
|
return try FilterExpression(token: filterToken, parser: self)
|
||||||
|
} catch {
|
||||||
|
guard var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
// find offset of filter in the containing token so that only filter is highligted, not the whole token
|
||||||
|
if let filterTokenRange = containingToken.contents.range(of: filterToken) {
|
||||||
|
var rangeLine = containingToken.sourceMap.line
|
||||||
|
rangeLine.offset += containingToken.contents.distance(from: containingToken.contents.startIndex, to: filterTokenRange.lowerBound)
|
||||||
|
syntaxError.token = .variable(value: filterToken, at: SourceMap(filename: containingToken.sourceMap.filename, line: rangeLine))
|
||||||
|
} else {
|
||||||
|
syntaxError.token = containingToken
|
||||||
|
}
|
||||||
|
throw syntaxError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, deprecated, message: "Use compileFilter(_:containedIn:)")
|
||||||
public func compileFilter(_ token: String) throws -> Resolvable {
|
public func compileFilter(_ token: String) throws -> Resolvable {
|
||||||
return try FilterExpression(token: token, parser: self)
|
return try FilterExpression(token: token, parser: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(*, deprecated, message: "Use compileResolvable(_:containedIn:)")
|
||||||
public func compileResolvable(_ token: String) throws -> Resolvable {
|
public func compileResolvable(_ token: String) throws -> Resolvable {
|
||||||
return try RangeVariable(token, parser: self)
|
return try RangeVariable(token, parser: self)
|
||||||
?? compileFilter(token)
|
?? compileFilter(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable {
|
||||||
|
return try RangeVariable(token, parser: self, containedIn: containingToken)
|
||||||
|
?? compileFilter(token, containedIn: containingToken)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
|
// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ let NSFileNoSuchFileError = 4
|
|||||||
|
|
||||||
/// A class representing a template
|
/// A class representing a template
|
||||||
open class Template: ExpressibleByStringLiteral {
|
open class Template: ExpressibleByStringLiteral {
|
||||||
let environment: Environment
|
let templateString: String
|
||||||
|
internal(set) var environment: Environment
|
||||||
let tokens: [Token]
|
let tokens: [Token]
|
||||||
|
|
||||||
/// The name of the loaded Template if the Template was loaded from a Loader
|
/// The name of the loaded Template if the Template was loaded from a Loader
|
||||||
@@ -17,8 +18,9 @@ open class Template: ExpressibleByStringLiteral {
|
|||||||
public required init(templateString: String, environment: Environment? = nil, name: String? = nil) {
|
public required init(templateString: String, environment: Environment? = nil, name: String? = nil) {
|
||||||
self.environment = environment ?? Environment()
|
self.environment = environment ?? Environment()
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.templateString = templateString
|
||||||
|
|
||||||
let lexer = Lexer(templateString: templateString)
|
let lexer = Lexer(templateName: name, templateString: templateString)
|
||||||
tokens = lexer.tokenize()
|
tokens = lexer.tokenize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,59 +55,79 @@ extension String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct SourceMap: Equatable {
|
||||||
|
public let filename: String?
|
||||||
|
public let line: RangeLine
|
||||||
|
|
||||||
|
init(filename: String? = nil, line: RangeLine = ("", 0, 0)) {
|
||||||
|
self.filename = filename
|
||||||
|
self.line = line
|
||||||
|
}
|
||||||
|
|
||||||
|
static let unknown = SourceMap()
|
||||||
|
|
||||||
|
public static func ==(lhs: SourceMap, rhs: SourceMap) -> Bool {
|
||||||
|
return lhs.filename == rhs.filename && lhs.line == rhs.line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum Token : Equatable {
|
public enum Token : Equatable {
|
||||||
/// A token representing a piece of text.
|
/// A token representing a piece of text.
|
||||||
case text(value: String)
|
case text(value: String, at: SourceMap)
|
||||||
|
|
||||||
/// A token representing a variable.
|
/// A token representing a variable.
|
||||||
case variable(value: String)
|
case variable(value: String, at: SourceMap)
|
||||||
|
|
||||||
/// A token representing a comment.
|
/// A token representing a comment.
|
||||||
case comment(value: String)
|
case comment(value: String, at: SourceMap)
|
||||||
|
|
||||||
/// A token representing a template block.
|
/// A token representing a template block.
|
||||||
case block(value: String)
|
case block(value: String, at: SourceMap)
|
||||||
|
|
||||||
/// Returns the underlying value as an array seperated by spaces
|
/// Returns the underlying value as an array seperated by spaces
|
||||||
public func components() -> [String] {
|
public func components() -> [String] {
|
||||||
switch self {
|
switch self {
|
||||||
case .block(let value):
|
case .block(let value, _),
|
||||||
return value.smartSplit()
|
.variable(let value, _),
|
||||||
case .variable(let value):
|
.text(let value, _),
|
||||||
return value.smartSplit()
|
.comment(let value, _):
|
||||||
case .text(let value):
|
|
||||||
return value.smartSplit()
|
|
||||||
case .comment(let value):
|
|
||||||
return value.smartSplit()
|
return value.smartSplit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var contents: String {
|
public var contents: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .block(let value):
|
case .block(let value, _),
|
||||||
return value
|
.variable(let value, _),
|
||||||
case .variable(let value):
|
.text(let value, _),
|
||||||
return value
|
.comment(let value, _):
|
||||||
case .text(let value):
|
|
||||||
return value
|
|
||||||
case .comment(let value):
|
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var sourceMap: SourceMap {
|
||||||
|
switch self {
|
||||||
|
case .block(_, let sourceMap),
|
||||||
|
.variable(_, let sourceMap),
|
||||||
|
.text(_, let sourceMap),
|
||||||
|
.comment(_, let sourceMap):
|
||||||
|
return sourceMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public func == (lhs: Token, rhs: Token) -> Bool {
|
public func == (lhs: Token, rhs: Token) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case (.text(let lhsValue), .text(let rhsValue)):
|
case let (.text(lhsValue, lhsAt), .text(rhsValue, rhsAt)):
|
||||||
return lhsValue == rhsValue
|
return lhsValue == rhsValue && lhsAt == rhsAt
|
||||||
case (.variable(let lhsValue), .variable(let rhsValue)):
|
case let (.variable(lhsValue, lhsAt), .variable(rhsValue, rhsAt)):
|
||||||
return lhsValue == rhsValue
|
return lhsValue == rhsValue && lhsAt == rhsAt
|
||||||
case (.block(let lhsValue), .block(let rhsValue)):
|
case let (.block(lhsValue, lhsAt), .block(rhsValue, rhsAt)):
|
||||||
return lhsValue == rhsValue
|
return lhsValue == rhsValue && lhsAt == rhsAt
|
||||||
case (.comment(let lhsValue), .comment(let rhsValue)):
|
case let (.comment(lhsValue, lhsAt), .comment(rhsValue, rhsAt)):
|
||||||
return lhsValue == rhsValue
|
return lhsValue == rhsValue && lhsAt == rhsAt
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ class FilterExpression : Resolvable {
|
|||||||
init(token: String, parser: TokenParser) throws {
|
init(token: String, parser: TokenParser) throws {
|
||||||
let bits = token.characters.split(separator: "|").map({ String($0).trim(character: " ") })
|
let bits = token.characters.split(separator: "|").map({ String($0).trim(character: " ") })
|
||||||
if bits.isEmpty {
|
if bits.isEmpty {
|
||||||
filters = []
|
|
||||||
variable = Variable("")
|
|
||||||
throw TemplateSyntaxError("Variable tags must include at least 1 argument")
|
throw TemplateSyntaxError("Variable tags must include at least 1 argument")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +50,7 @@ public struct Variable : Equatable, Resolvable {
|
|||||||
|
|
||||||
// Split the lookup string and resolve references if possible
|
// Split the lookup string and resolve references if possible
|
||||||
fileprivate func lookup(_ context: Context) throws -> [String] {
|
fileprivate func lookup(_ context: Context) throws -> [String] {
|
||||||
var keyPath = KeyPath(variable, in: context)
|
let keyPath = KeyPath(variable, in: context)
|
||||||
return try keyPath.parse()
|
return try keyPath.parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,11 +101,11 @@ public struct Variable : Equatable, Resolvable {
|
|||||||
current = array.count
|
current = array.count
|
||||||
}
|
}
|
||||||
} else if let object = current as? NSObject { // NSKeyValueCoding
|
} else if let object = current as? NSObject { // NSKeyValueCoding
|
||||||
#if os(Linux)
|
#if os(Linux)
|
||||||
return nil
|
return nil
|
||||||
#else
|
#else
|
||||||
current = object.value(forKey: bit)
|
current = object.value(forKey: bit)
|
||||||
#endif
|
#endif
|
||||||
} else if let value = current {
|
} else if let value = current {
|
||||||
current = Mirror(reflecting: value).getValue(for: bit)
|
current = Mirror(reflecting: value).getValue(for: bit)
|
||||||
if current == nil {
|
if current == nil {
|
||||||
@@ -140,6 +138,7 @@ public struct RangeVariable: Resolvable {
|
|||||||
public let from: Resolvable
|
public let from: Resolvable
|
||||||
public let to: Resolvable
|
public let to: Resolvable
|
||||||
|
|
||||||
|
@available(*, deprecated, message: "Use init?(_:parser:containedIn:)")
|
||||||
public init?(_ token: String, parser: TokenParser) throws {
|
public init?(_ token: String, parser: TokenParser) throws {
|
||||||
let components = token.components(separatedBy: "...")
|
let components = token.components(separatedBy: "...")
|
||||||
guard components.count == 2 else {
|
guard components.count == 2 else {
|
||||||
@@ -150,6 +149,16 @@ public struct RangeVariable: Resolvable {
|
|||||||
self.to = try parser.compileFilter(components[1])
|
self.to = try parser.compileFilter(components[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public init?(_ token: String, parser: TokenParser, containedIn containingToken: Token) throws {
|
||||||
|
let components = token.components(separatedBy: "...")
|
||||||
|
guard components.count == 2 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.from = try parser.compileFilter(components[0], containedIn: containingToken)
|
||||||
|
self.to = try parser.compileFilter(components[1], containedIn: containingToken)
|
||||||
|
}
|
||||||
|
|
||||||
public func resolve(_ context: Context) throws -> Any? {
|
public func resolve(_ context: Context) throws -> Any? {
|
||||||
let fromResolved = try from.resolve(context)
|
let fromResolved = try from.resolve(context)
|
||||||
let toResolved = try to.resolve(context)
|
let toResolved = try to.resolve(context)
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
import PathKit
|
||||||
|
@testable import Stencil
|
||||||
|
|
||||||
|
|
||||||
func testEnvironment() {
|
func testEnvironment() {
|
||||||
describe("Environment") {
|
describe("Environment") {
|
||||||
let environment = Environment(loader: ExampleLoader())
|
var environment: Environment!
|
||||||
|
var template: Template!
|
||||||
|
|
||||||
|
$0.before {
|
||||||
|
environment = Environment(loader: ExampleLoader())
|
||||||
|
template = nil
|
||||||
|
}
|
||||||
|
|
||||||
$0.it("can load a template from a name") {
|
$0.it("can load a template from a name") {
|
||||||
let template = try environment.loadTemplate(name: "example.html")
|
let template = try environment.loadTemplate(name: "example.html")
|
||||||
@@ -32,9 +39,309 @@ func testEnvironment() {
|
|||||||
|
|
||||||
try expect(result) == "here"
|
try expect(result) == "here"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError {
|
||||||
|
guard let range = template.templateString.range(of: token) else {
|
||||||
|
fatalError("Can't find '\(token)' in '\(template)'")
|
||||||
|
}
|
||||||
|
let rangeLine = template.templateString.rangeLine(range)
|
||||||
|
let sourceMap = SourceMap(filename: template.name, line: rangeLine)
|
||||||
|
let token = Token.block(value: token, at: sourceMap)
|
||||||
|
return TemplateSyntaxError(reason: description, token: token, stackTrace: [])
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectError(reason: String, token: String,
|
||||||
|
file: String = #file, line: Int = #line, function: String = #function) throws {
|
||||||
|
let expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
||||||
|
|
||||||
|
let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"]),
|
||||||
|
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
||||||
|
let reporter = SimpleErrorReporter()
|
||||||
|
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.context("given syntax error") {
|
||||||
|
|
||||||
|
$0.it("reports syntax error on invalid for tag syntax") {
|
||||||
|
template = "Hello {% for name in %}{{ name }}, {% endfor %}!"
|
||||||
|
try expectError(reason: "'for' statements should use the syntax: `for <x> in <y> [where <condition>]`.", token: "for name in")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports syntax error on missing endfor") {
|
||||||
|
template = "{% for name in names %}{{ name }}"
|
||||||
|
try expectError(reason: "`endfor` was not found.", token: "for name in names")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports syntax error on unknown tag") {
|
||||||
|
template = "{% for name in names %}{{ name }}{% end %}"
|
||||||
|
try expectError(reason: "Unknown template tag 'end'", token: "end")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.context("given unknown filter") {
|
||||||
|
|
||||||
|
$0.it("reports syntax error in for tag") {
|
||||||
|
template = "{% for name in names|unknown %}{{ name }}{% endfor %}"
|
||||||
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "names|unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports syntax error in for-where tag") {
|
||||||
|
template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}"
|
||||||
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports syntax error in if tag") {
|
||||||
|
template = "{% if name|unknown %}{{ name }}{% endif %}"
|
||||||
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports syntax error in elif tag") {
|
||||||
|
template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}"
|
||||||
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports syntax error in ifnot tag") {
|
||||||
|
template = "{% ifnot name|unknown %}{{ name }}{% endif %}"
|
||||||
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports syntax error in filter tag") {
|
||||||
|
template = "{% filter unknown %}Text{% endfilter %}"
|
||||||
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "filter unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports syntax error in variable tag") {
|
||||||
|
template = "{{ name|unknown }}"
|
||||||
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.context("given rendering error") {
|
||||||
|
|
||||||
|
$0.it("reports rendering error in variable filter") {
|
||||||
|
let filterExtension = Extension()
|
||||||
|
filterExtension.registerFilter("throw") { (value: Any?) in
|
||||||
|
throw TemplateSyntaxError("filter error")
|
||||||
|
}
|
||||||
|
environment.extensions += [filterExtension]
|
||||||
|
|
||||||
|
template = Template(templateString: "{{ name|throw }}", environment: environment)
|
||||||
|
try expectError(reason: "filter error", token: "name|throw")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports rendering error in filter tag") {
|
||||||
|
let filterExtension = Extension()
|
||||||
|
filterExtension.registerFilter("throw") { (value: Any?) in
|
||||||
|
throw TemplateSyntaxError("filter error")
|
||||||
|
}
|
||||||
|
environment.extensions += [filterExtension]
|
||||||
|
|
||||||
|
template = Template(templateString: "{% filter throw %}Test{% endfilter %}", environment: environment)
|
||||||
|
try expectError(reason: "filter error", token: "filter throw")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports rendering error in simple tag") {
|
||||||
|
let tagExtension = Extension()
|
||||||
|
tagExtension.registerSimpleTag("simpletag") { context in
|
||||||
|
throw TemplateSyntaxError("simpletag error")
|
||||||
|
}
|
||||||
|
environment.extensions += [tagExtension]
|
||||||
|
|
||||||
|
template = Template(templateString: "{% simpletag %}", environment: environment)
|
||||||
|
try expectError(reason: "simpletag error", token: "simpletag")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reporsts passing argument to simple filter") {
|
||||||
|
template = "{{ name|uppercase:5 }}"
|
||||||
|
try expectError(reason: "cannot invoke filter with an argument", token: "name|uppercase:5")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports rendering error in custom tag") {
|
||||||
|
let tagExtension = Extension()
|
||||||
|
tagExtension.registerTag("customtag") { parser, token in
|
||||||
|
return ErrorNode(token: token)
|
||||||
|
}
|
||||||
|
environment.extensions += [tagExtension]
|
||||||
|
|
||||||
|
template = Template(templateString: "{% customtag %}", environment: environment)
|
||||||
|
try expectError(reason: "Custom Error", token: "customtag")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports rendering error in for body") {
|
||||||
|
let tagExtension = Extension()
|
||||||
|
tagExtension.registerTag("customtag") { parser, token in
|
||||||
|
return ErrorNode(token: token)
|
||||||
|
}
|
||||||
|
environment.extensions += [tagExtension]
|
||||||
|
|
||||||
|
template = Template(templateString: "{% for name in names %}{% customtag %}{% endfor %}", environment: environment)
|
||||||
|
try expectError(reason: "Custom Error", token: "customtag")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports rendering error in block") {
|
||||||
|
let tagExtension = Extension()
|
||||||
|
tagExtension.registerTag("customtag") { parser, token in
|
||||||
|
return ErrorNode(token: token)
|
||||||
|
}
|
||||||
|
environment.extensions += [tagExtension]
|
||||||
|
|
||||||
|
template = Template(templateString: "{% block some %}{% customtag %}{% endblock %}", environment: environment)
|
||||||
|
try expectError(reason: "Custom Error", token: "customtag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.context("given included template") {
|
||||||
|
let path = Path(#file) + ".." + "fixtures"
|
||||||
|
let loader = FileSystemLoader(paths: [path])
|
||||||
|
var environment = Environment(loader: loader)
|
||||||
|
var template: Template!
|
||||||
|
var includedTemplate: Template!
|
||||||
|
|
||||||
|
$0.before {
|
||||||
|
environment = Environment(loader: loader)
|
||||||
|
template = nil
|
||||||
|
includedTemplate = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectError(reason: String, token: String, includedToken: String,
|
||||||
|
file: String = #file, line: Int = #line, function: String = #function) throws {
|
||||||
|
var expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
||||||
|
expectedError.stackTrace = [expectedSyntaxError(token: includedToken, template: includedTemplate, description: reason).token!]
|
||||||
|
|
||||||
|
let error = try expect(environment.render(template: template, context: ["target": "World"]),
|
||||||
|
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
||||||
|
let reporter = SimpleErrorReporter()
|
||||||
|
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports syntax error in included template") {
|
||||||
|
template = Template(templateString: "{% include \"invalid-include.html\" %}", environment: environment)
|
||||||
|
includedTemplate = try environment.loadTemplate(name: "invalid-include.html")
|
||||||
|
|
||||||
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||||
|
token: "include \"invalid-include.html\"",
|
||||||
|
includedToken: "target|unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports runtime error in included template") {
|
||||||
|
let filterExtension = Extension()
|
||||||
|
filterExtension.registerFilter("unknown", filter: { (_: Any?) in
|
||||||
|
throw TemplateSyntaxError("filter error")
|
||||||
|
})
|
||||||
|
environment.extensions += [filterExtension]
|
||||||
|
|
||||||
|
template = Template(templateString: "{% include \"invalid-include.html\" %}", environment: environment)
|
||||||
|
includedTemplate = try environment.loadTemplate(name: "invalid-include.html")
|
||||||
|
|
||||||
|
try expectError(reason: "filter error",
|
||||||
|
token: "include \"invalid-include.html\"",
|
||||||
|
includedToken: "target|unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.context("given base and child templates") {
|
||||||
|
let path = Path(#file) + ".." + "fixtures"
|
||||||
|
let loader = FileSystemLoader(paths: [path])
|
||||||
|
var environment: Environment!
|
||||||
|
var childTemplate: Template!
|
||||||
|
var baseTemplate: Template!
|
||||||
|
|
||||||
|
$0.before {
|
||||||
|
environment = Environment(loader: loader)
|
||||||
|
childTemplate = nil
|
||||||
|
baseTemplate = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectError(reason: String, childToken: String, baseToken: String?,
|
||||||
|
file: String = #file, line: Int = #line, function: String = #function) throws {
|
||||||
|
var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason)
|
||||||
|
if let baseToken = baseToken {
|
||||||
|
expectedError.stackTrace = [expectedSyntaxError(token: baseToken, template: baseTemplate, description: reason).token!]
|
||||||
|
}
|
||||||
|
let error = try expect(environment.render(template: childTemplate, context: ["target": "World"]),
|
||||||
|
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
||||||
|
let reporter = SimpleErrorReporter()
|
||||||
|
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports syntax error in base template") {
|
||||||
|
childTemplate = try environment.loadTemplate(name: "invalid-child-super.html")
|
||||||
|
baseTemplate = try environment.loadTemplate(name: "invalid-base.html")
|
||||||
|
|
||||||
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||||
|
childToken: "extends \"invalid-base.html\"",
|
||||||
|
baseToken: "target|unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports runtime error in base template") {
|
||||||
|
let filterExtension = Extension()
|
||||||
|
filterExtension.registerFilter("unknown", filter: { (_: Any?) in
|
||||||
|
throw TemplateSyntaxError("filter error")
|
||||||
|
})
|
||||||
|
environment.extensions += [filterExtension]
|
||||||
|
|
||||||
|
childTemplate = try environment.loadTemplate(name: "invalid-child-super.html")
|
||||||
|
baseTemplate = try environment.loadTemplate(name: "invalid-base.html")
|
||||||
|
|
||||||
|
try expectError(reason: "filter error",
|
||||||
|
childToken: "block.super",
|
||||||
|
baseToken: "target|unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports syntax error in child template") {
|
||||||
|
childTemplate = Template(templateString: "{% extends \"base.html\" %}\n" +
|
||||||
|
"{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil)
|
||||||
|
|
||||||
|
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||||
|
childToken: "target|unknown",
|
||||||
|
baseToken: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("reports runtime error in child template") {
|
||||||
|
let filterExtension = Extension()
|
||||||
|
filterExtension.registerFilter("unknown", filter: { (_: Any?) in
|
||||||
|
throw TemplateSyntaxError("filter error")
|
||||||
|
})
|
||||||
|
environment.extensions += [filterExtension]
|
||||||
|
|
||||||
|
childTemplate = Template(templateString: "{% extends \"base.html\" %}\n" +
|
||||||
|
"{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil)
|
||||||
|
|
||||||
|
try expectError(reason: "filter error",
|
||||||
|
childToken: "target|unknown",
|
||||||
|
baseToken: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Expectation {
|
||||||
|
@discardableResult
|
||||||
|
func toThrow<T: Error>() throws -> T {
|
||||||
|
var thrownError: Error? = nil
|
||||||
|
|
||||||
|
do {
|
||||||
|
_ = try expression()
|
||||||
|
} catch {
|
||||||
|
thrownError = error
|
||||||
|
}
|
||||||
|
|
||||||
|
if let thrownError = thrownError {
|
||||||
|
if let thrownError = thrownError as? T {
|
||||||
|
return thrownError
|
||||||
|
} else {
|
||||||
|
throw failure("\(thrownError) is not \(T.self)")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw failure("expression did not throw an error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fileprivate class ExampleLoader: Loader {
|
fileprivate class ExampleLoader: Loader {
|
||||||
func loadTemplate(name: String, environment: Environment) throws -> Template {
|
func loadTemplate(name: String, environment: Environment) throws -> Template {
|
||||||
|
|||||||
@@ -105,19 +105,19 @@ func testExpressions() {
|
|||||||
|
|
||||||
$0.describe("expression parsing") {
|
$0.describe("expression parsing") {
|
||||||
$0.it("can parse a variable expression") {
|
$0.it("can parse a variable expression") {
|
||||||
let expression = try parseExpression(components: ["value"], tokenParser: parser)
|
let expression = try parseExpression(components: ["value"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||||
try expect(expression.evaluate(context: Context())).to.beFalse()
|
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can parse a not expression") {
|
$0.it("can parse a not expression") {
|
||||||
let expression = try parseExpression(components: ["not", "value"], tokenParser: parser)
|
let expression = try parseExpression(components: ["not", "value"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||||
try expect(expression.evaluate(context: Context())).to.beTrue()
|
try expect(expression.evaluate(context: Context())).to.beTrue()
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("and expression") {
|
$0.describe("and expression") {
|
||||||
let expression = try! parseExpression(components: ["lhs", "and", "rhs"], tokenParser: parser)
|
let expression = try! parseExpression(components: ["lhs", "and", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||||
|
|
||||||
$0.it("evaluates to false with lhs false") {
|
$0.it("evaluates to false with lhs false") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse()
|
||||||
@@ -137,7 +137,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("or expression") {
|
$0.describe("or expression") {
|
||||||
let expression = try! parseExpression(components: ["lhs", "or", "rhs"], tokenParser: parser)
|
let expression = try! parseExpression(components: ["lhs", "or", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||||
|
|
||||||
$0.it("evaluates to true with lhs true") {
|
$0.it("evaluates to true with lhs true") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue()
|
||||||
@@ -157,7 +157,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("equality expression") {
|
$0.describe("equality expression") {
|
||||||
let expression = try! parseExpression(components: ["lhs", "==", "rhs"], tokenParser: parser)
|
let expression = try! parseExpression(components: ["lhs", "==", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||||
|
|
||||||
$0.it("evaluates to true with equal lhs/rhs") {
|
$0.it("evaluates to true with equal lhs/rhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue()
|
||||||
@@ -193,7 +193,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("inequality expression") {
|
$0.describe("inequality expression") {
|
||||||
let expression = try! parseExpression(components: ["lhs", "!=", "rhs"], tokenParser: parser)
|
let expression = try! parseExpression(components: ["lhs", "!=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||||
|
|
||||||
$0.it("evaluates to true with inequal lhs/rhs") {
|
$0.it("evaluates to true with inequal lhs/rhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue()
|
||||||
@@ -205,7 +205,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("more than expression") {
|
$0.describe("more than expression") {
|
||||||
let expression = try! parseExpression(components: ["lhs", ">", "rhs"], tokenParser: parser)
|
let expression = try! parseExpression(components: ["lhs", ">", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||||
|
|
||||||
$0.it("evaluates to true with lhs > rhs") {
|
$0.it("evaluates to true with lhs > rhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue()
|
||||||
@@ -217,7 +217,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("more than equal expression") {
|
$0.describe("more than equal expression") {
|
||||||
let expression = try! parseExpression(components: ["lhs", ">=", "rhs"], tokenParser: parser)
|
let expression = try! parseExpression(components: ["lhs", ">=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||||
|
|
||||||
$0.it("evaluates to true with lhs == rhs") {
|
$0.it("evaluates to true with lhs == rhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
|
||||||
@@ -229,7 +229,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("less than expression") {
|
$0.describe("less than expression") {
|
||||||
let expression = try! parseExpression(components: ["lhs", "<", "rhs"], tokenParser: parser)
|
let expression = try! parseExpression(components: ["lhs", "<", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||||
|
|
||||||
$0.it("evaluates to true with lhs < rhs") {
|
$0.it("evaluates to true with lhs < rhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue()
|
||||||
@@ -241,7 +241,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("less than equal expression") {
|
$0.describe("less than equal expression") {
|
||||||
let expression = try! parseExpression(components: ["lhs", "<=", "rhs"], tokenParser: parser)
|
let expression = try! parseExpression(components: ["lhs", "<=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||||
|
|
||||||
$0.it("evaluates to true with lhs == rhs") {
|
$0.it("evaluates to true with lhs == rhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
|
||||||
@@ -253,7 +253,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("multiple expression") {
|
$0.describe("multiple expression") {
|
||||||
let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"], tokenParser: parser)
|
let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||||
|
|
||||||
$0.it("evaluates to true with one") {
|
$0.it("evaluates to true with one") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue()
|
||||||
@@ -281,7 +281,7 @@ func testExpressions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.describe("in expression") {
|
$0.describe("in expression") {
|
||||||
let expression = try! parseExpression(components: ["lhs", "in", "rhs"], tokenParser: parser)
|
let expression = try! parseExpression(components: ["lhs", "in", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||||
|
|
||||||
$0.it("evaluates to true when rhs contains lhs") {
|
$0.it("evaluates to true when rhs contains lhs") {
|
||||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue()
|
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue()
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func testFilter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let context = Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))
|
let context = Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))
|
||||||
try expect(try template.render(context)).toThrow(TemplateSyntaxError("No Repeat"))
|
try expect(try template.render(context)).toThrow(TemplateSyntaxError(reason: "No Repeat", token: template.tokens.first))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("allows you to override a default filter") {
|
$0.it("allows you to override a default filter") {
|
||||||
@@ -214,32 +214,52 @@ func testFilter() {
|
|||||||
|
|
||||||
|
|
||||||
describe("filter suggestion") {
|
describe("filter suggestion") {
|
||||||
|
var template: Template!
|
||||||
|
var filterExtension: Extension!
|
||||||
|
|
||||||
|
func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError {
|
||||||
|
guard let range = template.templateString.range(of: token) else {
|
||||||
|
fatalError("Can't find '\(token)' in '\(template)'")
|
||||||
|
}
|
||||||
|
let rangeLine = template.templateString.rangeLine(range)
|
||||||
|
let sourceMap = SourceMap(filename: template.name, line: rangeLine)
|
||||||
|
let token = Token.block(value: token, at: sourceMap)
|
||||||
|
return TemplateSyntaxError(reason: description, token: token, stackTrace: [])
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectError(reason: String, token: String,
|
||||||
|
file: String = #file, line: Int = #line, function: String = #function) throws {
|
||||||
|
let expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
||||||
|
let environment = Environment(extensions: [filterExtension])
|
||||||
|
|
||||||
|
let error = try expect(environment.render(template: template, context: [:]),
|
||||||
|
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
||||||
|
let reporter = SimpleErrorReporter()
|
||||||
|
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
||||||
|
}
|
||||||
|
|
||||||
$0.it("made for unknown filter") {
|
$0.it("made for unknown filter") {
|
||||||
let template = Template(templateString: "{{ value|unknownFilter }}")
|
template = Template(templateString: "{{ value|unknownFilter }}")
|
||||||
let expectedError = TemplateSyntaxError("Unknown filter 'unknownFilter'. Found similar filters: 'knownFilter'")
|
|
||||||
|
|
||||||
let filterExtension = Extension()
|
filterExtension = Extension()
|
||||||
filterExtension.registerFilter("knownFilter") { value, _ in value }
|
filterExtension.registerFilter("knownFilter") { value, _ in value }
|
||||||
|
|
||||||
try expect(template.render(Context(dictionary: [:], environment: Environment(extensions: [filterExtension])))).toThrow(expectedError)
|
try expectError(reason: "Unknown filter 'unknownFilter'. Found similar filters: 'knownFilter'.", token: "value|unknownFilter")
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("made for multiple similar filters") {
|
$0.it("made for multiple similar filters") {
|
||||||
let template = Template(templateString: "{{ value|lowerFirst }}")
|
template = Template(templateString: "{{ value|lowerFirst }}")
|
||||||
let expectedError = TemplateSyntaxError("Unknown filter 'lowerFirst'. Found similar filters: 'lowerFirstWord', 'lowercase'")
|
|
||||||
|
|
||||||
let filterExtension = Extension()
|
filterExtension = Extension()
|
||||||
filterExtension.registerFilter("lowerFirstWord") { value, _ in value }
|
filterExtension.registerFilter("lowerFirstWord") { value, _ in value }
|
||||||
filterExtension.registerFilter("lowerFirstLetter") { value, _ in value }
|
filterExtension.registerFilter("lowerFirstLetter") { value, _ in value }
|
||||||
|
|
||||||
try expect(template.render(Context(dictionary: [:], environment: Environment(extensions: [filterExtension])))).toThrow(expectedError)
|
try expectError(reason: "Unknown filter 'lowerFirst'. Found similar filters: 'lowerFirstWord', 'lowercase'.", token: "value|lowerFirst")
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("not made when can't find similar filter") {
|
$0.it("not made when can't find similar filter") {
|
||||||
let template = Template(templateString: "{{ value|unknownFilter }}")
|
template = Template(templateString: "{{ value|unknownFilter }}")
|
||||||
let expectedError = TemplateSyntaxError("Unknown filter 'unknownFilter'.")
|
try expectError(reason: "Unknown filter 'unknownFilter'. Found similar filters: 'lowerFirstWord'.", token: "value|unknownFilter")
|
||||||
try expect(template.render(Context(dictionary: [:]))).toThrow(expectedError)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func testFilterTag() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("errors without a filter") {
|
$0.it("errors without a filter") {
|
||||||
let template = Template(templateString: "{% filter %}Test{% endfilter %}")
|
let template = Template(templateString: "Some {% filter %}Test{% endfilter %}")
|
||||||
try expect(try template.render()).toThrow()
|
try expect(try template.render()).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ func testForNode() {
|
|||||||
"two": "II",
|
"two": "II",
|
||||||
],
|
],
|
||||||
"tuples": [(1, 2, 3), (4, 5, 6)]
|
"tuples": [(1, 2, 3), (4, 5, 6)]
|
||||||
])
|
])
|
||||||
|
|
||||||
$0.it("renders the given nodes for each item") {
|
$0.it("renders the given nodes for each item") {
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||||
@@ -31,7 +31,7 @@ func testForNode() {
|
|||||||
$0.it("renders a context variable of type Array<Any>") {
|
$0.it("renders a context variable of type Array<Any>") {
|
||||||
let any_context = Context(dictionary: [
|
let any_context = Context(dictionary: [
|
||||||
"items": ([1, 2, 3] as [Any])
|
"items": ([1, 2, 3] as [Any])
|
||||||
])
|
])
|
||||||
|
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||||
@@ -54,17 +54,17 @@ func testForNode() {
|
|||||||
try expect(try node.render(context)) == "123"
|
try expect(try node.render(context)) == "123"
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(OSX)
|
#if os(OSX)
|
||||||
$0.it("renders a context variable of type NSArray") {
|
$0.it("renders a context variable of type NSArray") {
|
||||||
let nsarray_context = Context(dictionary: [
|
let nsarray_context = Context(dictionary: [
|
||||||
"items": NSArray(array: [1, 2, 3])
|
"items": NSArray(array: [1, 2, 3])
|
||||||
])
|
])
|
||||||
|
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||||
try expect(try node.render(nsarray_context)) == "123"
|
try expect(try node.render(nsarray_context)) == "123"
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
$0.it("renders the given nodes while providing if the item is first in the context") {
|
$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 nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.first")]
|
||||||
@@ -97,31 +97,31 @@ func testForNode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("renders the given nodes while filtering items using where expression") {
|
$0.it("renders the given nodes while filtering items using where expression") {
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
|
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
|
||||||
let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment()))
|
let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown))
|
||||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`)
|
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`)
|
||||||
try expect(try node.render(context)) == "2132"
|
try expect(try node.render(context)) == "2132"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("renders the given empty nodes when all items filtered out with where expression") {
|
$0.it("renders the given empty nodes when all items filtered out with where expression") {
|
||||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||||
let `where` = try parseExpression(components: ["item", "==", "0"], tokenParser: TokenParser(tokens: [], environment: Environment()))
|
let `where` = try parseExpression(components: ["item", "==", "0"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown))
|
||||||
let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`)
|
let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`)
|
||||||
try expect(try node.render(context)) == "empty"
|
try expect(try node.render(context)) == "empty"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can render a filter with spaces") {
|
$0.it("can render a filter with spaces") {
|
||||||
let templateString = "{% for article in ars | default: a, b , articles %}" +
|
let templateString = "{% for article in ars | default: a, b , articles %}" +
|
||||||
"- {{ article.title }} by {{ article.author }}.\n" +
|
"- {{ article.title }} by {{ article.author }}.\n" +
|
||||||
"{% endfor %}\n"
|
"{% endfor %}\n"
|
||||||
|
|
||||||
let context = Context(dictionary: [
|
let context = Context(dictionary: [
|
||||||
"articles": [
|
"articles": [
|
||||||
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
|
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
|
||||||
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
|
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
|
|
||||||
let template = Template(templateString: templateString)
|
let template = Template(templateString: templateString)
|
||||||
let result = try template.render(context)
|
let result = try template.render(context)
|
||||||
@@ -129,7 +129,7 @@ func testForNode() {
|
|||||||
let fixture = "" +
|
let fixture = "" +
|
||||||
"- Migrating from OCUnit to XCTest by Kyle Fuller.\n" +
|
"- Migrating from OCUnit to XCTest by Kyle Fuller.\n" +
|
||||||
"- Memory Management with ARC by Kyle Fuller.\n" +
|
"- Memory Management with ARC by Kyle Fuller.\n" +
|
||||||
"\n"
|
"\n"
|
||||||
|
|
||||||
try expect(result) == fixture
|
try expect(result) == fixture
|
||||||
}
|
}
|
||||||
@@ -184,7 +184,7 @@ func testForNode() {
|
|||||||
$0.it("can iterate over dictionary") {
|
$0.it("can iterate over dictionary") {
|
||||||
let templateString = "{% for key, value in dict %}" +
|
let templateString = "{% for key, value in dict %}" +
|
||||||
"{{ key }}: {{ value }}," +
|
"{{ key }}: {{ value }}," +
|
||||||
"{% endfor %}"
|
"{% endfor %}"
|
||||||
|
|
||||||
let template = Template(templateString: templateString)
|
let template = Template(templateString: templateString)
|
||||||
let result = try template.render(context)
|
let result = try template.render(context)
|
||||||
@@ -197,7 +197,7 @@ func testForNode() {
|
|||||||
let nodes: [NodeType] = [
|
let nodes: [NodeType] = [
|
||||||
VariableNode(variable: "key"),
|
VariableNode(variable: "key"),
|
||||||
TextNode(text: ","),
|
TextNode(text: ","),
|
||||||
]
|
]
|
||||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||||
let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key"], nodes: nodes, emptyNodes: emptyNodes, where: nil)
|
let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key"], nodes: nodes, emptyNodes: emptyNodes, where: nil)
|
||||||
let result = try node.render(context)
|
let result = try node.render(context)
|
||||||
@@ -212,7 +212,7 @@ func testForNode() {
|
|||||||
TextNode(text: "="),
|
TextNode(text: "="),
|
||||||
VariableNode(variable: "value"),
|
VariableNode(variable: "value"),
|
||||||
TextNode(text: ","),
|
TextNode(text: ","),
|
||||||
]
|
]
|
||||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||||
let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key", "value"], nodes: nodes, emptyNodes: emptyNodes, where: nil)
|
let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key", "value"], nodes: nodes, emptyNodes: emptyNodes, where: nil)
|
||||||
|
|
||||||
@@ -223,11 +223,9 @@ func testForNode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("handles invalid input") {
|
$0.it("handles invalid input") {
|
||||||
let tokens: [Token] = [
|
let token = Token.block(value: "for i", at: .unknown)
|
||||||
.block(value: "for i"),
|
let parser = TokenParser(tokens: [token], environment: Environment())
|
||||||
]
|
let error = TemplateSyntaxError(reason: "'for' statements should use the syntax: `for <x> in <y> [where <condition>]`.", token: token)
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
|
||||||
let error = TemplateSyntaxError("'for' statements should use the syntax: `for <x> in <y> [where <condition>]")
|
|
||||||
try expect(try parser.parse()).toThrow(error)
|
try expect(try parser.parse()).toThrow(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,14 +237,14 @@ func testForNode() {
|
|||||||
|
|
||||||
let context = Context(dictionary: [
|
let context = Context(dictionary: [
|
||||||
"struct": MyStruct(string: "abc", number: 123)
|
"struct": MyStruct(string: "abc", number: 123)
|
||||||
])
|
])
|
||||||
|
|
||||||
let nodes: [NodeType] = [
|
let nodes: [NodeType] = [
|
||||||
VariableNode(variable: "property"),
|
VariableNode(variable: "property"),
|
||||||
TextNode(text: "="),
|
TextNode(text: "="),
|
||||||
VariableNode(variable: "value"),
|
VariableNode(variable: "value"),
|
||||||
TextNode(text: "\n"),
|
TextNode(text: "\n"),
|
||||||
]
|
]
|
||||||
let node = ForNode(resolvable: Variable("struct"), loopVariables: ["property", "value"], nodes: nodes, emptyNodes: [])
|
let node = ForNode(resolvable: Variable("struct"), loopVariables: ["property", "value"], nodes: nodes, emptyNodes: [])
|
||||||
let result = try node.render(context)
|
let result = try node.render(context)
|
||||||
|
|
||||||
@@ -256,14 +254,14 @@ func testForNode() {
|
|||||||
$0.it("can iterate tuple items") {
|
$0.it("can iterate tuple items") {
|
||||||
let context = Context(dictionary: [
|
let context = Context(dictionary: [
|
||||||
"tuple": (one: 1, two: "dva"),
|
"tuple": (one: 1, two: "dva"),
|
||||||
])
|
])
|
||||||
|
|
||||||
let nodes: [NodeType] = [
|
let nodes: [NodeType] = [
|
||||||
VariableNode(variable: "label"),
|
VariableNode(variable: "label"),
|
||||||
TextNode(text: "="),
|
TextNode(text: "="),
|
||||||
VariableNode(variable: "value"),
|
VariableNode(variable: "value"),
|
||||||
TextNode(text: "\n"),
|
TextNode(text: "\n"),
|
||||||
]
|
]
|
||||||
|
|
||||||
let node = ForNode(resolvable: Variable("tuple"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: [])
|
let node = ForNode(resolvable: Variable("tuple"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: [])
|
||||||
let result = try node.render(context)
|
let result = try node.render(context)
|
||||||
@@ -291,15 +289,15 @@ func testForNode() {
|
|||||||
|
|
||||||
let context = Context(dictionary: [
|
let context = Context(dictionary: [
|
||||||
"class": MySubclass("child", "base", 1)
|
"class": MySubclass("child", "base", 1)
|
||||||
])
|
])
|
||||||
|
|
||||||
let nodes: [NodeType] = [
|
let nodes: [NodeType] = [
|
||||||
VariableNode(variable: "label"),
|
VariableNode(variable: "label"),
|
||||||
TextNode(text: "="),
|
TextNode(text: "="),
|
||||||
VariableNode(variable: "value"),
|
VariableNode(variable: "value"),
|
||||||
TextNode(text: "\n"),
|
TextNode(text: "\n"),
|
||||||
]
|
]
|
||||||
|
|
||||||
let node = ForNode(resolvable: Variable("class"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: [])
|
let node = ForNode(resolvable: Variable("class"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: [])
|
||||||
let result = try node.render(context)
|
let result = try node.render(context)
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ func testIfNode() {
|
|||||||
$0.describe("parsing") {
|
$0.describe("parsing") {
|
||||||
$0.it("can parse an if block") {
|
$0.it("can parse an if block") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value"),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -25,11 +25,11 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("can parse an if with else block") {
|
$0.it("can parse an if with else block") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value"),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "else"),
|
.block(value: "else", at: .unknown),
|
||||||
.text(value: "false"),
|
.text(value: "false", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -50,13 +50,13 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("can parse an if with elif block") {
|
$0.it("can parse an if with elif block") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value"),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "elif something"),
|
.block(value: "elif something", at: .unknown),
|
||||||
.text(value: "some"),
|
.text(value: "some", at: .unknown),
|
||||||
.block(value: "else"),
|
.block(value: "else", at: .unknown),
|
||||||
.text(value: "false"),
|
.text(value: "false", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -81,11 +81,11 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("can parse an if with elif block without else") {
|
$0.it("can parse an if with elif block without else") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value"),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "elif something"),
|
.block(value: "elif something", at: .unknown),
|
||||||
.text(value: "some"),
|
.text(value: "some", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -106,15 +106,15 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("can parse an if with multiple elif block") {
|
$0.it("can parse an if with multiple elif block") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value"),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "elif something1"),
|
.block(value: "elif something1", at: .unknown),
|
||||||
.text(value: "some1"),
|
.text(value: "some1", at: .unknown),
|
||||||
.block(value: "elif something2"),
|
.block(value: "elif something2", at: .unknown),
|
||||||
.text(value: "some2"),
|
.text(value: "some2", at: .unknown),
|
||||||
.block(value: "else"),
|
.block(value: "else", at: .unknown),
|
||||||
.text(value: "false"),
|
.text(value: "false", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -144,9 +144,9 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("can parse an if with complex expression") {
|
$0.it("can parse an if with complex expression") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value == \"test\" and not name"),
|
.block(value: "if value == \"test\" and not name", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -156,11 +156,11 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("can parse an ifnot block") {
|
$0.it("can parse an ifnot block") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "ifnot value"),
|
.block(value: "ifnot value", at: .unknown),
|
||||||
.text(value: "false"),
|
.text(value: "false", at: .unknown),
|
||||||
.block(value: "else"),
|
.block(value: "else", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -179,22 +179,18 @@ func testIfNode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws an error when parsing an if block without an endif") {
|
$0.it("throws an error when parsing an if block without an endif") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [.block(value: "if value", at: .unknown)]
|
||||||
.block(value: "if value"),
|
|
||||||
]
|
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
let error = TemplateSyntaxError("`endif` was not found.")
|
let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first)
|
||||||
try expect(try parser.parse()).toThrow(error)
|
try expect(try parser.parse()).toThrow(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws an error when parsing an ifnot without an endif") {
|
$0.it("throws an error when parsing an ifnot without an endif") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [.block(value: "ifnot value", at: .unknown)]
|
||||||
.block(value: "ifnot value"),
|
|
||||||
]
|
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
let error = TemplateSyntaxError("`endif` was not found.")
|
let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first)
|
||||||
try expect(try parser.parse()).toThrow(error)
|
try expect(try parser.parse()).toThrow(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,9 +238,9 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("supports variable filters in the if expression") {
|
$0.it("supports variable filters in the if expression") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value|uppercase == \"TEST\""),
|
.block(value: "if value|uppercase == \"TEST\"", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -256,9 +252,9 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("evaluates nil properties as false") {
|
$0.it("evaluates nil properties as false") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if instance.value"),
|
.block(value: "if instance.value", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -273,11 +269,11 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("supports closed range variables") {
|
$0.it("supports closed range variables") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value in 1...3"),
|
.block(value: "if value in 1...3", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "else"),
|
.block(value: "else", at: .unknown),
|
||||||
.text(value: "false"),
|
.text(value: "false", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ func testInclude() {
|
|||||||
|
|
||||||
$0.describe("parsing") {
|
$0.describe("parsing") {
|
||||||
$0.it("throws an error when no template is given") {
|
$0.it("throws an error when no template is given") {
|
||||||
let tokens: [Token] = [ .block(value: "include") ]
|
let tokens: [Token] = [ .block(value: "include", at: .unknown) ]
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
|
|
||||||
let error = TemplateSyntaxError("'include' tag requires one argument, the template file to be included. A second optional argument can be used to specify the context that will be passed to the included file")
|
let error = TemplateSyntaxError(reason: "'include' tag requires one argument, the template file to be included. A second optional argument can be used to specify the context that will be passed to the included file", token: tokens.first)
|
||||||
try expect(try parser.parse()).toThrow(error)
|
try expect(try parser.parse()).toThrow(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can parse a valid include block") {
|
$0.it("can parse a valid include block") {
|
||||||
let tokens: [Token] = [ .block(value: "include \"test.html\"") ]
|
let tokens: [Token] = [ .block(value: "include \"test.html\"", at: .unknown) ]
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
|
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
@@ -31,7 +31,7 @@ func testInclude() {
|
|||||||
|
|
||||||
$0.describe("rendering") {
|
$0.describe("rendering") {
|
||||||
$0.it("throws an error when rendering without a loader") {
|
$0.it("throws an error when rendering without a loader") {
|
||||||
let node = IncludeNode(templateName: Variable("\"test.html\""))
|
let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown))
|
||||||
|
|
||||||
do {
|
do {
|
||||||
_ = try node.render(Context())
|
_ = try node.render(Context())
|
||||||
@@ -41,7 +41,7 @@ func testInclude() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws an error when it cannot find the included template") {
|
$0.it("throws an error when it cannot find the included template") {
|
||||||
let node = IncludeNode(templateName: Variable("\"unknown.html\""))
|
let node = IncludeNode(templateName: Variable("\"unknown.html\""), token: .block(value: "", at: .unknown))
|
||||||
|
|
||||||
do {
|
do {
|
||||||
_ = try node.render(Context(environment: environment))
|
_ = try node.render(Context(environment: environment))
|
||||||
@@ -51,7 +51,7 @@ func testInclude() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("successfully renders a found included template") {
|
$0.it("successfully renders a found included template") {
|
||||||
let node = IncludeNode(templateName: Variable("\"test.html\""))
|
let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown))
|
||||||
let context = Context(dictionary: ["target": "World"], environment: environment)
|
let context = Context(dictionary: ["target": "World"], environment: environment)
|
||||||
let value = try node.render(context)
|
let value = try node.render(context)
|
||||||
try expect(value) == "Hello World!"
|
try expect(value) == "Hello World!"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ func testLexer() {
|
|||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
try expect(tokens.count) == 1
|
try expect(tokens.count) == 1
|
||||||
try expect(tokens.first) == .text(value: "Hello World")
|
try expect(tokens.first) == .text(value: "Hello World", at: SourceMap(line: ("Hello World", 1, 0)))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize a comment") {
|
$0.it("can tokenize a comment") {
|
||||||
@@ -17,7 +17,7 @@ func testLexer() {
|
|||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
try expect(tokens.count) == 1
|
try expect(tokens.count) == 1
|
||||||
try expect(tokens.first) == .comment(value: "Comment")
|
try expect(tokens.first) == .comment(value: "Comment", at: SourceMap(line: ("{# Comment #}", 1, 3)))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize a variable") {
|
$0.it("can tokenize a variable") {
|
||||||
@@ -25,34 +25,37 @@ func testLexer() {
|
|||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
try expect(tokens.count) == 1
|
try expect(tokens.count) == 1
|
||||||
try expect(tokens.first) == .variable(value: "Variable")
|
try expect(tokens.first) == .variable(value: "Variable", at: SourceMap(line: ("{{ Variable }}", 1, 3)))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize unclosed tag by ignoring it") {
|
$0.it("can tokenize unclosed tag by ignoring it") {
|
||||||
let lexer = Lexer(templateString: "{{ thing")
|
let templateString = "{{ thing"
|
||||||
|
let lexer = Lexer(templateString: templateString)
|
||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
try expect(tokens.count) == 1
|
try expect(tokens.count) == 1
|
||||||
try expect(tokens.first) == .text(value: "")
|
try expect(tokens.first) == .text(value: "", at: SourceMap(line: ("{{ thing", 1, 0)))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize a mixture of content") {
|
$0.it("can tokenize a mixture of content") {
|
||||||
let lexer = Lexer(templateString: "My name is {{ name }}.")
|
let templateString = "My name is {{ myname }}."
|
||||||
|
let lexer = Lexer(templateString: templateString)
|
||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
try expect(tokens.count) == 3
|
try expect(tokens.count) == 3
|
||||||
try expect(tokens[0]) == Token.text(value: "My name is ")
|
try expect(tokens[0]) == Token.text(value: "My name is ", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "My name is ")!)))
|
||||||
try expect(tokens[1]) == Token.variable(value: "name")
|
try expect(tokens[1]) == Token.variable(value: "myname", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "myname")!)))
|
||||||
try expect(tokens[2]) == Token.text(value: ".")
|
try expect(tokens[2]) == Token.text(value: ".", at: SourceMap(line: templateString.rangeLine(templateString.range(of: ".")!)))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize two variables without being greedy") {
|
$0.it("can tokenize two variables without being greedy") {
|
||||||
let lexer = Lexer(templateString: "{{ thing }}{{ name }}")
|
let templateString = "{{ thing }}{{ name }}"
|
||||||
|
let lexer = Lexer(templateString: templateString)
|
||||||
let tokens = lexer.tokenize()
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
try expect(tokens.count) == 2
|
try expect(tokens.count) == 2
|
||||||
try expect(tokens[0]) == Token.variable(value: "thing")
|
try expect(tokens[0]) == Token.variable(value: "thing", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "thing")!)))
|
||||||
try expect(tokens[1]) == Token.variable(value: "name")
|
try expect(tokens[1]) == Token.variable(value: "name", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "name")!)))
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize an unclosed block") {
|
$0.it("can tokenize an unclosed block") {
|
||||||
@@ -66,25 +69,26 @@ func testLexer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize with new lines") {
|
$0.it("can tokenize with new lines") {
|
||||||
let lexer = Lexer(templateString:
|
let templateString =
|
||||||
"My name is {%\n" +
|
"My name is {%\n" +
|
||||||
" if name\n" +
|
" if name\n" +
|
||||||
" and\n" +
|
" and\n" +
|
||||||
" name\n" +
|
" name\n" +
|
||||||
"%}{{\n" +
|
"%}{{\n" +
|
||||||
"name\n" +
|
"name\n" +
|
||||||
"}}{%\n" +
|
"}}{%\n" +
|
||||||
"endif %}.")
|
"endif %}."
|
||||||
|
|
||||||
let tokens = lexer.tokenize()
|
let lexer = Lexer(templateString: templateString)
|
||||||
|
|
||||||
try expect(tokens.count) == 5
|
let tokens = lexer.tokenize()
|
||||||
try expect(tokens[0]) == Token.text(value: "My name is ")
|
|
||||||
try expect(tokens[1]) == Token.block(value: "if name and name")
|
|
||||||
try expect(tokens[2]) == Token.variable(value: "name")
|
|
||||||
try expect(tokens[3]) == Token.block(value: "endif")
|
|
||||||
try expect(tokens[4]) == Token.text(value: ".")
|
|
||||||
|
|
||||||
|
try expect(tokens.count) == 5
|
||||||
|
try expect(tokens[0]) == Token.text(value: "My name is ", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "My name is")!)))
|
||||||
|
try expect(tokens[1]) == Token.block(value: "if name and name", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "{%")!)))
|
||||||
|
try expect(tokens[2]) == Token.variable(value: "name", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "name", options: [.backwards])!)))
|
||||||
|
try expect(tokens[3]) == Token.block(value: "endif", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "endif")!)))
|
||||||
|
try expect(tokens[4]) == Token.text(value: ".", at: SourceMap(line: templateString.rangeLine(templateString.range(of: ".")!)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ import Spectre
|
|||||||
|
|
||||||
|
|
||||||
class ErrorNode : NodeType {
|
class ErrorNode : NodeType {
|
||||||
|
let token: Token?
|
||||||
|
init(token: Token? = nil) {
|
||||||
|
self.token = token
|
||||||
|
}
|
||||||
|
|
||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
throw TemplateSyntaxError("Custom Error")
|
throw TemplateSyntaxError("Custom Error")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ func testNowNode() {
|
|||||||
describe("NowNode") {
|
describe("NowNode") {
|
||||||
$0.describe("parsing") {
|
$0.describe("parsing") {
|
||||||
$0.it("parses default format without any now arguments") {
|
$0.it("parses default format without any now arguments") {
|
||||||
let tokens: [Token] = [ .block(value: "now") ]
|
let tokens: [Token] = [ .block(value: "now", at: .unknown) ]
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
|
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
@@ -18,7 +18,7 @@ func testNowNode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("parses now with a format") {
|
$0.it("parses now with a format") {
|
||||||
let tokens: [Token] = [ .block(value: "now \"HH:mm\"") ]
|
let tokens: [Token] = [ .block(value: "now \"HH:mm\"", at: .unknown) ]
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
let node = nodes.first as? NowNode
|
let node = nodes.first as? NowNode
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ func testTokenParser() {
|
|||||||
describe("TokenParser") {
|
describe("TokenParser") {
|
||||||
$0.it("can parse a text token") {
|
$0.it("can parse a text token") {
|
||||||
let parser = TokenParser(tokens: [
|
let parser = TokenParser(tokens: [
|
||||||
.text(value: "Hello World")
|
.text(value: "Hello World", at: .unknown)
|
||||||
], environment: Environment())
|
], environment: Environment())
|
||||||
|
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
@@ -18,7 +18,7 @@ func testTokenParser() {
|
|||||||
|
|
||||||
$0.it("can parse a variable token") {
|
$0.it("can parse a variable token") {
|
||||||
let parser = TokenParser(tokens: [
|
let parser = TokenParser(tokens: [
|
||||||
.variable(value: "'name'")
|
.variable(value: "'name'", at: .unknown)
|
||||||
], environment: Environment())
|
], environment: Environment())
|
||||||
|
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
@@ -30,7 +30,7 @@ func testTokenParser() {
|
|||||||
|
|
||||||
$0.it("can parse a comment token") {
|
$0.it("can parse a comment token") {
|
||||||
let parser = TokenParser(tokens: [
|
let parser = TokenParser(tokens: [
|
||||||
.comment(value: "Secret stuff!")
|
.comment(value: "Secret stuff!", at: .unknown)
|
||||||
], environment: Environment())
|
], environment: Environment())
|
||||||
|
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
@@ -44,7 +44,7 @@ func testTokenParser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let parser = TokenParser(tokens: [
|
let parser = TokenParser(tokens: [
|
||||||
.block(value: "known"),
|
.block(value: "known", at: .unknown),
|
||||||
], environment: Environment(extensions: [simpleExtension]))
|
], environment: Environment(extensions: [simpleExtension]))
|
||||||
|
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
@@ -52,11 +52,10 @@ func testTokenParser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("errors when parsing an unknown tag") {
|
$0.it("errors when parsing an unknown tag") {
|
||||||
let parser = TokenParser(tokens: [
|
let tokens: [Token] = [.block(value: "unknown", at: .unknown)]
|
||||||
.block(value: "unknown"),
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
], environment: Environment())
|
|
||||||
|
|
||||||
try expect(try parser.parse()).toThrow(TemplateSyntaxError("Unknown template tag 'unknown'"))
|
try expect(try parser.parse()).toThrow(TemplateSyntaxError(reason: "Unknown template tag 'unknown'", token: tokens.first))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import Spectre
|
|||||||
import Stencil
|
import Stencil
|
||||||
|
|
||||||
|
|
||||||
fileprivate class CustomNode : NodeType {
|
fileprivate struct CustomNode : NodeType {
|
||||||
|
let token: Token?
|
||||||
func render(_ context:Context) throws -> String {
|
func render(_ context:Context) throws -> String {
|
||||||
return "Hello World"
|
return "Hello World"
|
||||||
}
|
}
|
||||||
@@ -24,7 +25,7 @@ func testStencil() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exampleExtension.registerTag("customtag") { parser, token in
|
exampleExtension.registerTag("customtag") { parser, token in
|
||||||
return CustomNode()
|
return CustomNode(token: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
let environment = Environment(extensions: [exampleExtension])
|
let environment = Environment(extensions: [exampleExtension])
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
@testable import Stencil
|
||||||
|
|
||||||
|
|
||||||
func testTemplate() {
|
func testTemplate() {
|
||||||
@@ -15,5 +15,6 @@ func testTemplate() {
|
|||||||
let result = try template.render([ "name": "Kyle" ])
|
let result = try template.render([ "name": "Kyle" ])
|
||||||
try expect(result) == "Hello World"
|
try expect(result) == "Hello World"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
@testable import Stencil
|
||||||
|
|
||||||
|
|
||||||
func testToken() {
|
func testToken() {
|
||||||
describe("Token") {
|
describe("Token") {
|
||||||
$0.it("can split the contents into components") {
|
$0.it("can split the contents into components") {
|
||||||
let token = Token.text(value: "hello world")
|
let token = Token.text(value: "hello world", at: .unknown)
|
||||||
let components = token.components()
|
let components = token.components()
|
||||||
|
|
||||||
try expect(components.count) == 2
|
try expect(components.count) == 2
|
||||||
@@ -14,7 +14,7 @@ func testToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can split the contents into components with single quoted strings") {
|
$0.it("can split the contents into components with single quoted strings") {
|
||||||
let token = Token.text(value: "hello 'kyle fuller'")
|
let token = Token.text(value: "hello 'kyle fuller'", at: .unknown)
|
||||||
let components = token.components()
|
let components = token.components()
|
||||||
|
|
||||||
try expect(components.count) == 2
|
try expect(components.count) == 2
|
||||||
@@ -23,7 +23,7 @@ func testToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can split the contents into components with double quoted strings") {
|
$0.it("can split the contents into components with double quoted strings") {
|
||||||
let token = Token.text(value: "hello \"kyle fuller\"")
|
let token = Token.text(value: "hello \"kyle fuller\"", at: .unknown)
|
||||||
let components = token.components()
|
let components = token.components()
|
||||||
|
|
||||||
try expect(components.count) == 2
|
try expect(components.count) == 2
|
||||||
|
|||||||
@@ -292,7 +292,9 @@ func testVariable() {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
func makeVariable(_ token: String) throws -> RangeVariable? {
|
func makeVariable(_ token: String) throws -> RangeVariable? {
|
||||||
return try RangeVariable(token, parser: TokenParser(tokens: [], environment: context.environment))
|
let token = Token.variable(value: token, at: .unknown)
|
||||||
|
let parser = TokenParser(tokens: [token], environment: context.environment)
|
||||||
|
return try RangeVariable(token.contents, parser: parser, containedIn: token)
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can resolve closed range as array") {
|
$0.it("can resolve closed range as array") {
|
||||||
@@ -321,11 +323,11 @@ func testVariable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws is left range value is missing") {
|
$0.it("throws is left range value is missing") {
|
||||||
try expect(makeVariable("...1")).toThrow()
|
try expect(makeVariable("...1")).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("throws is right range value is missing") {
|
$0.it("throws is right range value is missing") {
|
||||||
try expect(makeVariable("1...")).toThrow()
|
try expect(makeVariable("1...")).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
2
Tests/StencilTests/fixtures/invalid-base.html
Normal file
2
Tests/StencilTests/fixtures/invalid-base.html
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
{% block header %}Header{% endblock %}
|
||||||
|
{% block body %}Body {{ target|unknown }} {% endblock %}
|
||||||
3
Tests/StencilTests/fixtures/invalid-child-super.html
Normal file
3
Tests/StencilTests/fixtures/invalid-child-super.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{% extends "invalid-base.html" %}
|
||||||
|
{% block body %}Child {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
1
Tests/StencilTests/fixtures/invalid-include.html
Normal file
1
Tests/StencilTests/fixtures/invalid-include.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Hello {{ target|unknown }}!
|
||||||
Reference in New Issue
Block a user