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 {
|
func render(template: Template, context: [String: Any]?) throws -> String {
|
||||||
// update temaplte environment as it cen be created from string literal with default environment
|
// update temaplte environment as it cen be created from string literal with default environment
|
||||||
template.environment = self
|
template.environment = self
|
||||||
errorReporter.context = ErrorReporterContext(template: template)
|
|
||||||
do {
|
|
||||||
return try template.render(context)
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,13 +22,15 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
|
|||||||
public let reason: String
|
public let reason: String
|
||||||
public var description: String { return reason }
|
public var description: String { return reason }
|
||||||
public internal(set) var token: Token?
|
public internal(set) var token: Token?
|
||||||
public internal(set) var template: Template?
|
public internal(set) var stackTrace: [Token]
|
||||||
public internal(set) var parentError: Error?
|
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.reason = reason
|
||||||
self.parentError = parentError
|
self.stackTrace = stackTrace
|
||||||
self.template = template
|
|
||||||
self.token = token
|
self.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,77 +39,34 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool {
|
public static func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool {
|
||||||
guard lhs.description == rhs.description else { return false }
|
return lhs.description == rhs.description && lhs.token == rhs.token && lhs.stackTrace == rhs.stackTrace
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
public protocol ErrorReporter: class {
|
||||||
var context: ErrorReporterContext! { get set }
|
|
||||||
func reportError(_ error: Error) -> Error
|
|
||||||
func renderError(_ error: Error) -> String
|
func renderError(_ error: Error) -> String
|
||||||
}
|
}
|
||||||
|
|
||||||
open class SimpleErrorReporter: ErrorReporter {
|
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 {
|
open func renderError(_ error: Error) -> String {
|
||||||
guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription }
|
guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription }
|
||||||
|
|
||||||
let description: String
|
func describe(token: Token) -> String {
|
||||||
if let template = templateError.template, let token = templateError.token {
|
let templateName = token.sourceMap.filename ?? ""
|
||||||
let templateName = template.name.map({ "\($0):" }) ?? ""
|
let line = token.sourceMap.line
|
||||||
let range = template.templateString.range(of: token.contents, range: token.range) ?? token.range
|
|
||||||
let line = template.templateString.rangeLine(range)
|
|
||||||
let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(token.contents.characters.count - 1, 0))))"
|
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"
|
+ "\(line.content)\n"
|
||||||
+ "\(highlight)\n"
|
+ "\(highlight)\n"
|
||||||
} else {
|
|
||||||
description = templateError.reason
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var descriptions = [description]
|
var descriptions = templateError.stackTrace.reduce([]) { $0 + [describe(token: $1)] }
|
||||||
|
let description = templateError.token.map(describe(token:)) ?? templateError.reason
|
||||||
var currentError: TemplateSyntaxError? = templateError
|
descriptions.append(description)
|
||||||
while let parentError = currentError?.parentError {
|
return descriptions.joined(separator: "\n")
|
||||||
descriptions.append(renderError(parentError))
|
|
||||||
currentError = parentError as? TemplateSyntaxError
|
|
||||||
}
|
|
||||||
|
|
||||||
return descriptions.reversed().joined(separator: "\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,14 +28,12 @@ class IncludeNode : NodeType {
|
|||||||
let template = try context.environment.loadTemplate(name: templateName)
|
let template = try context.environment.loadTemplate(name: templateName)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
return try context.environment.pushTemplate(template, token: token) {
|
return try context.push {
|
||||||
try context.push {
|
|
||||||
return try template.render(context)
|
return try template.render(context)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
if let parentError = error as? TemplateSyntaxError {
|
if let error = error as? TemplateSyntaxError {
|
||||||
throw TemplateSyntaxError(reason: parentError.reason, parentError: parentError)
|
throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens)
|
||||||
} else {
|
} else {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,22 @@ 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
|
// 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
|
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] {
|
if var blocks = blocks[blockName] {
|
||||||
blocks.append((block, template))
|
blocks.append(block)
|
||||||
self.blocks[blockName] = blocks
|
self.blocks[blockName] = blocks
|
||||||
} else {
|
} 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] {
|
if var blocks = blocks[blockName] {
|
||||||
let block = blocks.removeFirst()
|
let block = blocks.removeFirst()
|
||||||
if blocks.isEmpty {
|
if blocks.isEmpty {
|
||||||
@@ -86,34 +86,31 @@ class ExtendsNode : NodeType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let baseTemplate = try context.environment.loadTemplate(name: templateName)
|
let baseTemplate = try context.environment.loadTemplate(name: templateName)
|
||||||
let template = context.environment.template
|
|
||||||
|
|
||||||
let blockContext: BlockContext
|
let blockContext: BlockContext
|
||||||
if let _blockContext = context[BlockContext.contextKey] as? BlockContext {
|
if let _blockContext = context[BlockContext.contextKey] as? BlockContext {
|
||||||
blockContext = _blockContext
|
blockContext = _blockContext
|
||||||
for (name, block) in blocks {
|
for (name, block) in blocks {
|
||||||
blockContext.pushBlock(block, named: name, definedIn: template)
|
blockContext.pushBlock(block, named: name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var blocks = [String: [(BlockNode, Template?)]]()
|
var blocks = [String: [BlockNode]]()
|
||||||
self.blocks.forEach { blocks[$0.key] = [($0.value, template)] }
|
self.blocks.forEach { blocks[$0.key] = [$0.value] }
|
||||||
blockContext = BlockContext(blocks: blocks)
|
blockContext = BlockContext(blocks: blocks)
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
// pushes base template and renders it's content
|
// pushes base template and renders it's content
|
||||||
// block_context contains all blocks from child templates
|
// block_context contains all blocks from child templates
|
||||||
return try context.environment.pushTemplate(baseTemplate, token: token) {
|
return try context.push(dictionary: [BlockContext.contextKey: blockContext]) {
|
||||||
try context.push(dictionary: [BlockContext.contextKey: blockContext]) {
|
|
||||||
return try baseTemplate.render(context)
|
return try baseTemplate.render(context)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// if error template is already set (see catch in BlockNode)
|
// if error template is already set (see catch in BlockNode)
|
||||||
// and it happend in the same template as current template
|
// and it happend in the same template as current template
|
||||||
// there is no need to wrap it in another error
|
// there is no need to wrap it in another error
|
||||||
if let error = error as? TemplateSyntaxError, error.template !== context.environment.template {
|
if let error = error as? TemplateSyntaxError, error.templateName != token?.sourceMap.filename {
|
||||||
throw TemplateSyntaxError(reason: error.reason, parentError: error)
|
throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens)
|
||||||
} else {
|
} else {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@@ -152,7 +149,7 @@ class BlockNode : NodeType {
|
|||||||
|
|
||||||
var newContext: [String: Any] = [BlockContext.contextKey: blockContext]
|
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 }
|
if case .variable(let variable, _)? = $0.token, variable == "block.super" { return true }
|
||||||
else { return false}
|
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
|
// 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)]
|
newContext["block"] = ["super": try self.render(context)]
|
||||||
} catch {
|
} catch {
|
||||||
let baseError = context.errorReporter.reportError(error)
|
if let error = error as? TemplateSyntaxError {
|
||||||
throw TemplateSyntaxError(
|
throw TemplateSyntaxError(
|
||||||
reason: (baseError as? TemplateSyntaxError)?.reason ?? "\(baseError)",
|
reason: error.reason,
|
||||||
token: blockSuperNode.token,
|
token: blockSuperNode.token,
|
||||||
template: child.template,
|
stackTrace: error.allTokens)
|
||||||
parentError: baseError)
|
} else {
|
||||||
|
throw TemplateSyntaxError(
|
||||||
|
reason: "\(error)",
|
||||||
|
token: blockSuperNode.token,
|
||||||
|
stackTrace: [])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// render extension node
|
// render extension node
|
||||||
do {
|
do {
|
||||||
return try context.push(dictionary: newContext) {
|
return try context.push(dictionary: newContext) {
|
||||||
return try child.node.render(context)
|
return try child.render(context)
|
||||||
}
|
}
|
||||||
} catch {
|
} 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 {
|
if var error = error as? TemplateSyntaxError {
|
||||||
error.template = error.template ?? child.template
|
error.token = error.token ?? child.token
|
||||||
error.token = error.token ?? child.node.token
|
|
||||||
throw error
|
throw error
|
||||||
} else {
|
} else {
|
||||||
throw error
|
throw error
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import Foundation
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,14 +18,28 @@ struct Lexer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if string.hasPrefix("{{") {
|
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("{%") {
|
} 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("{#") {
|
} 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.
|
/// Returns an array of tokens from a given template string.
|
||||||
@@ -41,6 +57,7 @@ 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 {
|
||||||
|
let line = templateString.rangeLine(scanner.range)
|
||||||
tokens.append(createToken(string: text.1, at: 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)
|
let result = scanner.scan(until: end, returnUntil: true)
|
||||||
tokens.append(createToken(string: result, at: scanner.range))
|
tokens.append(createToken(string: result, at: scanner.range))
|
||||||
} else {
|
} else {
|
||||||
|
let line = templateString.rangeLine(scanner.range)
|
||||||
tokens.append(createToken(string: scanner.content, at: scanner.range))
|
tokens.append(createToken(string: scanner.content, at: scanner.range))
|
||||||
scanner.content = ""
|
scanner.content = ""
|
||||||
}
|
}
|
||||||
@@ -165,7 +183,7 @@ extension String {
|
|||||||
return String(self[first..<last])
|
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 lineNumber: UInt = 0
|
||||||
var offset: Int = 0
|
var offset: Int = 0
|
||||||
var lineContent = ""
|
var lineContent = ""
|
||||||
@@ -183,3 +201,5 @@ extension String {
|
|||||||
return (lineContent, lineNumber, offset)
|
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)
|
return try FilterExpression(token: filterToken, parser: self)
|
||||||
} catch {
|
} catch {
|
||||||
if var error = error as? TemplateSyntaxError, error.token == nil {
|
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
|
// find offset 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) {
|
if let filterTokenRange = containingToken.contents.range(of: filterToken) {
|
||||||
error.token = Token.variable(value: filterToken, at: filterTokenRange)
|
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 {
|
} else {
|
||||||
error.token = containingToken
|
error.token = containingToken
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ open class Template: ExpressibleByStringLiteral {
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.templateString = templateString
|
self.templateString = templateString
|
||||||
|
|
||||||
let lexer = Lexer(templateString: templateString)
|
let lexer = Lexer(templateName: name, templateString: templateString)
|
||||||
tokens = lexer.tokenize()
|
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 {
|
public enum Token : Equatable {
|
||||||
/// A token representing a piece of text.
|
/// 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.
|
/// A token representing a variable.
|
||||||
case variable(value: String, at: Range<String.Index>)
|
case variable(value: String, at: SourceMap)
|
||||||
|
|
||||||
/// A token representing a comment.
|
/// 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.
|
/// 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
|
/// Returns the underlying value as an array seperated by spaces
|
||||||
public func components() -> [String] {
|
public func components() -> [String] {
|
||||||
@@ -74,13 +90,13 @@ public enum Token : Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var range: Range<String.Index> {
|
public var sourceMap: SourceMap {
|
||||||
switch self {
|
switch self {
|
||||||
case .block(_, let range),
|
case .block(_, let sourceMap),
|
||||||
.variable(_, let range),
|
.variable(_, let sourceMap),
|
||||||
.text(_, let range),
|
.text(_, let sourceMap),
|
||||||
.comment(_, let range):
|
.comment(_, let sourceMap):
|
||||||
return range
|
return sourceMap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,14 +105,14 @@ public enum Token : Equatable {
|
|||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,15 +41,17 @@ func testEnvironment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError {
|
func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError {
|
||||||
let token = Token.block(value: token, at: template.templateString.range(of: token)!)
|
let range = template.templateString.range(of: token)!
|
||||||
return TemplateSyntaxError(reason: description, token: token, template: template, parentError: nil)
|
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 {
|
func expectError(reason: String, token: String) throws {
|
||||||
let expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
let expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
||||||
|
|
||||||
let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"]))
|
let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"])).toThrow() as TemplateSyntaxError
|
||||||
.toThrow(expectedError)
|
|
||||||
try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError)
|
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 {
|
func expectError(reason: String, token: String, includedToken: String) throws {
|
||||||
var expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
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"]))
|
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)
|
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 {
|
func expectError(reason: String, childToken: String, baseToken: String?) throws {
|
||||||
var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason)
|
var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason)
|
||||||
if let baseToken = baseToken {
|
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"]))
|
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)
|
try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +313,7 @@ func testEnvironment() {
|
|||||||
|
|
||||||
private extension Expectation {
|
private extension Expectation {
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func toThrow<T: Error & Equatable>(_ error: T) throws -> T {
|
func toThrow<T: Error>() throws -> T {
|
||||||
var thrownError: Error? = nil
|
var thrownError: Error? = nil
|
||||||
|
|
||||||
do {
|
do {
|
||||||
@@ -323,12 +324,9 @@ private extension Expectation {
|
|||||||
|
|
||||||
if let thrownError = thrownError {
|
if let thrownError = thrownError {
|
||||||
if let thrownError = thrownError as? T {
|
if let thrownError = thrownError as? T {
|
||||||
if error != thrownError {
|
|
||||||
throw failure("\(thrownError) is not \(error)")
|
|
||||||
}
|
|
||||||
return thrownError
|
return thrownError
|
||||||
} else {
|
} else {
|
||||||
throw failure("\(thrownError) is not \(error)")
|
throw failure("\(thrownError) is not \(T.self)")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw failure("expression did not throw an error")
|
throw failure("expression did not throw an error")
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
@@ -169,11 +169,9 @@ func testForNode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("handles invalid input") {
|
$0.it("handles invalid input") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [.block(value: "for i", at: .unknown)]
|
||||||
.block(value: "for i", at: .unknown),
|
|
||||||
]
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
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)
|
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") {
|
$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", at: .unknown),
|
|
||||||
]
|
|
||||||
|
|
||||||
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", at: .unknown),
|
|
||||||
]
|
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func testInclude() {
|
|||||||
let tokens: [Token] = [ .block(value: "include", at: .unknown) ]
|
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 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)
|
try expect(try parser.parse()).toThrow(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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", 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") {
|
$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", at: "{# Comment #}".range)
|
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,7 +25,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) == .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") {
|
$0.it("can tokenize unclosed tag by ignoring it") {
|
||||||
@@ -34,18 +34,18 @@ 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: "", at: "".range)
|
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 templateString = "My name is {{ name }}."
|
let templateString = "My name is {{ myname }}."
|
||||||
let lexer = Lexer(templateString: templateString)
|
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 ", at: templateString.range(of: "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", at: templateString.range(of: "{{ 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: ".", at: templateString.range(of: ".")!)
|
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") {
|
||||||
@@ -54,8 +54,8 @@ func testLexer() {
|
|||||||
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", at: templateString.range(of: "{{ 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", at: templateString.range(of: "{{ 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") {
|
||||||
|
|||||||
@@ -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", at: .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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user