From 6300dbc7bf809908970d5e3e02d6b4fa3622f208 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Tue, 3 Oct 2017 22:47:28 +0200 Subject: [PATCH] improved template syntax errors with file, line number and failed token highlighted in error message --- Sources/ForTag.swift | 2 +- Sources/IfTag.swift | 2 +- Sources/Lexer.swift | 56 ++++++++++++---- Sources/Node.swift | 12 ++++ Sources/NowTag.swift | 2 +- Sources/Parser.swift | 16 ++++- Sources/Template.swift | 15 ++++- Sources/Tokenizer.swift | 52 +++++++++------ Tests/StencilTests/FilterTagSpec.swift | 2 +- Tests/StencilTests/ForNodeSpec.swift | 81 +---------------------- Tests/StencilTests/IfNodeSpec.swift | 90 +++++++++++++------------- Tests/StencilTests/IncludeSpec.swift | 4 +- Tests/StencilTests/LexerSpec.swift | 27 ++++---- Tests/StencilTests/NowNodeSpec.swift | 4 +- Tests/StencilTests/ParserSpec.swift | 10 +-- Tests/StencilTests/TemplateSpec.swift | 27 +++++++- Tests/StencilTests/TokenSpec.swift | 8 +-- 17 files changed, 220 insertions(+), 190 deletions(-) diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index 25005e6..032653f 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -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 diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index 8f3b0fd..e4305a9 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -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]() diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 5bd590d..58ce89b 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -5,7 +5,7 @@ struct Lexer { self.templateString = templateString } - func createToken(string: String) -> Token { + func createToken(string: String, at range: Range) -> 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 + init(_ content: String) { + self._content = content self.content = content + range = content.startIndex.. String { + var index = content.startIndex + if until.isEmpty { return "" } - var index = content.startIndex + range = range.upperBound..) -> (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) + } + } diff --git a/Sources/Node.swift b/Sources/Node.swift index 5b47177..1c2eed4 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -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) + } + } diff --git a/Sources/NowTag.swift b/Sources/NowTag.swift index cd6e4ea..17a62a6 100644 --- a/Sources/NowTag.swift +++ b/Sources/NowTag.swift @@ -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]) diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 1a59edb..5c840bf 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -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 diff --git a/Sources/Template.swift b/Sources/Template.swift index d9b1893..7a80ee8 100644 --- a/Sources/Template.swift +++ b/Sources/Template.swift @@ -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 diff --git a/Sources/Tokenizer.swift b/Sources/Tokenizer.swift index 0e4bf1e..cae09cc 100644 --- a/Sources/Tokenizer.swift +++ b/Sources/Tokenizer.swift @@ -40,46 +40,62 @@ extension String { } } +extension Range where Bound == String.Index { + internal static var unknown: Range { + return "".range + } +} + +extension String { + var range: Range { + return startIndex..) /// A token representing a variable. - case variable(value: String) + case variable(value: String, at: Range) /// A token representing a comment. - case comment(value: String) + case comment(value: String, at: Range) /// A token representing a template block. - case block(value: String) + case block(value: String, at: Range) /// 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 { + switch self { + case .block(_, let range), + .variable(_, let range), + .text(_, let range), + .comment(_, let range): + return range + } + } + } diff --git a/Tests/StencilTests/FilterTagSpec.swift b/Tests/StencilTests/FilterTagSpec.swift index 2470450..a6e4513 100644 --- a/Tests/StencilTests/FilterTagSpec.swift +++ b/Tests/StencilTests/FilterTagSpec.swift @@ -17,7 +17,7 @@ func testFilterTag() { } $0.it("errors without a filter") { - let template = Template(templateString: "{% filter %}Test{% endfilter %}") + let template = Template(templateString: "Some {% filter %}Test{% endfilter %}") try expect(try template.render()).toThrow() } diff --git a/Tests/StencilTests/ForNodeSpec.swift b/Tests/StencilTests/ForNodeSpec.swift index 9cc98cb..6cf69b3 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -170,88 +170,13 @@ func testForNode() { $0.it("handles invalid input") { let tokens: [Token] = [ - .block(value: "for i"), + .block(value: "for i", at: .unknown), ] let parser = TokenParser(tokens: tokens, environment: Environment()) - let error = TemplateSyntaxError("'for' statements should use the following 'for x in y where condition' `for i`.") + let error = TemplateSyntaxError("'for' statements should use the following syntax 'for x in y where condition'.") try expect(try parser.parse()).toThrow(error) } - - $0.it("can iterate over struct properties") { - struct MyStruct { - let string: String - let number: Int - } - - let context = Context(dictionary: [ - "struct": MyStruct(string: "abc", number: 123) - ]) - - let nodes: [NodeType] = [ - VariableNode(variable: "property"), - TextNode(text: "="), - VariableNode(variable: "value"), - TextNode(text: "\n"), - ] - let node = ForNode(resolvable: Variable("struct"), loopVariables: ["property", "value"], nodes: nodes, emptyNodes: []) - let result = try node.render(context) - - try expect(result) == "string=abc\nnumber=123\n" - } - - $0.it("can iterate tuple items") { - let context = Context(dictionary: [ - "tuple": (one: 1, two: "dva"), - ]) - - let nodes: [NodeType] = [ - VariableNode(variable: "label"), - TextNode(text: "="), - VariableNode(variable: "value"), - TextNode(text: "\n"), - ] - - let node = ForNode(resolvable: Variable("tuple"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: []) - let result = try node.render(context) - - try expect(result) == "one=1\ntwo=dva\n" - } - - $0.it("can iterate over class properties") { - class MyClass { - var baseString: String - var baseInt: Int - init(_ string: String, _ int: Int) { - baseString = string - baseInt = int - } - } - - class MySubclass: MyClass { - var childString: String - init(_ childString: String, _ string: String, _ int: Int) { - self.childString = childString - super.init(string, int) - } - } - - let context = Context(dictionary: [ - "class": MySubclass("child", "base", 1) - ]) - - let nodes: [NodeType] = [ - VariableNode(variable: "label"), - TextNode(text: "="), - VariableNode(variable: "value"), - TextNode(text: "\n"), - ] - - let node = ForNode(resolvable: Variable("class"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: []) - let result = try node.render(context) - - try expect(result) == "childString=child\nbaseString=base\nbaseInt=1\n" - } - + } } diff --git a/Tests/StencilTests/IfNodeSpec.swift b/Tests/StencilTests/IfNodeSpec.swift index c77122f..25119eb 100644 --- a/Tests/StencilTests/IfNodeSpec.swift +++ b/Tests/StencilTests/IfNodeSpec.swift @@ -7,9 +7,9 @@ func testIfNode() { $0.describe("parsing") { $0.it("can parse an if block") { let tokens: [Token] = [ - .block(value: "if value"), - .text(value: "true"), - .block(value: "endif") + .block(value: "if value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "endif", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -25,11 +25,11 @@ func testIfNode() { $0.it("can parse an if with else block") { let tokens: [Token] = [ - .block(value: "if value"), - .text(value: "true"), - .block(value: "else"), - .text(value: "false"), - .block(value: "endif") + .block(value: "if value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "else", at: .unknown), + .text(value: "false", at: .unknown), + .block(value: "endif", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -50,13 +50,13 @@ func testIfNode() { $0.it("can parse an if with elif block") { let tokens: [Token] = [ - .block(value: "if value"), - .text(value: "true"), - .block(value: "elif something"), - .text(value: "some"), - .block(value: "else"), - .text(value: "false"), - .block(value: "endif") + .block(value: "if value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "elif something", at: .unknown), + .text(value: "some", at: .unknown), + .block(value: "else", at: .unknown), + .text(value: "false", at: .unknown), + .block(value: "endif", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -81,11 +81,11 @@ func testIfNode() { $0.it("can parse an if with elif block without else") { let tokens: [Token] = [ - .block(value: "if value"), - .text(value: "true"), - .block(value: "elif something"), - .text(value: "some"), - .block(value: "endif") + .block(value: "if value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "elif something", at: .unknown), + .text(value: "some", at: .unknown), + .block(value: "endif", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -106,15 +106,15 @@ func testIfNode() { $0.it("can parse an if with multiple elif block") { let tokens: [Token] = [ - .block(value: "if value"), - .text(value: "true"), - .block(value: "elif something1"), - .text(value: "some1"), - .block(value: "elif something2"), - .text(value: "some2"), - .block(value: "else"), - .text(value: "false"), - .block(value: "endif") + .block(value: "if value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "elif something1", at: .unknown), + .text(value: "some1", at: .unknown), + .block(value: "elif something2", at: .unknown), + .text(value: "some2", at: .unknown), + .block(value: "else", at: .unknown), + .text(value: "false", at: .unknown), + .block(value: "endif", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -144,9 +144,9 @@ func testIfNode() { $0.it("can parse an if with complex expression") { let tokens: [Token] = [ - .block(value: "if value == \"test\" and not name"), - .text(value: "true"), - .block(value: "endif") + .block(value: "if value == \"test\" and not name", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "endif", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -156,11 +156,11 @@ func testIfNode() { $0.it("can parse an ifnot block") { let tokens: [Token] = [ - .block(value: "ifnot value"), - .text(value: "false"), - .block(value: "else"), - .text(value: "true"), - .block(value: "endif") + .block(value: "ifnot value", at: .unknown), + .text(value: "false", at: .unknown), + .block(value: "else", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "endif", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -180,7 +180,7 @@ func testIfNode() { $0.it("throws an error when parsing an if block without an endif") { let tokens: [Token] = [ - .block(value: "if value"), + .block(value: "if value", at: .unknown), ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -190,7 +190,7 @@ func testIfNode() { $0.it("throws an error when parsing an ifnot without an endif") { let tokens: [Token] = [ - .block(value: "ifnot value"), + .block(value: "ifnot value", at: .unknown), ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -242,9 +242,9 @@ func testIfNode() { $0.it("supports variable filters in the if expression") { let tokens: [Token] = [ - .block(value: "if value|uppercase == \"TEST\""), - .text(value: "true"), - .block(value: "endif") + .block(value: "if value|uppercase == \"TEST\"", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "endif", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) @@ -256,9 +256,9 @@ func testIfNode() { $0.it("evaluates nil properties as false") { let tokens: [Token] = [ - .block(value: "if instance.value"), - .text(value: "true"), - .block(value: "endif") + .block(value: "if instance.value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "endif", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) diff --git a/Tests/StencilTests/IncludeSpec.swift b/Tests/StencilTests/IncludeSpec.swift index 0297896..9f789b7 100644 --- a/Tests/StencilTests/IncludeSpec.swift +++ b/Tests/StencilTests/IncludeSpec.swift @@ -11,7 +11,7 @@ func testInclude() { $0.describe("parsing") { $0.it("throws an error when no template is given") { - let tokens: [Token] = [ .block(value: "include") ] + let tokens: [Token] = [ .block(value: "include", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) let error = TemplateSyntaxError("'include' tag takes one argument, the template file to be included") @@ -19,7 +19,7 @@ func testInclude() { } $0.it("can parse a valid include block") { - let tokens: [Token] = [ .block(value: "include \"test.html\"") ] + let tokens: [Token] = [ .block(value: "include \"test.html\"", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) let nodes = try parser.parse() diff --git a/Tests/StencilTests/LexerSpec.swift b/Tests/StencilTests/LexerSpec.swift index ac9eb50..b74df68 100644 --- a/Tests/StencilTests/LexerSpec.swift +++ b/Tests/StencilTests/LexerSpec.swift @@ -9,7 +9,7 @@ func testLexer() { let tokens = lexer.tokenize() try expect(tokens.count) == 1 - try expect(tokens.first) == .text(value: "Hello World") + try expect(tokens.first) == .text(value: "Hello World", at: "Hello World".range) } $0.it("can tokenize a comment") { @@ -17,7 +17,7 @@ func testLexer() { let tokens = lexer.tokenize() try expect(tokens.count) == 1 - try expect(tokens.first) == .comment(value: "Comment") + try expect(tokens.first) == .comment(value: "Comment", at: "{# Comment #}".range) } $0.it("can tokenize a variable") { @@ -25,34 +25,37 @@ func testLexer() { let tokens = lexer.tokenize() try expect(tokens.count) == 1 - try expect(tokens.first) == .variable(value: "Variable") + try expect(tokens.first) == .variable(value: "Variable", at: "{{ Variable }}".range) } $0.it("can tokenize unclosed tag by ignoring it") { - let lexer = Lexer(templateString: "{{ thing") + let templateString = "{{ thing" + let lexer = Lexer(templateString: templateString) let tokens = lexer.tokenize() try expect(tokens.count) == 1 - try expect(tokens.first) == .text(value: "") + try expect(tokens.first) == .text(value: "", at: "".range) } $0.it("can tokenize a mixture of content") { - let lexer = Lexer(templateString: "My name is {{ name }}.") + let templateString = "My name is {{ name }}." + let lexer = Lexer(templateString: templateString) let tokens = lexer.tokenize() try expect(tokens.count) == 3 - try expect(tokens[0]) == Token.text(value: "My name is ") - try expect(tokens[1]) == Token.variable(value: "name") - try expect(tokens[2]) == Token.text(value: ".") + try expect(tokens[0]) == Token.text(value: "My name is ", at: templateString.range(of: "My name is ")!) + try expect(tokens[1]) == Token.variable(value: "name", at: templateString.range(of: "{{ name }}")!) + try expect(tokens[2]) == Token.text(value: ".", at: templateString.range(of: ".")!) } $0.it("can tokenize two variables without being greedy") { - let lexer = Lexer(templateString: "{{ thing }}{{ name }}") + let templateString = "{{ thing }}{{ name }}" + let lexer = Lexer(templateString: templateString) let tokens = lexer.tokenize() try expect(tokens.count) == 2 - try expect(tokens[0]) == Token.variable(value: "thing") - try expect(tokens[1]) == Token.variable(value: "name") + try expect(tokens[0]) == Token.variable(value: "thing", at: templateString.range(of: "{{ thing }}")!) + try expect(tokens[1]) == Token.variable(value: "name", at: templateString.range(of: "{{ name }}")!) } $0.it("can tokenize an unclosed block") { diff --git a/Tests/StencilTests/NowNodeSpec.swift b/Tests/StencilTests/NowNodeSpec.swift index 4adba36..33a0d3f 100644 --- a/Tests/StencilTests/NowNodeSpec.swift +++ b/Tests/StencilTests/NowNodeSpec.swift @@ -8,7 +8,7 @@ func testNowNode() { describe("NowNode") { $0.describe("parsing") { $0.it("parses default format without any now arguments") { - let tokens: [Token] = [ .block(value: "now") ] + let tokens: [Token] = [ .block(value: "now", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) let nodes = try parser.parse() @@ -18,7 +18,7 @@ func testNowNode() { } $0.it("parses now with a format") { - let tokens: [Token] = [ .block(value: "now \"HH:mm\"") ] + let tokens: [Token] = [ .block(value: "now \"HH:mm\"", at: .unknown) ] let parser = TokenParser(tokens: tokens, environment: Environment()) let nodes = try parser.parse() let node = nodes.first as? NowNode diff --git a/Tests/StencilTests/ParserSpec.swift b/Tests/StencilTests/ParserSpec.swift index b5c9bb2..facd07a 100644 --- a/Tests/StencilTests/ParserSpec.swift +++ b/Tests/StencilTests/ParserSpec.swift @@ -6,7 +6,7 @@ func testTokenParser() { describe("TokenParser") { $0.it("can parse a text token") { let parser = TokenParser(tokens: [ - .text(value: "Hello World") + .text(value: "Hello World", at: .unknown) ], environment: Environment()) let nodes = try parser.parse() @@ -18,7 +18,7 @@ func testTokenParser() { $0.it("can parse a variable token") { let parser = TokenParser(tokens: [ - .variable(value: "'name'") + .variable(value: "'name'", at: .unknown) ], environment: Environment()) let nodes = try parser.parse() @@ -30,7 +30,7 @@ func testTokenParser() { $0.it("can parse a comment token") { let parser = TokenParser(tokens: [ - .comment(value: "Secret stuff!") + .comment(value: "Secret stuff!", at: .unknown) ], environment: Environment()) let nodes = try parser.parse() @@ -44,7 +44,7 @@ func testTokenParser() { } let parser = TokenParser(tokens: [ - .block(value: "known"), + .block(value: "known", at: .unknown), ], environment: Environment(extensions: [simpleExtension])) let nodes = try parser.parse() @@ -53,7 +53,7 @@ func testTokenParser() { $0.it("errors when parsing an unknown tag") { let parser = TokenParser(tokens: [ - .block(value: "unknown"), + .block(value: "unknown", at: .unknown), ], environment: Environment()) try expect(try parser.parse()).toThrow(TemplateSyntaxError("Unknown template tag 'unknown'")) diff --git a/Tests/StencilTests/TemplateSpec.swift b/Tests/StencilTests/TemplateSpec.swift index ad03851..42dac28 100644 --- a/Tests/StencilTests/TemplateSpec.swift +++ b/Tests/StencilTests/TemplateSpec.swift @@ -1,5 +1,5 @@ import Spectre -import Stencil +@testable import Stencil func testTemplate() { @@ -15,5 +15,30 @@ func testTemplate() { let result = try template.render([ "name": "Kyle" ]) try expect(result) == "Hello World" } + + $0.it("throws syntax error on invalid for tag syntax") { + let template: Template = "Hello {% for name in %}{{ name }}, {% endfor %}!" + var error = TemplateSyntaxError("'for' statements should use the following syntax 'for x in y where condition'.") + error.token = Token.block(value: "{% for name in %}", at: template.templateString.range(of: "{% for name in %}")!) + error = error.contextAwareError(templateName: nil, templateContent: template.templateString)! + try expect(try template.render(["names": ["Bob", "Alice"]])).toThrow(error) + } + + $0.it("throws syntax error on missing endfor") { + let template: Template = "{% for name in names %}{{ name }}" + var error = TemplateSyntaxError("`endfor` was not found.") + error.token = Token.block(value: "{% for name in names %}", at: template.templateString.range(of: "{% for name in names %}")!) + error = error.contextAwareError(templateName: nil, templateContent: template.templateString)! + try expect(try template.render(["names": ["Bob", "Alice"]])).toThrow(error) + } + + $0.it("throws syntax error on unknown tag") { + let template: Template = "{% for name in names %}{{ name }}{% end %}" + var error = TemplateSyntaxError("Unknown template tag 'end'") + error.token = Token.block(value: "{% end %}", at: template.templateString.range(of: "{% end %}")!) + error = error.contextAwareError(templateName: nil, templateContent: template.templateString)! + try expect(try template.render(["names": ["Bob", "Alice"]])).toThrow(error) + } + } } diff --git a/Tests/StencilTests/TokenSpec.swift b/Tests/StencilTests/TokenSpec.swift index c7f9db1..dd73298 100644 --- a/Tests/StencilTests/TokenSpec.swift +++ b/Tests/StencilTests/TokenSpec.swift @@ -1,11 +1,11 @@ import Spectre -import Stencil +@testable import Stencil func testToken() { describe("Token") { $0.it("can split the contents into components") { - let token = Token.text(value: "hello world") + let token = Token.text(value: "hello world", at: .unknown) let components = token.components() try expect(components.count) == 2 @@ -14,7 +14,7 @@ func testToken() { } $0.it("can split the contents into components with single quoted strings") { - let token = Token.text(value: "hello 'kyle fuller'") + let token = Token.text(value: "hello 'kyle fuller'", at: .unknown) let components = token.components() try expect(components.count) == 2 @@ -23,7 +23,7 @@ func testToken() { } $0.it("can split the contents into components with double quoted strings") { - let token = Token.text(value: "hello \"kyle fuller\"") + let token = Token.text(value: "hello \"kyle fuller\"", at: .unknown) let components = token.components() try expect(components.count) == 2