storing full sourcemap in token, refactored error reporting

This commit is contained in:
Ilya Puchka
2017-12-27 02:31:47 +01:00
parent cb124319ec
commit ac2fd56e8e
15 changed files with 149 additions and 188 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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