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" &&
|
guard components.count >= 3 && components[2] == "in" &&
|
||||||
(components.count == 4 || (components.count >= 6 && components[4] == "where")) else {
|
(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
|
let loopVariables = components[1].characters
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ class IfNode : NodeType {
|
|||||||
class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType {
|
class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||||
var components = token.components()
|
var components = token.components()
|
||||||
guard components.count == 2 else {
|
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()
|
components.removeFirst()
|
||||||
var trueNodes = [NodeType]()
|
var trueNodes = [NodeType]()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ struct Lexer {
|
|||||||
self.templateString = templateString
|
self.templateString = templateString
|
||||||
}
|
}
|
||||||
|
|
||||||
func createToken(string: String) -> Token {
|
func createToken(string: String, at range: Range<String.Index>) -> Token {
|
||||||
func strip() -> String {
|
func strip() -> String {
|
||||||
guard string.characters.count > 4 else { return "" }
|
guard string.characters.count > 4 else { return "" }
|
||||||
let start = string.index(string.startIndex, offsetBy: 2)
|
let start = string.index(string.startIndex, offsetBy: 2)
|
||||||
@@ -14,14 +14,14 @@ struct Lexer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if string.hasPrefix("{{") {
|
if string.hasPrefix("{{") {
|
||||||
return .variable(value: strip())
|
return .variable(value: strip(), at: range)
|
||||||
} else if string.hasPrefix("{%") {
|
} else if string.hasPrefix("{%") {
|
||||||
return .block(value: strip())
|
return .block(value: strip(), at: range)
|
||||||
} else if string.hasPrefix("{#") {
|
} 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.
|
/// Returns an array of tokens from a given template string.
|
||||||
@@ -39,14 +39,14 @@ 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 {
|
||||||
tokens.append(createToken(string: text.1))
|
tokens.append(createToken(string: text.1, at: scanner.range))
|
||||||
}
|
}
|
||||||
|
|
||||||
let end = map[text.0]!
|
let end = map[text.0]!
|
||||||
let result = scanner.scan(until: end, returnUntil: true)
|
let result = scanner.scan(until: end, returnUntil: true)
|
||||||
tokens.append(createToken(string: result))
|
tokens.append(createToken(string: result, at: scanner.range))
|
||||||
} else {
|
} else {
|
||||||
tokens.append(createToken(string: scanner.content))
|
tokens.append(createToken(string: scanner.content, at: scanner.range))
|
||||||
scanner.content = ""
|
scanner.content = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,41 +57,50 @@ struct Lexer {
|
|||||||
|
|
||||||
|
|
||||||
class Scanner {
|
class Scanner {
|
||||||
|
let _content: String //stores original content
|
||||||
var content: String
|
var content: String
|
||||||
|
var range: Range<String.Index>
|
||||||
|
|
||||||
init(_ content: String) {
|
init(_ content: String) {
|
||||||
|
self._content = content
|
||||||
self.content = content
|
self.content = content
|
||||||
|
range = content.startIndex..<content.startIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
var isEmpty: Bool {
|
var isEmpty: Bool {
|
||||||
return content.isEmpty
|
return content.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
func scan(until: String, returnUntil: Bool = false) -> String {
|
func scan(until: String, returnUntil: Bool = false) -> String {
|
||||||
|
var index = content.startIndex
|
||||||
|
|
||||||
if until.isEmpty {
|
if until.isEmpty {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var index = content.startIndex
|
range = range.upperBound..<range.upperBound
|
||||||
while index != content.endIndex {
|
while index != content.endIndex {
|
||||||
let substring = content.substring(from: index)
|
let substring = content.substring(from: index)
|
||||||
|
|
||||||
if substring.hasPrefix(until) {
|
if substring.hasPrefix(until) {
|
||||||
let result = content.substring(to: index)
|
let result = content.substring(to: index)
|
||||||
content = substring
|
|
||||||
|
|
||||||
if returnUntil {
|
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
|
return result + until
|
||||||
}
|
}
|
||||||
|
|
||||||
|
content = substring
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
index = content.index(after: index)
|
index = content.index(after: index)
|
||||||
|
range = range.lowerBound..<_content.index(after: range.upperBound)
|
||||||
}
|
}
|
||||||
|
|
||||||
content = ""
|
content = ""
|
||||||
|
range = "".range
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +110,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var index = content.startIndex
|
var index = content.startIndex
|
||||||
|
range = range.upperBound..<range.upperBound
|
||||||
while index != content.endIndex {
|
while index != content.endIndex {
|
||||||
let substring = content.substring(from: index)
|
let substring = content.substring(from: index)
|
||||||
for string in until {
|
for string in until {
|
||||||
@@ -112,6 +122,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
index = content.index(after: index)
|
index = content.index(after: index)
|
||||||
|
range = range.lowerBound..<_content.index(after: range.upperBound)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -151,4 +162,21 @@ extension String {
|
|||||||
let last = findLastNot(character: character) ?? endIndex
|
let last = findLastNot(character: character) ?? endIndex
|
||||||
return String(self[first..<last])
|
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 struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
|
||||||
public let description:String
|
public let description:String
|
||||||
|
public var token: Token?
|
||||||
|
|
||||||
public init(_ description:String) {
|
public init(_ description:String) {
|
||||||
self.description = description
|
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()
|
let components = token.components()
|
||||||
guard components.count <= 2 else {
|
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 {
|
if components.count == 2 {
|
||||||
format = Variable(components[1])
|
format = Variable(components[1])
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ public class TokenParser {
|
|||||||
let token = nextToken()!
|
let token = nextToken()!
|
||||||
|
|
||||||
switch token {
|
switch token {
|
||||||
case .text(let text):
|
case .text(let text, _):
|
||||||
nodes.append(TextNode(text: text))
|
nodes.append(TextNode(text: text))
|
||||||
case .variable:
|
case .variable:
|
||||||
nodes.append(VariableNode(variable: try compileFilter(token.contents)))
|
nodes.append(VariableNode(variable: try compileFilter(token.contents)))
|
||||||
@@ -48,8 +48,18 @@ public class TokenParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let tag = token.components().first {
|
if let tag = token.components().first {
|
||||||
let parser = try findTag(name: tag)
|
do {
|
||||||
nodes.append(try parser(self, token))
|
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:
|
case .comment:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ let NSFileNoSuchFileError = 4
|
|||||||
|
|
||||||
/// A class representing a template
|
/// A class representing a template
|
||||||
open class Template: ExpressibleByStringLiteral {
|
open class Template: ExpressibleByStringLiteral {
|
||||||
|
let templateString: String
|
||||||
let environment: Environment
|
let environment: Environment
|
||||||
let tokens: [Token]
|
let tokens: [Token]
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ open class Template: ExpressibleByStringLiteral {
|
|||||||
public required init(templateString: String, environment: Environment? = nil, name: String? = nil) {
|
public required init(templateString: String, environment: Environment? = nil, name: String? = nil) {
|
||||||
self.environment = environment ?? Environment()
|
self.environment = environment ?? Environment()
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.templateString = templateString
|
||||||
|
|
||||||
let lexer = Lexer(templateString: templateString)
|
let lexer = Lexer(templateString: templateString)
|
||||||
tokens = lexer.tokenize()
|
tokens = lexer.tokenize()
|
||||||
@@ -66,8 +68,17 @@ open class Template: ExpressibleByStringLiteral {
|
|||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
let context = context
|
let context = context
|
||||||
let parser = TokenParser(tokens: tokens, environment: context.environment)
|
let parser = TokenParser(tokens: tokens, environment: context.environment)
|
||||||
let nodes = try parser.parse()
|
do {
|
||||||
return try renderNodes(nodes, context)
|
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
|
/// 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 {
|
public enum Token : Equatable {
|
||||||
/// A token representing a piece of text.
|
/// A token representing a piece of text.
|
||||||
case text(value: String)
|
case text(value: String, at: Range<String.Index>)
|
||||||
|
|
||||||
/// A token representing a variable.
|
/// A token representing a variable.
|
||||||
case variable(value: String)
|
case variable(value: String, at: Range<String.Index>)
|
||||||
|
|
||||||
/// A token representing a comment.
|
/// A token representing a comment.
|
||||||
case comment(value: String)
|
case comment(value: String, at: Range<String.Index>)
|
||||||
|
|
||||||
/// A token representing a template block.
|
/// 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
|
/// Returns the underlying value as an array seperated by spaces
|
||||||
public func components() -> [String] {
|
public func components() -> [String] {
|
||||||
switch self {
|
switch self {
|
||||||
case .block(let value):
|
case .block(let value, _),
|
||||||
return value.smartSplit()
|
.variable(let value, _),
|
||||||
case .variable(let value):
|
.text(let value, _),
|
||||||
return value.smartSplit()
|
.comment(let value, _):
|
||||||
case .text(let value):
|
|
||||||
return value.smartSplit()
|
|
||||||
case .comment(let value):
|
|
||||||
return value.smartSplit()
|
return value.smartSplit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var contents: String {
|
public var contents: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .block(let value):
|
case .block(let value, _),
|
||||||
return value
|
.variable(let value, _),
|
||||||
case .variable(let value):
|
.text(let value, _),
|
||||||
return value
|
.comment(let value, _):
|
||||||
case .text(let value):
|
|
||||||
return value
|
|
||||||
case .comment(let value):
|
|
||||||
return 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func testFilterTag() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("errors without a filter") {
|
$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()
|
try expect(try template.render()).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -170,88 +170,13 @@ func testForNode() {
|
|||||||
|
|
||||||
$0.it("handles invalid input") {
|
$0.it("handles invalid input") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "for i"),
|
.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 '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)
|
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ func testIfNode() {
|
|||||||
$0.describe("parsing") {
|
$0.describe("parsing") {
|
||||||
$0.it("can parse an if block") {
|
$0.it("can parse an if block") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value"),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -25,11 +25,11 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("can parse an if with else block") {
|
$0.it("can parse an if with else block") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value"),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "else"),
|
.block(value: "else", at: .unknown),
|
||||||
.text(value: "false"),
|
.text(value: "false", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -50,13 +50,13 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("can parse an if with elif block") {
|
$0.it("can parse an if with elif block") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value"),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "elif something"),
|
.block(value: "elif something", at: .unknown),
|
||||||
.text(value: "some"),
|
.text(value: "some", at: .unknown),
|
||||||
.block(value: "else"),
|
.block(value: "else", at: .unknown),
|
||||||
.text(value: "false"),
|
.text(value: "false", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -81,11 +81,11 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("can parse an if with elif block without else") {
|
$0.it("can parse an if with elif block without else") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value"),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "elif something"),
|
.block(value: "elif something", at: .unknown),
|
||||||
.text(value: "some"),
|
.text(value: "some", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -106,15 +106,15 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("can parse an if with multiple elif block") {
|
$0.it("can parse an if with multiple elif block") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value"),
|
.block(value: "if value", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "elif something1"),
|
.block(value: "elif something1", at: .unknown),
|
||||||
.text(value: "some1"),
|
.text(value: "some1", at: .unknown),
|
||||||
.block(value: "elif something2"),
|
.block(value: "elif something2", at: .unknown),
|
||||||
.text(value: "some2"),
|
.text(value: "some2", at: .unknown),
|
||||||
.block(value: "else"),
|
.block(value: "else", at: .unknown),
|
||||||
.text(value: "false"),
|
.text(value: "false", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -144,9 +144,9 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("can parse an if with complex expression") {
|
$0.it("can parse an if with complex expression") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value == \"test\" and not name"),
|
.block(value: "if value == \"test\" and not name", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -156,11 +156,11 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("can parse an ifnot block") {
|
$0.it("can parse an ifnot block") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "ifnot value"),
|
.block(value: "ifnot value", at: .unknown),
|
||||||
.text(value: "false"),
|
.text(value: "false", at: .unknown),
|
||||||
.block(value: "else"),
|
.block(value: "else", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
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") {
|
$0.it("throws an error when parsing an if block without an endif") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value"),
|
.block(value: "if value", at: .unknown),
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
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") {
|
$0.it("throws an error when parsing an ifnot without an endif") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "ifnot value"),
|
.block(value: "ifnot value", at: .unknown),
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -242,9 +242,9 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("supports variable filters in the if expression") {
|
$0.it("supports variable filters in the if expression") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if value|uppercase == \"TEST\""),
|
.block(value: "if value|uppercase == \"TEST\"", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
@@ -256,9 +256,9 @@ func testIfNode() {
|
|||||||
|
|
||||||
$0.it("evaluates nil properties as false") {
|
$0.it("evaluates nil properties as false") {
|
||||||
let tokens: [Token] = [
|
let tokens: [Token] = [
|
||||||
.block(value: "if instance.value"),
|
.block(value: "if instance.value", at: .unknown),
|
||||||
.text(value: "true"),
|
.text(value: "true", at: .unknown),
|
||||||
.block(value: "endif")
|
.block(value: "endif", at: .unknown)
|
||||||
]
|
]
|
||||||
|
|
||||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ func testInclude() {
|
|||||||
|
|
||||||
$0.describe("parsing") {
|
$0.describe("parsing") {
|
||||||
$0.it("throws an error when no template is given") {
|
$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 parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
|
|
||||||
let error = TemplateSyntaxError("'include' tag takes one argument, the template file to be included")
|
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") {
|
$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 parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
|
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
|
|||||||
@@ -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")
|
try expect(tokens.first) == .text(value: "Hello World", at: "Hello World".range)
|
||||||
}
|
}
|
||||||
|
|
||||||
$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")
|
try expect(tokens.first) == .comment(value: "Comment", at: "{# Comment #}".range)
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize a variable") {
|
$0.it("can tokenize a variable") {
|
||||||
@@ -25,34 +25,37 @@ 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")
|
try expect(tokens.first) == .variable(value: "Variable", at: "{{ Variable }}".range)
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize unclosed tag by ignoring it") {
|
$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()
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
try expect(tokens.count) == 1
|
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") {
|
$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()
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
try expect(tokens.count) == 3
|
try expect(tokens.count) == 3
|
||||||
try expect(tokens[0]) == Token.text(value: "My name is ")
|
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")
|
try expect(tokens[1]) == Token.variable(value: "name", at: templateString.range(of: "{{ name }}")!)
|
||||||
try expect(tokens[2]) == Token.text(value: ".")
|
try expect(tokens[2]) == Token.text(value: ".", at: templateString.range(of: ".")!)
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize two variables without being greedy") {
|
$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()
|
let tokens = lexer.tokenize()
|
||||||
|
|
||||||
try expect(tokens.count) == 2
|
try expect(tokens.count) == 2
|
||||||
try expect(tokens[0]) == Token.variable(value: "thing")
|
try expect(tokens[0]) == Token.variable(value: "thing", at: templateString.range(of: "{{ thing }}")!)
|
||||||
try expect(tokens[1]) == Token.variable(value: "name")
|
try expect(tokens[1]) == Token.variable(value: "name", at: templateString.range(of: "{{ name }}")!)
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can tokenize an unclosed block") {
|
$0.it("can tokenize an unclosed block") {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ func testNowNode() {
|
|||||||
describe("NowNode") {
|
describe("NowNode") {
|
||||||
$0.describe("parsing") {
|
$0.describe("parsing") {
|
||||||
$0.it("parses default format without any now arguments") {
|
$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 parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
|
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
@@ -18,7 +18,7 @@ func testNowNode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("parses now with a format") {
|
$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 parser = TokenParser(tokens: tokens, environment: Environment())
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
let node = nodes.first as? NowNode
|
let node = nodes.first as? NowNode
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ func testTokenParser() {
|
|||||||
describe("TokenParser") {
|
describe("TokenParser") {
|
||||||
$0.it("can parse a text token") {
|
$0.it("can parse a text token") {
|
||||||
let parser = TokenParser(tokens: [
|
let parser = TokenParser(tokens: [
|
||||||
.text(value: "Hello World")
|
.text(value: "Hello World", at: .unknown)
|
||||||
], environment: Environment())
|
], environment: Environment())
|
||||||
|
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
@@ -18,7 +18,7 @@ func testTokenParser() {
|
|||||||
|
|
||||||
$0.it("can parse a variable token") {
|
$0.it("can parse a variable token") {
|
||||||
let parser = TokenParser(tokens: [
|
let parser = TokenParser(tokens: [
|
||||||
.variable(value: "'name'")
|
.variable(value: "'name'", at: .unknown)
|
||||||
], environment: Environment())
|
], environment: Environment())
|
||||||
|
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
@@ -30,7 +30,7 @@ func testTokenParser() {
|
|||||||
|
|
||||||
$0.it("can parse a comment token") {
|
$0.it("can parse a comment token") {
|
||||||
let parser = TokenParser(tokens: [
|
let parser = TokenParser(tokens: [
|
||||||
.comment(value: "Secret stuff!")
|
.comment(value: "Secret stuff!", at: .unknown)
|
||||||
], environment: Environment())
|
], environment: Environment())
|
||||||
|
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
@@ -44,7 +44,7 @@ func testTokenParser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let parser = TokenParser(tokens: [
|
let parser = TokenParser(tokens: [
|
||||||
.block(value: "known"),
|
.block(value: "known", at: .unknown),
|
||||||
], environment: Environment(extensions: [simpleExtension]))
|
], environment: Environment(extensions: [simpleExtension]))
|
||||||
|
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
@@ -53,7 +53,7 @@ func testTokenParser() {
|
|||||||
|
|
||||||
$0.it("errors when parsing an unknown tag") {
|
$0.it("errors when parsing an unknown tag") {
|
||||||
let parser = TokenParser(tokens: [
|
let parser = TokenParser(tokens: [
|
||||||
.block(value: "unknown"),
|
.block(value: "unknown", at: .unknown),
|
||||||
], environment: Environment())
|
], environment: Environment())
|
||||||
|
|
||||||
try expect(try parser.parse()).toThrow(TemplateSyntaxError("Unknown template tag 'unknown'"))
|
try expect(try parser.parse()).toThrow(TemplateSyntaxError("Unknown template tag 'unknown'"))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
@testable import Stencil
|
||||||
|
|
||||||
|
|
||||||
func testTemplate() {
|
func testTemplate() {
|
||||||
@@ -15,5 +15,30 @@ func testTemplate() {
|
|||||||
let result = try template.render([ "name": "Kyle" ])
|
let result = try template.render([ "name": "Kyle" ])
|
||||||
try expect(result) == "Hello World"
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
@testable import Stencil
|
||||||
|
|
||||||
|
|
||||||
func testToken() {
|
func testToken() {
|
||||||
describe("Token") {
|
describe("Token") {
|
||||||
$0.it("can split the contents into components") {
|
$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()
|
let components = token.components()
|
||||||
|
|
||||||
try expect(components.count) == 2
|
try expect(components.count) == 2
|
||||||
@@ -14,7 +14,7 @@ func testToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can split the contents into components with single quoted strings") {
|
$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()
|
let components = token.components()
|
||||||
|
|
||||||
try expect(components.count) == 2
|
try expect(components.count) == 2
|
||||||
@@ -23,7 +23,7 @@ func testToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can split the contents into components with double quoted strings") {
|
$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()
|
let components = token.components()
|
||||||
|
|
||||||
try expect(components.count) == 2
|
try expect(components.count) == 2
|
||||||
|
|||||||
Reference in New Issue
Block a user