improved template syntax errors with file, line number and failed token highlighted in error message
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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]()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user