storing full sourcemap in token, refactored error reporting
This commit is contained in:
@@ -45,30 +45,7 @@ public struct Environment {
|
||||
func render(template: Template, context: [String: Any]?) throws -> String {
|
||||
// update temaplte environment as it cen be created from string literal with default environment
|
||||
template.environment = self
|
||||
errorReporter.context = ErrorReporterContext(template: template)
|
||||
do {
|
||||
return try template.render(context)
|
||||
} catch {
|
||||
throw errorReporter.reportError(error)
|
||||
}
|
||||
}
|
||||
|
||||
var template: Template? {
|
||||
return errorReporter.context?.template
|
||||
}
|
||||
|
||||
public func pushTemplate<Result>(_ template: Template, token: Token?, closure: (() throws -> Result)) rethrows -> Result {
|
||||
let errorReporterContext = errorReporter.context
|
||||
defer { errorReporter.context = errorReporterContext }
|
||||
errorReporter.context = ErrorReporterContext(
|
||||
template: template,
|
||||
parent: errorReporterContext != nil ? (errorReporterContext!, token) : nil
|
||||
)
|
||||
do {
|
||||
return try closure()
|
||||
} catch {
|
||||
throw errorReporter.reportError(error)
|
||||
}
|
||||
return try template.render(context)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,13 +22,15 @@ 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 template: Template?
|
||||
public internal(set) var parentError: Error?
|
||||
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, template: Template? = nil, parentError: Error? = nil) {
|
||||
public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) {
|
||||
self.reason = reason
|
||||
self.parentError = parentError
|
||||
self.template = template
|
||||
self.stackTrace = stackTrace
|
||||
self.token = token
|
||||
}
|
||||
|
||||
@@ -37,77 +39,34 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
|
||||
}
|
||||
|
||||
public static func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool {
|
||||
guard lhs.description == rhs.description else { return false }
|
||||
|
||||
switch (lhs.parentError, rhs.parentError) {
|
||||
case let (lhsParent? as TemplateSyntaxError?, rhsParent? as TemplateSyntaxError?):
|
||||
return lhsParent == rhsParent
|
||||
case let (lhsParent?, rhsParent?):
|
||||
return String(describing: lhsParent) == String(describing: rhsParent)
|
||||
default:
|
||||
return lhs.parentError == nil && rhs.parentError == nil
|
||||
}
|
||||
return lhs.description == rhs.description && lhs.token == rhs.token && lhs.stackTrace == rhs.stackTrace
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class ErrorReporterContext {
|
||||
public let template: Template
|
||||
|
||||
public typealias ParentContext = (context: ErrorReporterContext, token: Token?)
|
||||
public let parent: ParentContext?
|
||||
|
||||
public init(template: Template, parent: ParentContext? = nil) {
|
||||
self.template = template
|
||||
self.parent = parent
|
||||
}
|
||||
}
|
||||
|
||||
public protocol ErrorReporter: class {
|
||||
var context: ErrorReporterContext! { get set }
|
||||
func reportError(_ error: Error) -> Error
|
||||
func renderError(_ error: Error) -> String
|
||||
}
|
||||
|
||||
open class SimpleErrorReporter: ErrorReporter {
|
||||
public var context: ErrorReporterContext!
|
||||
|
||||
open func reportError(_ error: Error) -> Error {
|
||||
guard let context = context else { return error }
|
||||
|
||||
return TemplateSyntaxError(reason: (error as? TemplateSyntaxError)?.reason ?? "\(error)",
|
||||
token: (error as? TemplateSyntaxError)?.token,
|
||||
template: (error as? TemplateSyntaxError)?.template ?? context.template,
|
||||
parentError: (error as? TemplateSyntaxError)?.parentError
|
||||
)
|
||||
}
|
||||
|
||||
open func renderError(_ error: Error) -> String {
|
||||
guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription }
|
||||
|
||||
let description: String
|
||||
if let template = templateError.template, let token = templateError.token {
|
||||
let templateName = template.name.map({ "\($0):" }) ?? ""
|
||||
let range = template.templateString.range(of: token.contents, range: token.range) ?? token.range
|
||||
let line = template.templateString.rangeLine(range)
|
||||
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))))"
|
||||
|
||||
description = "\(templateName)\(line.number):\(line.offset): error: \(templateError.reason)\n"
|
||||
return "\(templateName)\(line.number):\(line.offset): error: \(templateError.reason)\n"
|
||||
+ "\(line.content)\n"
|
||||
+ "\(highlight)\n"
|
||||
} else {
|
||||
description = templateError.reason
|
||||
}
|
||||
|
||||
var descriptions = [description]
|
||||
|
||||
var currentError: TemplateSyntaxError? = templateError
|
||||
while let parentError = currentError?.parentError {
|
||||
descriptions.append(renderError(parentError))
|
||||
currentError = parentError as? TemplateSyntaxError
|
||||
}
|
||||
|
||||
return descriptions.reversed().joined(separator: "\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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,14 +28,12 @@ class IncludeNode : NodeType {
|
||||
let template = try context.environment.loadTemplate(name: templateName)
|
||||
|
||||
do {
|
||||
return try context.environment.pushTemplate(template, token: token) {
|
||||
try context.push {
|
||||
return try template.render(context)
|
||||
}
|
||||
return try context.push {
|
||||
return try template.render(context)
|
||||
}
|
||||
} catch {
|
||||
if let parentError = error as? TemplateSyntaxError {
|
||||
throw TemplateSyntaxError(reason: parentError.reason, parentError: parentError)
|
||||
if let error = error as? TemplateSyntaxError {
|
||||
throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -2,22 +2,22 @@ class BlockContext {
|
||||
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, Template?)]]
|
||||
var blocks: [String: [BlockNode]]
|
||||
|
||||
init(blocks: [String: [(BlockNode, Template?)]]) {
|
||||
init(blocks: [String: [BlockNode]]) {
|
||||
self.blocks = blocks
|
||||
}
|
||||
|
||||
func pushBlock(_ block: BlockNode, named blockName: String, definedIn template: Template?) {
|
||||
func pushBlock(_ block: BlockNode, named blockName: String) {
|
||||
if var blocks = blocks[blockName] {
|
||||
blocks.append((block, template))
|
||||
blocks.append(block)
|
||||
self.blocks[blockName] = blocks
|
||||
} else {
|
||||
self.blocks[blockName] = [(block, template)]
|
||||
self.blocks[blockName] = [block]
|
||||
}
|
||||
}
|
||||
|
||||
func popBlock(named blockName: String) -> (node: BlockNode, template: Template?)? {
|
||||
func popBlock(named blockName: String) -> BlockNode? {
|
||||
if var blocks = blocks[blockName] {
|
||||
let block = blocks.removeFirst()
|
||||
if blocks.isEmpty {
|
||||
@@ -86,34 +86,31 @@ class ExtendsNode : NodeType {
|
||||
}
|
||||
|
||||
let baseTemplate = try context.environment.loadTemplate(name: templateName)
|
||||
let template = context.environment.template
|
||||
|
||||
let blockContext: BlockContext
|
||||
if let _blockContext = context[BlockContext.contextKey] as? BlockContext {
|
||||
blockContext = _blockContext
|
||||
for (name, block) in blocks {
|
||||
blockContext.pushBlock(block, named: name, definedIn: template)
|
||||
blockContext.pushBlock(block, named: name)
|
||||
}
|
||||
} else {
|
||||
var blocks = [String: [(BlockNode, Template?)]]()
|
||||
self.blocks.forEach { blocks[$0.key] = [($0.value, template)] }
|
||||
var blocks = [String: [BlockNode]]()
|
||||
self.blocks.forEach { blocks[$0.key] = [$0.value] }
|
||||
blockContext = BlockContext(blocks: blocks)
|
||||
}
|
||||
|
||||
do {
|
||||
// pushes base template and renders it's content
|
||||
// block_context contains all blocks from child templates
|
||||
return try context.environment.pushTemplate(baseTemplate, token: token) {
|
||||
try context.push(dictionary: [BlockContext.contextKey: blockContext]) {
|
||||
return try baseTemplate.render(context)
|
||||
}
|
||||
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.template !== context.environment.template {
|
||||
throw TemplateSyntaxError(reason: error.reason, parentError: error)
|
||||
if let error = error as? TemplateSyntaxError, error.templateName != token?.sourceMap.filename {
|
||||
throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
@@ -152,7 +149,7 @@ class BlockNode : NodeType {
|
||||
|
||||
var newContext: [String: Any] = [BlockContext.contextKey: blockContext]
|
||||
|
||||
if let blockSuperNode = child.node.nodes.first(where: {
|
||||
if let blockSuperNode = child.nodes.first(where: {
|
||||
if case .variable(let variable, _)? = $0.token, variable == "block.super" { return true }
|
||||
else { return false}
|
||||
}) {
|
||||
@@ -160,27 +157,28 @@ class BlockNode : NodeType {
|
||||
// render current (base) node so that its content can be used as part of node that extends it
|
||||
newContext["block"] = ["super": try self.render(context)]
|
||||
} catch {
|
||||
let baseError = context.errorReporter.reportError(error)
|
||||
throw TemplateSyntaxError(
|
||||
reason: (baseError as? TemplateSyntaxError)?.reason ?? "\(baseError)",
|
||||
token: blockSuperNode.token,
|
||||
template: child.template,
|
||||
parentError: baseError)
|
||||
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: [])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// render extension node
|
||||
do {
|
||||
return try context.push(dictionary: newContext) {
|
||||
return try child.node.render(context)
|
||||
return try child.render(context)
|
||||
}
|
||||
} catch {
|
||||
// child node belongs to child template, which is currently not on top of stack
|
||||
// so we need to use node's template to report errors, not current template
|
||||
// unless it's already set
|
||||
if var error = error as? TemplateSyntaxError {
|
||||
error.template = error.template ?? child.template
|
||||
error.token = error.token ?? child.node.token
|
||||
error.token = error.token ?? child.token
|
||||
throw error
|
||||
} else {
|
||||
throw error
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import Foundation
|
||||
|
||||
struct Lexer {
|
||||
let templateName: String?
|
||||
let templateString: String
|
||||
|
||||
init(templateString: String) {
|
||||
init(templateName: String? = nil, templateString: String) {
|
||||
self.templateName = templateName
|
||||
self.templateString = templateString
|
||||
}
|
||||
|
||||
@@ -16,14 +18,28 @@ struct Lexer {
|
||||
}
|
||||
|
||||
if string.hasPrefix("{{") {
|
||||
return .variable(value: strip(), at: range)
|
||||
let value = strip()
|
||||
let range = templateString.range(of: value, range: range) ?? range
|
||||
let line = templateString.rangeLine(range)
|
||||
let sourceMap = SourceMap(filename: templateName, line: line)
|
||||
return .variable(value: value, at: sourceMap)
|
||||
} else if string.hasPrefix("{%") {
|
||||
return .block(value: strip(), at: range)
|
||||
let value = strip()
|
||||
let range = templateString.range(of: value, range: range) ?? range
|
||||
let line = templateString.rangeLine(range)
|
||||
let sourceMap = SourceMap(filename: templateName, line: line)
|
||||
return .block(value: value, at: sourceMap)
|
||||
} else if string.hasPrefix("{#") {
|
||||
return .comment(value: strip(), at: range)
|
||||
let value = strip()
|
||||
let range = templateString.range(of: value, range: range) ?? range
|
||||
let line = templateString.rangeLine(range)
|
||||
let sourceMap = SourceMap(filename: templateName, line: line)
|
||||
return .comment(value: value, at: sourceMap)
|
||||
}
|
||||
|
||||
return .text(value: string, at: range)
|
||||
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.
|
||||
@@ -41,6 +57,7 @@ struct Lexer {
|
||||
while !scanner.isEmpty {
|
||||
if let text = scanner.scan(until: ["{{", "{%", "{#"]) {
|
||||
if !text.1.isEmpty {
|
||||
let line = templateString.rangeLine(scanner.range)
|
||||
tokens.append(createToken(string: text.1, at: scanner.range))
|
||||
}
|
||||
|
||||
@@ -48,6 +65,7 @@ struct Lexer {
|
||||
let result = scanner.scan(until: end, returnUntil: true)
|
||||
tokens.append(createToken(string: result, at: scanner.range))
|
||||
} else {
|
||||
let line = templateString.rangeLine(scanner.range)
|
||||
tokens.append(createToken(string: scanner.content, at: scanner.range))
|
||||
scanner.content = ""
|
||||
}
|
||||
@@ -165,7 +183,7 @@ extension String {
|
||||
return String(self[first..<last])
|
||||
}
|
||||
|
||||
public func rangeLine(_ range: Range<String.Index>) -> (content: String, number: UInt, offset: String.IndexDistance) {
|
||||
public func rangeLine(_ range: Range<String.Index>) -> RangeLine {
|
||||
var lineNumber: UInt = 0
|
||||
var offset: Int = 0
|
||||
var lineContent = ""
|
||||
@@ -183,3 +201,5 @@ extension String {
|
||||
return (lineContent, lineNumber, offset)
|
||||
}
|
||||
}
|
||||
|
||||
public typealias RangeLine = (content: String, number: UInt, offset: String.IndexDistance)
|
||||
|
||||
@@ -107,9 +107,11 @@ public class TokenParser {
|
||||
return try FilterExpression(token: filterToken, parser: self)
|
||||
} catch {
|
||||
if var error = error as? TemplateSyntaxError, error.token == nil {
|
||||
// find range of filter in the containing token so that only filter is highligted, not the whole token
|
||||
if let filterTokenRange = environment.template?.templateString.range(of: filterToken, range: containingToken.range) {
|
||||
error.token = Token.variable(value: filterToken, at: filterTokenRange)
|
||||
// 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)
|
||||
error.token = .variable(value: filterToken, at: SourceMap(filename: containingToken.sourceMap.filename, line: rangeLine))
|
||||
} else {
|
||||
error.token = containingToken
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ open class Template: ExpressibleByStringLiteral {
|
||||
self.name = name
|
||||
self.templateString = templateString
|
||||
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let lexer = Lexer(templateName: name, templateString: templateString)
|
||||
tokens = lexer.tokenize()
|
||||
}
|
||||
|
||||
|
||||
@@ -40,18 +40,34 @@ 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 {
|
||||
/// A token representing a piece of text.
|
||||
case text(value: String, at: Range<String.Index>)
|
||||
case text(value: String, at: SourceMap)
|
||||
|
||||
/// A token representing a variable.
|
||||
case variable(value: String, at: Range<String.Index>)
|
||||
case variable(value: String, at: SourceMap)
|
||||
|
||||
/// A token representing a comment.
|
||||
case comment(value: String, at: Range<String.Index>)
|
||||
case comment(value: String, at: SourceMap)
|
||||
|
||||
/// A token representing a template block.
|
||||
case block(value: String, at: Range<String.Index>)
|
||||
case block(value: String, at: SourceMap)
|
||||
|
||||
/// Returns the underlying value as an array seperated by spaces
|
||||
public func components() -> [String] {
|
||||
@@ -74,29 +90,29 @@ public enum Token : Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public var range: Range<String.Index> {
|
||||
public var sourceMap: SourceMap {
|
||||
switch self {
|
||||
case .block(_, let range),
|
||||
.variable(_, let range),
|
||||
.text(_, let range),
|
||||
.comment(_, let range):
|
||||
return range
|
||||
case .block(_, let sourceMap),
|
||||
.variable(_, let sourceMap),
|
||||
.text(_, let sourceMap),
|
||||
.comment(_, let sourceMap):
|
||||
return sourceMap
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public func == (lhs: Token, rhs: Token) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.text(let lhsValue), .text(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.variable(let lhsValue), .variable(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.block(let lhsValue), .block(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.comment(let lhsValue), .comment(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case let (.text(lhsValue, lhsAt), .text(rhsValue, rhsAt)):
|
||||
return lhsValue == rhsValue && lhsAt == rhsAt
|
||||
case let (.variable(lhsValue, lhsAt), .variable(rhsValue, rhsAt)):
|
||||
return lhsValue == rhsValue && lhsAt == rhsAt
|
||||
case let (.block(lhsValue, lhsAt), .block(rhsValue, rhsAt)):
|
||||
return lhsValue == rhsValue && lhsAt == rhsAt
|
||||
case let (.comment(lhsValue, lhsAt), .comment(rhsValue, rhsAt)):
|
||||
return lhsValue == rhsValue && lhsAt == rhsAt
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -41,15 +41,17 @@ func testEnvironment() {
|
||||
}
|
||||
|
||||
func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError {
|
||||
let token = Token.block(value: token, at: template.templateString.range(of: token)!)
|
||||
return TemplateSyntaxError(reason: description, token: token, template: template, parentError: nil)
|
||||
let range = template.templateString.range(of: token)!
|
||||
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) throws {
|
||||
let expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
||||
|
||||
let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"]))
|
||||
.toThrow(expectedError)
|
||||
let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"])).toThrow() as TemplateSyntaxError
|
||||
try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError)
|
||||
}
|
||||
|
||||
@@ -200,10 +202,10 @@ func testEnvironment() {
|
||||
|
||||
func expectError(reason: String, token: String, includedToken: String) throws {
|
||||
var expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
||||
expectedError.parentError = expectedSyntaxError(token: includedToken, template: includedTemplate, description: reason)
|
||||
expectedError.stackTrace = [expectedSyntaxError(token: includedToken, template: includedTemplate, description: reason).token!]
|
||||
|
||||
let error = try expect(environment.render(template: template, context: ["target": "World"]))
|
||||
.toThrow(expectedError)
|
||||
.toThrow() as TemplateSyntaxError
|
||||
try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError)
|
||||
}
|
||||
|
||||
@@ -249,11 +251,10 @@ func testEnvironment() {
|
||||
func expectError(reason: String, childToken: String, baseToken: String?) throws {
|
||||
var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason)
|
||||
if let baseToken = baseToken {
|
||||
expectedError.parentError = expectedSyntaxError(token: baseToken, template: baseTemplate, description: reason)
|
||||
expectedError.stackTrace = [expectedSyntaxError(token: baseToken, template: baseTemplate, description: reason).token!]
|
||||
}
|
||||
|
||||
let error = try expect(environment.render(template: childTemplate, context: ["target": "World"]))
|
||||
.toThrow(expectedError)
|
||||
.toThrow() as TemplateSyntaxError
|
||||
try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError)
|
||||
}
|
||||
|
||||
@@ -312,7 +313,7 @@ func testEnvironment() {
|
||||
|
||||
private extension Expectation {
|
||||
@discardableResult
|
||||
func toThrow<T: Error & Equatable>(_ error: T) throws -> T {
|
||||
func toThrow<T: Error>() throws -> T {
|
||||
var thrownError: Error? = nil
|
||||
|
||||
do {
|
||||
@@ -323,12 +324,9 @@ private extension Expectation {
|
||||
|
||||
if let thrownError = thrownError {
|
||||
if let thrownError = thrownError as? T {
|
||||
if error != thrownError {
|
||||
throw failure("\(thrownError) is not \(error)")
|
||||
}
|
||||
return thrownError
|
||||
} else {
|
||||
throw failure("\(thrownError) is not \(error)")
|
||||
throw failure("\(thrownError) is not \(T.self)")
|
||||
}
|
||||
} else {
|
||||
throw failure("expression did not throw an error")
|
||||
|
||||
@@ -62,7 +62,7 @@ func testFilter() {
|
||||
}
|
||||
|
||||
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") {
|
||||
@@ -91,7 +91,7 @@ func testFilter() {
|
||||
|
||||
|
||||
describe("capitalize filter") {
|
||||
let template = Template(templateString: "{{ name|capitalize }}")
|
||||
let template = Template(templateString: "{{ name|capitalize }}")
|
||||
|
||||
$0.it("capitalizes a string") {
|
||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
||||
|
||||
@@ -169,11 +169,9 @@ func testForNode() {
|
||||
}
|
||||
|
||||
$0.it("handles invalid input") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "for i", at: .unknown),
|
||||
]
|
||||
let tokens: [Token] = [.block(value: "for i", at: .unknown)]
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let error = TemplateSyntaxError("'for' statements should use the following syntax 'for x in y where condition'.")
|
||||
let error = TemplateSyntaxError(reason: "'for' statements should use the following syntax 'for x in y where condition'.", token: tokens.first)
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
|
||||
|
||||
@@ -179,22 +179,18 @@ func testIfNode() {
|
||||
}
|
||||
|
||||
$0.it("throws an error when parsing an if block without an endif") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value", at: .unknown),
|
||||
]
|
||||
let tokens: [Token] = [.block(value: "if value", at: .unknown)]
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
$0.it("throws an error when parsing an ifnot without an endif") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "ifnot value", at: .unknown),
|
||||
]
|
||||
let tokens: [Token] = [.block(value: "ifnot value", at: .unknown)]
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func testInclude() {
|
||||
let tokens: [Token] = [ .block(value: "include", at: .unknown) ]
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
|
||||
let error = TemplateSyntaxError("'include' tag takes one argument, the template file to be included")
|
||||
let error = TemplateSyntaxError(reason: "'include' tag takes one argument, the template file to be included", token: tokens.first)
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ func testLexer() {
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .text(value: "Hello World", at: "Hello World".range)
|
||||
try expect(tokens.first) == .text(value: "Hello World", at: SourceMap(line: ("Hello World", 1, 0)))
|
||||
}
|
||||
|
||||
$0.it("can tokenize a comment") {
|
||||
@@ -17,7 +17,7 @@ func testLexer() {
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .comment(value: "Comment", at: "{# Comment #}".range)
|
||||
try expect(tokens.first) == .comment(value: "Comment", at: SourceMap(line: ("{# Comment #}", 1, 3)))
|
||||
}
|
||||
|
||||
$0.it("can tokenize a variable") {
|
||||
@@ -25,7 +25,7 @@ func testLexer() {
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .variable(value: "Variable", at: "{{ Variable }}".range)
|
||||
try expect(tokens.first) == .variable(value: "Variable", at: SourceMap(line: ("{{ Variable }}", 1, 3)))
|
||||
}
|
||||
|
||||
$0.it("can tokenize unclosed tag by ignoring it") {
|
||||
@@ -34,18 +34,18 @@ func testLexer() {
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .text(value: "", at: "".range)
|
||||
try expect(tokens.first) == .text(value: "", at: SourceMap(line: ("{{ thing", 1, 0)))
|
||||
}
|
||||
|
||||
$0.it("can tokenize a mixture of content") {
|
||||
let templateString = "My name is {{ name }}."
|
||||
let templateString = "My name is {{ myname }}."
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 3
|
||||
try expect(tokens[0]) == Token.text(value: "My name is ", at: templateString.range(of: "My name is ")!)
|
||||
try expect(tokens[1]) == Token.variable(value: "name", at: templateString.range(of: "{{ name }}")!)
|
||||
try expect(tokens[2]) == Token.text(value: ".", at: templateString.range(of: ".")!)
|
||||
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: "myname", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "myname")!)))
|
||||
try expect(tokens[2]) == Token.text(value: ".", at: SourceMap(line: templateString.rangeLine(templateString.range(of: ".")!)))
|
||||
}
|
||||
|
||||
$0.it("can tokenize two variables without being greedy") {
|
||||
@@ -54,8 +54,8 @@ func testLexer() {
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 2
|
||||
try expect(tokens[0]) == Token.variable(value: "thing", at: templateString.range(of: "{{ thing }}")!)
|
||||
try expect(tokens[1]) == Token.variable(value: "name", at: templateString.range(of: "{{ name }}")!)
|
||||
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", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "name")!)))
|
||||
}
|
||||
|
||||
$0.it("can tokenize an unclosed block") {
|
||||
|
||||
@@ -52,11 +52,10 @@ func testTokenParser() {
|
||||
}
|
||||
|
||||
$0.it("errors when parsing an unknown tag") {
|
||||
let parser = TokenParser(tokens: [
|
||||
.block(value: "unknown", at: .unknown),
|
||||
], environment: Environment())
|
||||
let tokens: [Token] = [.block(value: "unknown", at: .unknown)]
|
||||
let parser = TokenParser(tokens: tokens, 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user