improved template syntax errors with file, line number and failed token highlighted in error message

This commit is contained in:
Ilya Puchka
2017-10-03 22:47:28 +02:00
parent 2e80f70f67
commit 6300dbc7bf
17 changed files with 220 additions and 190 deletions

View File

@@ -12,7 +12,7 @@ class ForNode : NodeType {
guard components.count >= 3 && components[2] == "in" &&
(components.count == 4 || (components.count >= 6 && components[4] == "where")) else {
throw TemplateSyntaxError("'for' statements should use the following 'for x in y where condition' `\(token.contents)`.")
throw TemplateSyntaxError("'for' statements should use the following syntax 'for x in y where condition'.")
}
let loopVariables = components[1].characters

View File

@@ -219,7 +219,7 @@ class IfNode : NodeType {
class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType {
var components = token.components()
guard components.count == 2 else {
throw TemplateSyntaxError("'ifnot' statements should use the following 'ifnot condition' `\(token.contents)`.")
throw TemplateSyntaxError("'ifnot' statements should use the following syntax 'ifnot condition'.")
}
components.removeFirst()
var trueNodes = [NodeType]()

View File

@@ -5,7 +5,7 @@ struct Lexer {
self.templateString = templateString
}
func createToken(string: String) -> Token {
func createToken(string: String, at range: Range<String.Index>) -> Token {
func strip() -> String {
guard string.characters.count > 4 else { return "" }
let start = string.index(string.startIndex, offsetBy: 2)
@@ -14,14 +14,14 @@ struct Lexer {
}
if string.hasPrefix("{{") {
return .variable(value: strip())
return .variable(value: strip(), at: range)
} else if string.hasPrefix("{%") {
return .block(value: strip())
return .block(value: strip(), at: range)
} else if string.hasPrefix("{#") {
return .comment(value: strip())
return .comment(value: strip(), at: range)
}
return .text(value: string)
return .text(value: string, at: range)
}
/// Returns an array of tokens from a given template string.
@@ -39,14 +39,14 @@ struct Lexer {
while !scanner.isEmpty {
if let text = scanner.scan(until: ["{{", "{%", "{#"]) {
if !text.1.isEmpty {
tokens.append(createToken(string: text.1))
tokens.append(createToken(string: text.1, at: scanner.range))
}
let end = map[text.0]!
let result = scanner.scan(until: end, returnUntil: true)
tokens.append(createToken(string: result))
tokens.append(createToken(string: result, at: scanner.range))
} else {
tokens.append(createToken(string: scanner.content))
tokens.append(createToken(string: scanner.content, at: scanner.range))
scanner.content = ""
}
}
@@ -57,41 +57,50 @@ struct Lexer {
class Scanner {
let _content: String //stores original content
var content: String
var range: Range<String.Index>
init(_ content: String) {
self._content = content
self.content = content
range = content.startIndex..<content.startIndex
}
var isEmpty: Bool {
return content.isEmpty
}
func scan(until: String, returnUntil: Bool = false) -> String {
var index = content.startIndex
if until.isEmpty {
return ""
}
var index = content.startIndex
range = range.upperBound..<range.upperBound
while index != content.endIndex {
let substring = content.substring(from: index)
if substring.hasPrefix(until) {
let result = content.substring(to: index)
content = substring
if returnUntil {
content = content.substring(from: until.endIndex)
range = range.lowerBound..<_content.index(range.upperBound, offsetBy: until.count)
content = substring.substring(from: until.endIndex)
return result + until
}
content = substring
return result
}
index = content.index(after: index)
range = range.lowerBound..<_content.index(after: range.upperBound)
}
content = ""
range = "".range
return ""
}
@@ -101,6 +110,7 @@ class Scanner {
}
var index = content.startIndex
range = range.upperBound..<range.upperBound
while index != content.endIndex {
let substring = content.substring(from: index)
for string in until {
@@ -112,6 +122,7 @@ class Scanner {
}
index = content.index(after: index)
range = range.lowerBound..<_content.index(after: range.upperBound)
}
return nil
@@ -151,4 +162,21 @@ extension String {
let last = findLastNot(character: character) ?? endIndex
return String(self[first..<last])
}
func lineAndPosition(at range: Range<String.Index>) -> (content: String, number: Int, offset: String.IndexDistance) {
var lineNumber: Int = 0
var offset = 0
var lineContent = ""
enumerateLines { (line, stop) in
lineNumber += 1
lineContent = line
if let rangeOfLine = self.range(of: line), rangeOfLine.contains(range.lowerBound) {
offset = self.distance(from: rangeOfLine.lowerBound, to: range.lowerBound)
stop = true
}
}
return (lineContent, lineNumber, offset)
}
}

View File

@@ -3,10 +3,22 @@ import Foundation
public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
public let description:String
public var token: Token?
public init(_ description:String) {
self.description = description
}
public func contextAwareError(templateName: String?, templateContent: String) -> TemplateSyntaxError? {
guard let token = token, token.range != .unknown else { return nil }
let templateName = templateName.map({ "\($0):" }) ?? ""
let (line, lineNumber, offset) = templateContent.lineAndPosition(at: token.range)
let tokenContent = templateContent.substring(with: token.range)
let highlight = "\(String(Array(repeating: " ", count: offset)))^\(String(Array(repeating: "~", count: max(tokenContent.count - 1, 0))))"
let description = "\(templateName)\(lineNumber):\(offset): error: " + self.description + "\n\(line)\n\(highlight)\n"
return TemplateSyntaxError(description)
}
}

View File

@@ -10,7 +10,7 @@ class NowNode : NodeType {
let components = token.components()
guard components.count <= 2 else {
throw TemplateSyntaxError("'now' tags may only have one argument: the format string `\(token.contents)`.")
throw TemplateSyntaxError("'now' tags may only have one argument: the format string.")
}
if components.count == 2 {
format = Variable(components[1])

View File

@@ -37,7 +37,7 @@ public class TokenParser {
let token = nextToken()!
switch token {
case .text(let text):
case .text(let text, _):
nodes.append(TextNode(text: text))
case .variable:
nodes.append(VariableNode(variable: try compileFilter(token.contents)))
@@ -48,8 +48,18 @@ public class TokenParser {
}
if let tag = token.components().first {
let parser = try findTag(name: tag)
nodes.append(try parser(self, token))
do {
let parser = try findTag(name: tag)
let node = try parser(self, token)
nodes.append(node)
} catch {
if var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil {
syntaxError.token = token
throw syntaxError
} else {
throw error
}
}
}
case .comment:
continue

View File

@@ -7,6 +7,7 @@ let NSFileNoSuchFileError = 4
/// A class representing a template
open class Template: ExpressibleByStringLiteral {
let templateString: String
let environment: Environment
let tokens: [Token]
@@ -17,6 +18,7 @@ open class Template: ExpressibleByStringLiteral {
public required init(templateString: String, environment: Environment? = nil, name: String? = nil) {
self.environment = environment ?? Environment()
self.name = name
self.templateString = templateString
let lexer = Lexer(templateString: templateString)
tokens = lexer.tokenize()
@@ -66,8 +68,17 @@ open class Template: ExpressibleByStringLiteral {
func render(_ context: Context) throws -> String {
let context = context
let parser = TokenParser(tokens: tokens, environment: context.environment)
let nodes = try parser.parse()
return try renderNodes(nodes, context)
do {
let nodes = try parser.parse()
return try renderNodes(nodes, context)
} catch {
if let syntaxError = error as? TemplateSyntaxError,
let error = syntaxError.contextAwareError(templateName: name, templateContent: templateString) {
throw error
} else {
throw error
}
}
}
/// Render the given template

View File

@@ -40,46 +40,62 @@ extension String {
}
}
extension Range where Bound == String.Index {
internal static var unknown: Range {
return "".range
}
}
extension String {
var range: Range<String.Index> {
return startIndex..<endIndex
}
}
public enum Token : Equatable {
/// A token representing a piece of text.
case text(value: String)
case text(value: String, at: Range<String.Index>)
/// A token representing a variable.
case variable(value: String)
case variable(value: String, at: Range<String.Index>)
/// A token representing a comment.
case comment(value: String)
case comment(value: String, at: Range<String.Index>)
/// A token representing a template block.
case block(value: String)
case block(value: String, at: Range<String.Index>)
/// Returns the underlying value as an array seperated by spaces
public func components() -> [String] {
switch self {
case .block(let value):
return value.smartSplit()
case .variable(let value):
return value.smartSplit()
case .text(let value):
return value.smartSplit()
case .comment(let value):
case .block(let value, _),
.variable(let value, _),
.text(let value, _),
.comment(let value, _):
return value.smartSplit()
}
}
public var contents: String {
switch self {
case .block(let value):
return value
case .variable(let value):
return value
case .text(let value):
return value
case .comment(let value):
case .block(let value, _),
.variable(let value, _),
.text(let value, _),
.comment(let value, _):
return value
}
}
public var range: Range<String.Index> {
switch self {
case .block(_, let range),
.variable(_, let range),
.text(_, let range),
.comment(_, let range):
return range
}
}
}