From 6300dbc7bf809908970d5e3e02d6b4fa3622f208 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Tue, 3 Oct 2017 22:47:28 +0200 Subject: [PATCH 01/81] 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 From 69cd8e4d3bf4682cb9a022adbb7b3322a1dea657 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 7 Oct 2017 20:54:42 +0200 Subject: [PATCH 02/81] replaced Token with Lexeme protocol on TemplateSyntaxError --- Sources/Errors.swift | 26 ++++++++++++++++++++++++++ Sources/Lexer.swift | 37 +++++++++++++++++++++---------------- Sources/Node.swift | 27 --------------------------- Sources/Parser.swift | 4 ++-- Sources/Template.swift | 13 ++----------- Sources/Tokenizer.swift | 14 +------------- 6 files changed, 52 insertions(+), 69 deletions(-) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 964eeae..1030d6d 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -17,3 +17,29 @@ public class TemplateDoesNotExist: Error, CustomStringConvertible { return "Template named `\(templates)` does not exist. No loaders found" } } + +public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { + public let description:String + var lexeme: Lexeme? + + public init(_ description:String) { + self.description = description + } + + public static func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool { + return lhs.description == rhs.description + } + +} + +extension Range where Bound == String.Index { + internal static var unknown: Range { + return "".range + } +} + +extension String { + var range: Range { + return startIndex.. (content: String, number: Int, offset: String.IndexDistance) { + var lineNumber: Int = 0 + var offset = 0 + var lineContent = "" + + templateString.enumerateLines { (line, stop) in + lineNumber += 1 + lineContent = line + if let rangeOfLine = self.templateString.range(of: line), rangeOfLine.contains(lexeme.range.lowerBound) { + offset = self.templateString.distance(from: rangeOfLine.lowerBound, to: + lexeme.range.lowerBound) + stop = true + } + } + return (lineContent, lineNumber, offset) + } + } +protocol Lexeme { + var range: Range { get } +} class Scanner { let _content: String //stores original content @@ -163,20 +184,4 @@ extension String { return String(self[first..) -> (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 1c2eed4..1d2020d 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -1,32 +1,5 @@ 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) - } - -} - - -public func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool { - return lhs.description == rhs.description -} - - public protocol NodeType { /// Render the node in the given context func render(_ context:Context) throws -> String diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 5c840bf..24fcdab 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -53,8 +53,8 @@ public class TokenParser { let node = try parser(self, token) nodes.append(node) } catch { - if var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil { - syntaxError.token = token + if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil { + syntaxError.lexeme = token throw syntaxError } else { throw error diff --git a/Sources/Template.swift b/Sources/Template.swift index 7a80ee8..db570e0 100644 --- a/Sources/Template.swift +++ b/Sources/Template.swift @@ -68,17 +68,8 @@ open class Template: ExpressibleByStringLiteral { func render(_ context: Context) throws -> String { let context = context let parser = TokenParser(tokens: tokens, environment: context.environment) - 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 - } - } + let nodes = try parser.parse() + return try renderNodes(nodes, context) } /// Render the given template diff --git a/Sources/Tokenizer.swift b/Sources/Tokenizer.swift index cae09cc..60f621b 100644 --- a/Sources/Tokenizer.swift +++ b/Sources/Tokenizer.swift @@ -40,19 +40,7 @@ extension String { } } -extension Range where Bound == String.Index { - internal static var unknown: Range { - return "".range - } -} - -extension String { - var range: Range { - return startIndex..) From 0edb38588d4d94a6bf5de83c577b4469875b13fd Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 7 Oct 2017 21:00:52 +0200 Subject: [PATCH 03/81] added ErrorReporter protocol with default implementation --- Sources/Errors.swift | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 1030d6d..e3c1441 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -32,6 +32,45 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { } +public class ErrorReporterContext { + public let template: Template + + public init(template: Template) { + self.template = template + } +} + +public protocol ErrorReporter: class { + var context: ErrorReporterContext! { get set } + func report(error: Error) throws + func contextAwareError(_ error: TemplateSyntaxError, context: ErrorReporterContext) -> Error? +} + +open class SimpleErrorReporter: ErrorReporter { + public var context: ErrorReporterContext! + + open func report(error: Error) throws { + guard let syntaxError = error as? TemplateSyntaxError else { throw error } + guard let context = context else { throw error } + throw contextAwareError(syntaxError, context: context) ?? error + } + + open func contextAwareError(_ error: TemplateSyntaxError, context: ErrorReporterContext) -> Error? { + guard let lexeme = error.lexeme, lexeme.range != .unknown else { return nil } + let templateName = context.template.name.map({ "\($0):" }) ?? "" + let tokenContent = context.template.templateString.substring(with: lexeme.range) + let lexer = Lexer(templateString: context.template.templateString) + let line = lexer.lexemeLine(lexeme) + let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(tokenContent.count - 1, 0))))" + let description = """ + \(templateName)\(line.number):\(line.offset): error: \(error.description) + \(line.content) + \(highlight) + """ + return TemplateSyntaxError(description) + } +} + extension Range where Bound == String.Index { internal static var unknown: Range { return "".range From d5f0be959f6f1c5c52486414539095618ed67de7 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 7 Oct 2017 21:01:28 +0200 Subject: [PATCH 04/81] using error reporter from environment to handle syntax errors --- Sources/Environment.swift | 22 +++++++++++-- Tests/StencilTests/EnvironmentSpec.swift | 42 +++++++++++++++++++++++- Tests/StencilTests/TemplateSpec.swift | 24 -------------- 3 files changed, 60 insertions(+), 28 deletions(-) diff --git a/Sources/Environment.swift b/Sources/Environment.swift index 6b78fec..940c146 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -3,9 +3,15 @@ public struct Environment { public var extensions: [Extension] public var loader: Loader? + public var errorReporter: ErrorReporter - public init(loader: Loader? = nil, extensions: [Extension]? = nil, templateClass: Template.Type = Template.self) { + public init(loader: Loader? = nil, + extensions: [Extension]? = nil, + templateClass: Template.Type = Template.self, + errorReporter: ErrorReporter = SimpleErrorReporter()) { + self.templateClass = templateClass + self.errorReporter = errorReporter self.loader = loader self.extensions = (extensions ?? []) + [DefaultExtension()] } @@ -28,11 +34,21 @@ public struct Environment { public func renderTemplate(name: String, context: [String: Any]? = nil) throws -> String { let template = try loadTemplate(name: name) - return try template.render(context) + return try render(template: template, context: context) } public func renderTemplate(string: String, context: [String: Any]? = nil) throws -> String { let template = templateClass.init(templateString: string, environment: self) - return try template.render(context) + return try render(template: template, context: context) + } + + func render(template: Template, context: [String: Any]?) throws -> String { + errorReporter.context = ErrorReporterContext(template: template) + do { + return try template.render(context) + } catch { + try errorReporter.report(error: error) + return "" + } } } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 8bb5bfc..afc8d9b 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -1,5 +1,5 @@ import Spectre -import Stencil +@testable import Stencil func testEnvironment() { @@ -32,6 +32,46 @@ func testEnvironment() { try expect(result) == "here" } + + func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { + var error = TemplateSyntaxError(description) + error.lexeme = Token.block(value: token, at: template.templateString.range(of: token)!) + let context = ErrorReporterContext(template: template) + error = environment.errorReporter.contextAwareError(error, context: context) as! TemplateSyntaxError + print(error) + return error + } + + $0.it("throws syntax error on invalid for tag syntax") { + let template: Template = "Hello {% for name in %}{{ name }}, {% endfor %}!" + let error = expectedSyntaxError( + token: "{% for name in %}", + template: template, + description: "'for' statements should use the following syntax 'for x in y where condition'." + ) + try expect(try environment.renderTemplate(string: template.templateString, context:["names": ["Bob", "Alice"]])).toThrow(error) + } + + $0.it("throws syntax error on missing endfor") { + let template: Template = "{% for name in names %}{{ name }}" + let error = expectedSyntaxError( + token: "{% for name in names %}", + template: template, + description: "`endfor` was not found." + ) + try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) + } + + $0.it("throws syntax error on unknown tag") { + let template: Template = "{% for name in names %}{{ name }}{% end %}" + let error = expectedSyntaxError( + token: "{% end %}", + template: template, + description: "Unknown template tag 'end'" + ) + try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) + } + } } diff --git a/Tests/StencilTests/TemplateSpec.swift b/Tests/StencilTests/TemplateSpec.swift index 42dac28..fee0c5e 100644 --- a/Tests/StencilTests/TemplateSpec.swift +++ b/Tests/StencilTests/TemplateSpec.swift @@ -15,30 +15,6 @@ 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) - } } } From e59609f1404deaf2ffd13d4533486c71f1567dcc Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 7 Oct 2017 23:10:27 +0200 Subject: [PATCH 05/81] handling unknown filter errors --- Sources/Environment.swift | 5 ++ Sources/FilterTag.swift | 2 +- Sources/ForTag.swift | 16 +++---- Sources/IfTag.swift | 26 +++++------ Sources/Parser.swift | 20 +++++++- Sources/Variable.swift | 2 - Tests/StencilTests/EnvironmentSpec.swift | 58 ++++++++++++++++++++++-- Tests/StencilTests/ExpressionSpec.swift | 24 +++++----- Tests/StencilTests/ForNodeSpec.swift | 4 +- 9 files changed, 114 insertions(+), 43 deletions(-) diff --git a/Sources/Environment.swift b/Sources/Environment.swift index 940c146..c6fe7e2 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -51,4 +51,9 @@ public struct Environment { return "" } } + + var template: Template? { + return errorReporter.context?.template + } + } diff --git a/Sources/FilterTag.swift b/Sources/FilterTag.swift index 63ce321..5448996 100644 --- a/Sources/FilterTag.swift +++ b/Sources/FilterTag.swift @@ -15,7 +15,7 @@ class FilterNode : NodeType { throw TemplateSyntaxError("`endfilter` was not found.") } - let resolvable = try parser.compileFilter("filter_value|\(bits[1])") + let resolvable = try parser.compileFilter("filter_value|\(bits[1])", containedIn: token) return FilterNode(nodes: blocks, resolvable: resolvable) } diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index 032653f..5eb181d 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -21,24 +21,24 @@ class ForNode : NodeType { .map { $0.trimmingCharacters(in: CharacterSet.whitespaces) } let variable = components[3] + let filter = try parser.compileFilter(variable, containedIn: token) var emptyNodes = [NodeType]() let forNodes = try parser.parse(until(["endfor", "empty"])) - guard let token = parser.nextToken() else { + if let token = parser.nextToken() { + if token.contents == "empty" { + emptyNodes = try parser.parse(until(["endfor"])) + _ = parser.nextToken() + } + } else { throw TemplateSyntaxError("`endfor` was not found.") } - if token.contents == "empty" { - emptyNodes = try parser.parse(until(["endfor"])) - _ = parser.nextToken() - } - - let filter = try parser.compileFilter(variable) let `where`: Expression? if components.count >= 6 { - `where` = try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser) + `where` = try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser, token: token) } else { `where` = nil } diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index e4305a9..706744a 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -100,7 +100,7 @@ final class IfExpressionParser { let tokens: [IfToken] var position: Int = 0 - init(components: [String], tokenParser: TokenParser) throws { + init(components: [String], tokenParser: TokenParser, token: Token) throws { self.tokens = try components.map { component in if let op = findOperator(name: component) { switch op { @@ -111,7 +111,7 @@ final class IfExpressionParser { } } - return .variable(try tokenParser.compileFilter(component)) + return .variable(try tokenParser.compileFilter(component, containedIn: token)) } } @@ -155,8 +155,8 @@ final class IfExpressionParser { } -func parseExpression(components: [String], tokenParser: TokenParser) throws -> Expression { - let parser = try IfExpressionParser(components: components, tokenParser: tokenParser) +func parseExpression(components: [String], tokenParser: TokenParser, token: Token) throws -> Expression { + let parser = try IfExpressionParser(components: components, tokenParser: tokenParser, token: token) return try parser.parse() } @@ -187,7 +187,7 @@ class IfNode : NodeType { var components = token.components() components.removeFirst() - let expression = try parseExpression(components: components, tokenParser: parser) + let expression = try parseExpression(components: components, tokenParser: parser, token: token) let nodes = try parser.parse(until(["endif", "elif", "else"])) var conditions: [IfCondition] = [ IfCondition(expression: expression, nodes: nodes) @@ -197,7 +197,7 @@ class IfNode : NodeType { while let current = token, current.contents.hasPrefix("elif") { var components = current.components() components.removeFirst() - let expression = try parseExpression(components: components, tokenParser: parser) + let expression = try parseExpression(components: components, tokenParser: parser, token: current) let nodes = try parser.parse(until(["endif", "elif", "else"])) token = parser.nextToken() @@ -227,16 +227,16 @@ class IfNode : NodeType { falseNodes = try parser.parse(until(["endif", "else"])) - guard let token = parser.nextToken() else { + if let token = parser.nextToken() { + if token.contents == "else" { + trueNodes = try parser.parse(until(["endif"])) + _ = parser.nextToken() + } + } else { throw TemplateSyntaxError("`endif` was not found.") } - if token.contents == "else" { - trueNodes = try parser.parse(until(["endif"])) - _ = parser.nextToken() - } - - let expression = try parseExpression(components: components, tokenParser: parser) + let expression = try parseExpression(components: components, tokenParser: parser, token: token) return IfNode(conditions: [ IfCondition(expression: expression, nodes: trueNodes), IfCondition(expression: nil, nodes: falseNodes), diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 24fcdab..f59d205 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -40,7 +40,7 @@ public class TokenParser { case .text(let text, _): nodes.append(TextNode(text: text)) case .variable: - nodes.append(VariableNode(variable: try compileFilter(token.contents))) + nodes.append(VariableNode(variable: try compileFilter(token.contents, containedIn: token))) case .block: if let parse_until = parse_until , parse_until(self, token) { prependToken(token) @@ -100,7 +100,23 @@ public class TokenParser { throw TemplateSyntaxError("Unknown filter '\(name)'") } - + + public func compileFilter(_ filterToken: String, containedIn containingToken: Token) throws -> Resolvable { + do { + return try FilterExpression(token: filterToken, parser: self) + } catch { + if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil, + let filterTokenRange = environment.template?.templateString.range(of: filterToken, range: containingToken.range) { + + syntaxError.lexeme = Token.block(value: filterToken, at: filterTokenRange) + throw syntaxError + } else { + throw error + } + } + } + + @available(*, deprecated, message: "Use compileFilter(_:containedIn:)") public func compileFilter(_ token: String) throws -> Resolvable { return try FilterExpression(token: token, parser: self) } diff --git a/Sources/Variable.swift b/Sources/Variable.swift index c17b966..bc40900 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -11,8 +11,6 @@ class FilterExpression : Resolvable { init(token: String, parser: TokenParser) throws { let bits = token.characters.split(separator: "|").map({ String($0).trim(character: " ") }) if bits.isEmpty { - filters = [] - variable = Variable("") throw TemplateSyntaxError("Variable tags must include at least 1 argument") } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index afc8d9b..070655c 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -42,7 +42,7 @@ func testEnvironment() { return error } - $0.it("throws syntax error on invalid for tag syntax") { + $0.it("reports syntax error on invalid for tag syntax") { let template: Template = "Hello {% for name in %}{{ name }}, {% endfor %}!" let error = expectedSyntaxError( token: "{% for name in %}", @@ -52,7 +52,7 @@ func testEnvironment() { try expect(try environment.renderTemplate(string: template.templateString, context:["names": ["Bob", "Alice"]])).toThrow(error) } - $0.it("throws syntax error on missing endfor") { + $0.it("reports syntax error on missing endfor") { let template: Template = "{% for name in names %}{{ name }}" let error = expectedSyntaxError( token: "{% for name in names %}", @@ -62,7 +62,7 @@ func testEnvironment() { try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) } - $0.it("throws syntax error on unknown tag") { + $0.it("reports syntax error on unknown tag") { let template: Template = "{% for name in names %}{{ name }}{% end %}" let error = expectedSyntaxError( token: "{% end %}", @@ -72,6 +72,58 @@ func testEnvironment() { try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) } + $0.context("given unknown filter") { + func expectedFilterError(token: String, template: Template) -> TemplateSyntaxError { + return expectedSyntaxError( + token: token, + template: template, + description: "Unknown filter 'unknown'" + ) + } + + $0.it("reports syntax error in for tag") { + let template: Template = "{% for name in names|unknown %}{{ name }}{% endfor %}" + let error = expectedFilterError(token: "names|unknown", template: template) + try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) + } + + $0.it("reports syntax error in for-where tag") { + let template: Template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}" + let error = expectedFilterError(token: "name|unknown", template: template) + try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) + } + + $0.it("reports syntax error in if tag") { + let template: Template = "{% if name|unknown %}{{ name }}{% endif %}" + let error = expectedFilterError(token: "name|unknown", template: template) + try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) + } + + $0.it("reports syntax error in elif tag") { + let template: Template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}" + let error = expectedFilterError(token: "name|unknown", template: template) + try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) + } + + $0.it("reports syntax error in ifnot tag") { + let template: Template = "{% ifnot name|unknown %}{{ name }}{% endif %}" + let error = expectedFilterError(token: "name|unknown", template: template) + try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) + } + + $0.it("reports syntax error in filter tag") { + let template: Template = "{% filter unknown %}Text{% endfilter %}" + let error = expectedFilterError(token: "{% filter unknown %}", template: template) + try expect(try environment.renderTemplate(string: template.templateString, context: [:])).toThrow(error) + } + + $0.it("reports syntax error in variable tag") { + let template: Template = "{{ name|unknown }}" + let error = expectedFilterError(token: "name|unknown", template: template) + try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) + } + } + } } diff --git a/Tests/StencilTests/ExpressionSpec.swift b/Tests/StencilTests/ExpressionSpec.swift index 4b7958d..00bd68a 100644 --- a/Tests/StencilTests/ExpressionSpec.swift +++ b/Tests/StencilTests/ExpressionSpec.swift @@ -105,19 +105,19 @@ func testExpressions() { $0.describe("expression parsing") { $0.it("can parse a variable expression") { - let expression = try parseExpression(components: ["value"], tokenParser: parser) + let expression = try parseExpression(components: ["value"], tokenParser: parser, token: .text(value: "", at: .unknown)) try expect(expression.evaluate(context: Context())).to.beFalse() try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue() } $0.it("can parse a not expression") { - let expression = try parseExpression(components: ["not", "value"], tokenParser: parser) + let expression = try parseExpression(components: ["not", "value"], tokenParser: parser, token: .text(value: "", at: .unknown)) try expect(expression.evaluate(context: Context())).to.beTrue() try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse() } $0.describe("and expression") { - let expression = try! parseExpression(components: ["lhs", "and", "rhs"], tokenParser: parser) + let expression = try! parseExpression(components: ["lhs", "and", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to false with lhs false") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse() @@ -137,7 +137,7 @@ func testExpressions() { } $0.describe("or expression") { - let expression = try! parseExpression(components: ["lhs", "or", "rhs"], tokenParser: parser) + let expression = try! parseExpression(components: ["lhs", "or", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to true with lhs true") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue() @@ -157,7 +157,7 @@ func testExpressions() { } $0.describe("equality expression") { - let expression = try! parseExpression(components: ["lhs", "==", "rhs"], tokenParser: parser) + let expression = try! parseExpression(components: ["lhs", "==", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to true with equal lhs/rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue() @@ -193,7 +193,7 @@ func testExpressions() { } $0.describe("inequality expression") { - let expression = try! parseExpression(components: ["lhs", "!=", "rhs"], tokenParser: parser) + let expression = try! parseExpression(components: ["lhs", "!=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to true with inequal lhs/rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue() @@ -205,7 +205,7 @@ func testExpressions() { } $0.describe("more than expression") { - let expression = try! parseExpression(components: ["lhs", ">", "rhs"], tokenParser: parser) + let expression = try! parseExpression(components: ["lhs", ">", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to true with lhs > rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue() @@ -217,7 +217,7 @@ func testExpressions() { } $0.describe("more than equal expression") { - let expression = try! parseExpression(components: ["lhs", ">=", "rhs"], tokenParser: parser) + let expression = try! parseExpression(components: ["lhs", ">=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to true with lhs == rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() @@ -229,7 +229,7 @@ func testExpressions() { } $0.describe("less than expression") { - let expression = try! parseExpression(components: ["lhs", "<", "rhs"], tokenParser: parser) + let expression = try! parseExpression(components: ["lhs", "<", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to true with lhs < rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue() @@ -241,7 +241,7 @@ func testExpressions() { } $0.describe("less than equal expression") { - let expression = try! parseExpression(components: ["lhs", "<=", "rhs"], tokenParser: parser) + let expression = try! parseExpression(components: ["lhs", "<=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to true with lhs == rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() @@ -253,7 +253,7 @@ func testExpressions() { } $0.describe("multiple expression") { - let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"], tokenParser: parser) + let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to true with one") { try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue() @@ -281,7 +281,7 @@ func testExpressions() { } $0.describe("in expression") { - let expression = try! parseExpression(components: ["lhs", "in", "rhs"], tokenParser: parser) + let expression = try! parseExpression(components: ["lhs", "in", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) $0.it("evaluates to true when rhs contains lhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue() diff --git a/Tests/StencilTests/ForNodeSpec.swift b/Tests/StencilTests/ForNodeSpec.swift index 6cf69b3..8d41c1d 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -91,7 +91,7 @@ func testForNode() { $0.it("renders the given nodes while filtering items using where expression") { let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")] - let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment())) + let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown)) let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`) try expect(try node.render(context)) == "2132" } @@ -99,7 +99,7 @@ func testForNode() { $0.it("renders the given empty nodes when all items filtered out with where expression") { let nodes: [NodeType] = [VariableNode(variable: "item")] let emptyNodes: [NodeType] = [TextNode(text: "empty")] - let `where` = try parseExpression(components: ["item", "==", "0"], tokenParser: TokenParser(tokens: [], environment: Environment())) + let `where` = try parseExpression(components: ["item", "==", "0"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown)) let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`) try expect(try node.render(context)) == "empty" } From 079fdf39b8561fa9059c6bfb860adbbc1d845979 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 7 Oct 2017 21:02:27 +0200 Subject: [PATCH 06/81] added parent context to ErrorReporterContext and handling errors in include and extend nodes --- Sources/Environment.swift | 16 +++++++++- Sources/Errors.swift | 11 +++++-- Sources/Include.swift | 12 +++++--- Sources/Inheritence.swift | 12 +++++--- Tests/StencilTests/EnvironmentSpec.swift | 29 +++++++++++++++++++ Tests/StencilTests/IncludeSpec.swift | 6 ++-- Tests/StencilTests/fixtures/invalid-base.html | 2 ++ .../fixtures/invalid-child-super.html | 3 ++ .../fixtures/invalid-include.html | 1 + 9 files changed, 77 insertions(+), 15 deletions(-) create mode 100644 Tests/StencilTests/fixtures/invalid-base.html create mode 100644 Tests/StencilTests/fixtures/invalid-child-super.html create mode 100644 Tests/StencilTests/fixtures/invalid-include.html diff --git a/Sources/Environment.swift b/Sources/Environment.swift index c6fe7e2..611e14e 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -48,7 +48,6 @@ public struct Environment { return try template.render(context) } catch { try errorReporter.report(error: error) - return "" } } @@ -56,4 +55,19 @@ public struct Environment { return errorReporter.context?.template } + + public func pushTemplate(_ 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 { + try errorReporter.report(error: error) + } + } + } diff --git a/Sources/Errors.swift b/Sources/Errors.swift index e3c1441..8ab3c49 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -35,26 +35,31 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { public class ErrorReporterContext { public let template: Template - public init(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 { var context: ErrorReporterContext! { get set } - func report(error: Error) throws + func report(error: Error) throws -> Never func contextAwareError(_ error: TemplateSyntaxError, context: ErrorReporterContext) -> Error? } open class SimpleErrorReporter: ErrorReporter { public var context: ErrorReporterContext! - open func report(error: Error) throws { + open func report(error: Error) throws -> Never { guard let syntaxError = error as? TemplateSyntaxError else { throw error } guard let context = context else { throw error } throw contextAwareError(syntaxError, context: context) ?? error } + // TODO: add stack trace using parent context open func contextAwareError(_ error: TemplateSyntaxError, context: ErrorReporterContext) -> Error? { guard let lexeme = error.lexeme, lexeme.range != .unknown else { return nil } let templateName = context.template.name.map({ "\($0):" }) ?? "" diff --git a/Sources/Include.swift b/Sources/Include.swift index cd9cc5c..2699312 100644 --- a/Sources/Include.swift +++ b/Sources/Include.swift @@ -3,6 +3,7 @@ import PathKit class IncludeNode : NodeType { let templateName: Variable + let token: Token class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { let bits = token.components() @@ -11,11 +12,12 @@ class IncludeNode : NodeType { throw TemplateSyntaxError("'include' tag takes one argument, the template file to be included") } - return IncludeNode(templateName: Variable(bits[1])) + return IncludeNode(templateName: Variable(bits[1]), token: token) } - init(templateName: Variable) { + init(templateName: Variable, token: Token) { self.templateName = templateName + self.token = token } func render(_ context: Context) throws -> String { @@ -25,8 +27,10 @@ class IncludeNode : NodeType { let template = try context.environment.loadTemplate(name: templateName) - return try context.push { - return try template.render(context) + return try context.environment.pushTemplate(template, token: token) { + try context.push { + return try template.render(context) + } } } } diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index b9bf87a..4097c97 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -51,6 +51,7 @@ extension Collection { class ExtendsNode : NodeType { let templateName: Variable let blocks: [String:BlockNode] + let token: Token class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { let bits = token.components() @@ -72,12 +73,13 @@ class ExtendsNode : NodeType { return dict } - return ExtendsNode(templateName: Variable(bits[1]), blocks: nodes) + return ExtendsNode(templateName: Variable(bits[1]), blocks: nodes, token: token) } - init(templateName: Variable, blocks: [String: BlockNode]) { + init(templateName: Variable, blocks: [String: BlockNode], token: Token) { self.templateName = templateName self.blocks = blocks + self.token = token } func render(_ context: Context) throws -> String { @@ -98,8 +100,10 @@ class ExtendsNode : NodeType { blockContext = BlockContext(blocks: blocks) } - return try context.push(dictionary: [BlockContext.contextKey: blockContext]) { - return try template.render(context) + return try context.environment.pushTemplate(template, token: token) { + try context.push(dictionary: [BlockContext.contextKey: blockContext]) { + return try template.render(context) + } } } } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 070655c..cebb73c 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -1,4 +1,5 @@ import Spectre +import PathKit @testable import Stencil @@ -124,6 +125,34 @@ func testEnvironment() { } } + $0.context("given related templates") { + let path = Path(#file) + ".." + "fixtures" + let loader = FileSystemLoader(paths: [path]) + let environment = Environment(loader: loader) + + $0.it("reports syntax error in included template") { + let template: Template = "{% include \"invalid-include.html\"%}" + environment.errorReporter.context = ErrorReporterContext(template: template) + + let context = Context(dictionary: ["target": "World"], environment: environment) + + let includedTemplate = try environment.loadTemplate(name: "invalid-include.html") + let error = expectedSyntaxError(token: "target|unknown", template: includedTemplate, description: "Unknown filter 'unknown'") + + try expect(try template.render(context)).toThrow(error) + } + + $0.it("reports syntax error in extended template") { + let template = try environment.loadTemplate(name: "invalid-child-super.html") + let context = Context(dictionary: ["target": "World"], environment: environment) + + let baseTemplate = try environment.loadTemplate(name: "invalid-base.html") + let error = expectedSyntaxError(token: "target|unknown", template: baseTemplate, description: "Unknown filter 'unknown'") + + try expect(try template.render(context)).toThrow(error) + } + } + } } diff --git a/Tests/StencilTests/IncludeSpec.swift b/Tests/StencilTests/IncludeSpec.swift index 9f789b7..bf95c18 100644 --- a/Tests/StencilTests/IncludeSpec.swift +++ b/Tests/StencilTests/IncludeSpec.swift @@ -31,7 +31,7 @@ func testInclude() { $0.describe("rendering") { $0.it("throws an error when rendering without a loader") { - let node = IncludeNode(templateName: Variable("\"test.html\"")) + let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown)) do { _ = try node.render(Context()) @@ -41,7 +41,7 @@ func testInclude() { } $0.it("throws an error when it cannot find the included template") { - let node = IncludeNode(templateName: Variable("\"unknown.html\"")) + let node = IncludeNode(templateName: Variable("\"unknown.html\""), token: .block(value: "", at: .unknown)) do { _ = try node.render(Context(environment: environment)) @@ -51,7 +51,7 @@ func testInclude() { } $0.it("successfully renders a found included template") { - let node = IncludeNode(templateName: Variable("\"test.html\"")) + let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown)) let context = Context(dictionary: ["target": "World"], environment: environment) let value = try node.render(context) try expect(value) == "Hello World!" diff --git a/Tests/StencilTests/fixtures/invalid-base.html b/Tests/StencilTests/fixtures/invalid-base.html new file mode 100644 index 0000000..51c27a0 --- /dev/null +++ b/Tests/StencilTests/fixtures/invalid-base.html @@ -0,0 +1,2 @@ +{% block header %}Header{% endblock %} +{% block body %}Body {{ target|unknown }} {% endblock %} diff --git a/Tests/StencilTests/fixtures/invalid-child-super.html b/Tests/StencilTests/fixtures/invalid-child-super.html new file mode 100644 index 0000000..23aea65 --- /dev/null +++ b/Tests/StencilTests/fixtures/invalid-child-super.html @@ -0,0 +1,3 @@ +{% extends "invalid-base.html" %} +{% block body %}Child {{ block.super }}{% endblock %} + diff --git a/Tests/StencilTests/fixtures/invalid-include.html b/Tests/StencilTests/fixtures/invalid-include.html new file mode 100644 index 0000000..014ba0e --- /dev/null +++ b/Tests/StencilTests/fixtures/invalid-include.html @@ -0,0 +1 @@ +Hello {{ target|unknown }}! From 768832620464bb47eb047e9d954d0ae5f73a9924 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sun, 8 Oct 2017 00:07:10 +0200 Subject: [PATCH 07/81] renamed Scanner property for original content --- Sources/Lexer.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 7ea6ebc..68ec385 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -78,12 +78,12 @@ protocol Lexeme { } class Scanner { - let _content: String //stores original content + let originalContent: String var content: String var range: Range init(_ content: String) { - self._content = content + self.originalContent = content self.content = content range = content.startIndex.. Date: Sun, 8 Oct 2017 00:38:49 +0200 Subject: [PATCH 08/81] xcode backward compatibility fixes - moved back to single line string literal - fixed calculating string lenght --- Sources/Errors.swift | 20 +++++++++++++------- Sources/Lexer.swift | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 8ab3c49..8fb988e 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -66,13 +66,10 @@ open class SimpleErrorReporter: ErrorReporter { let tokenContent = context.template.templateString.substring(with: lexeme.range) let lexer = Lexer(templateString: context.template.templateString) let line = lexer.lexemeLine(lexeme) - let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(tokenContent.count - 1, 0))))" - let description = """ - \(templateName)\(line.number):\(line.offset): error: \(error.description) - \(line.content) - \(highlight) - """ - return TemplateSyntaxError(description) + let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(tokenContent.length - 1, 0))))" + let description = "\(templateName)\(line.number):\(line.offset): error: \(error.description)\n\(line.content)\n\(highlight)" + let error = TemplateSyntaxError(description) + return error } } @@ -86,4 +83,13 @@ extension String { var range: Range { return startIndex..=3.2) + return count + #else + return characters.count + #endif + } + } diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 68ec385..51d8372 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -107,7 +107,7 @@ class Scanner { let result = content.substring(to: index) if returnUntil { - range = range.lowerBound.. Date: Sun, 8 Oct 2017 01:53:41 +0200 Subject: [PATCH 09/81] fixed iterating over template lines on linux --- Sources/ForTag.swift | 2 +- Sources/Lexer.swift | 13 ++++++++----- Tests/StencilTests/EnvironmentSpec.swift | 1 - 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index 5eb181d..ae32479 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -87,7 +87,7 @@ class ForNode : NodeType { var values: [Any] if let dictionary = resolved as? [String: Any], !dictionary.isEmpty { - values = dictionary.map { ($0.key, $0.value) } + values = dictionary.map { ($0.key, $0.value) } as [(String, Any)] } else if let array = resolved as? [Any] { values = array } else if let range = resolved as? CountableClosedRange { diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 51d8372..8d350e7 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -1,3 +1,5 @@ +import Foundation + struct Lexer { let templateString: String @@ -58,16 +60,17 @@ struct Lexer { var lineNumber: Int = 0 var offset = 0 var lineContent = "" - - templateString.enumerateLines { (line, stop) in + + for line in templateString.components(separatedBy: CharacterSet.newlines) { lineNumber += 1 lineContent = line - if let rangeOfLine = self.templateString.range(of: line), rangeOfLine.contains(lexeme.range.lowerBound) { - offset = self.templateString.distance(from: rangeOfLine.lowerBound, to: + if let rangeOfLine = templateString.range(of: line), rangeOfLine.contains(lexeme.range.lowerBound) { + offset = templateString.distance(from: rangeOfLine.lowerBound, to: lexeme.range.lowerBound) - stop = true + break } } + return (lineContent, lineNumber, offset) } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index cebb73c..3b33a15 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -39,7 +39,6 @@ func testEnvironment() { error.lexeme = Token.block(value: token, at: template.templateString.range(of: token)!) let context = ErrorReporterContext(template: template) error = environment.errorReporter.contextAwareError(error, context: context) as! TemplateSyntaxError - print(error) return error } From 27135f3ea3baa242050256d2df44425eb0fe0fed Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Wed, 29 Nov 2017 23:41:18 +0100 Subject: [PATCH 10/81] changer Never return type to Error in ErrorReporter this resolves warning related to Never type --- Sources/Environment.swift | 4 ++-- Sources/Errors.swift | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/Environment.swift b/Sources/Environment.swift index 611e14e..d612ae4 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -47,7 +47,7 @@ public struct Environment { do { return try template.render(context) } catch { - try errorReporter.report(error: error) + throw errorReporter.reportError(error) } } @@ -66,7 +66,7 @@ public struct Environment { do { return try closure() } catch { - try errorReporter.report(error: error) + throw errorReporter.reportError(error) } } diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 8fb988e..97e93b7 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -46,17 +46,17 @@ public class ErrorReporterContext { public protocol ErrorReporter: class { var context: ErrorReporterContext! { get set } - func report(error: Error) throws -> Never + func reportError(_ error: Error) -> Error func contextAwareError(_ error: TemplateSyntaxError, context: ErrorReporterContext) -> Error? } open class SimpleErrorReporter: ErrorReporter { public var context: ErrorReporterContext! - open func report(error: Error) throws -> Never { - guard let syntaxError = error as? TemplateSyntaxError else { throw error } - guard let context = context else { throw error } - throw contextAwareError(syntaxError, context: context) ?? error + open func reportError(_ error: Error) -> Error { + guard let syntaxError = error as? TemplateSyntaxError else { return error } + guard let context = context else { return error } + return contextAwareError(syntaxError, context: context) ?? error } // TODO: add stack trace using parent context From 53c1550c5bc7ef281293a4a3b236d79ce9c9fcc4 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 23 Dec 2017 15:19:36 +0100 Subject: [PATCH 11/81] =?UTF-8?q?reporting=20node=20rendering=20errors=20u?= =?UTF-8?q?sing=20reference=20to=20node=E2=80=99s=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/Environment.swift | 3 +- Sources/Errors.swift | 19 +++--- Sources/Extension.swift | 2 +- Sources/FilterTag.swift | 8 ++- Sources/ForTag.swift | 6 +- Sources/IfTag.swift | 20 +++--- Sources/Include.swift | 2 +- Sources/Inheritence.swift | 8 ++- Sources/Lexer.swift | 17 +++++ Sources/Node.swift | 33 ++++++++-- Sources/NowTag.swift | 6 +- Sources/Parser.swift | 17 +++-- Tests/StencilTests/EnvironmentSpec.swift | 82 +++++++++++++++++++++++- Tests/StencilTests/NodeSpec.swift | 5 ++ Tests/StencilTests/StencilSpec.swift | 5 +- 15 files changed, 186 insertions(+), 47 deletions(-) diff --git a/Sources/Environment.swift b/Sources/Environment.swift index d612ae4..aafab8a 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -55,8 +55,7 @@ public struct Environment { return errorReporter.context?.template } - - public func pushTemplate(_ template: Template, token: Token, closure: (() throws -> Result)) rethrows -> Result { + public func pushTemplate(_ template: Template, token: Token?, closure: (() throws -> Result)) rethrows -> Result { let errorReporterContext = errorReporter.context defer { errorReporter.context = errorReporterContext } errorReporter.context = ErrorReporterContext( diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 97e93b7..32450cd 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -35,7 +35,7 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { public class ErrorReporterContext { public let template: Template - public typealias ParentContext = (context: ErrorReporterContext, token: Token) + public typealias ParentContext = (context: ErrorReporterContext, token: Token?) public let parent: ParentContext? public init(template: Template, parent: ParentContext? = nil) { @@ -47,30 +47,29 @@ public class ErrorReporterContext { public protocol ErrorReporter: class { var context: ErrorReporterContext! { get set } func reportError(_ error: Error) -> Error - func contextAwareError(_ error: TemplateSyntaxError, context: ErrorReporterContext) -> Error? + func contextAwareError(_ error: Error, at range: Range?, context: ErrorReporterContext) -> Error? } open class SimpleErrorReporter: ErrorReporter { public var context: ErrorReporterContext! open func reportError(_ error: Error) -> Error { - guard let syntaxError = error as? TemplateSyntaxError else { return error } guard let context = context else { return error } - return contextAwareError(syntaxError, context: context) ?? error + return contextAwareError(error, at: (error as? TemplateSyntaxError)?.lexeme?.range, context: context) ?? error } // TODO: add stack trace using parent context - open func contextAwareError(_ error: TemplateSyntaxError, context: ErrorReporterContext) -> Error? { - guard let lexeme = error.lexeme, lexeme.range != .unknown else { return nil } + open func contextAwareError(_ error: Error, at range: Range?, context: ErrorReporterContext) -> Error? { + guard let range = range, range != .unknown else { return nil } let templateName = context.template.name.map({ "\($0):" }) ?? "" - let tokenContent = context.template.templateString.substring(with: lexeme.range) - let lexer = Lexer(templateString: context.template.templateString) - let line = lexer.lexemeLine(lexeme) + let tokenContent = context.template.templateString.substring(with: range) + let line = context.template.templateString.rangeLine(range) let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(tokenContent.length - 1, 0))))" - let description = "\(templateName)\(line.number):\(line.offset): error: \(error.description)\n\(line.content)\n\(highlight)" + let description = "\(templateName)\(line.number):\(line.offset): error: \(error)\n\(line.content)\n\(highlight)" let error = TemplateSyntaxError(description) return error } + } extension Range where Bound == String.Index { diff --git a/Sources/Extension.swift b/Sources/Extension.swift index 9dfa879..6e77aad 100644 --- a/Sources/Extension.swift +++ b/Sources/Extension.swift @@ -15,7 +15,7 @@ open class Extension { /// Registers a simple template tag with a name and a handler public func registerSimpleTag(_ name: String, handler: @escaping (Context) throws -> String) { registerTag(name, parser: { parser, token in - return SimpleNode(handler: handler) + return SimpleNode(token: token, handler: handler) }) } diff --git a/Sources/FilterTag.swift b/Sources/FilterTag.swift index 5448996..4cf9746 100644 --- a/Sources/FilterTag.swift +++ b/Sources/FilterTag.swift @@ -1,6 +1,7 @@ class FilterNode : NodeType { let resolvable: Resolvable let nodes: [NodeType] + let token: Token? class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { let bits = token.components() @@ -16,19 +17,20 @@ class FilterNode : NodeType { } let resolvable = try parser.compileFilter("filter_value|\(bits[1])", containedIn: token) - return FilterNode(nodes: blocks, resolvable: resolvable) + return FilterNode(nodes: blocks, resolvable: resolvable, token: token) } - init(nodes: [NodeType], resolvable: Resolvable) { + init(nodes: [NodeType], resolvable: Resolvable, token: Token) { self.nodes = nodes self.resolvable = resolvable + self.token = token } func render(_ context: Context) throws -> String { let value = try renderNodes(nodes, context) return try context.push(dictionary: ["filter_value": value]) { - return try VariableNode(variable: resolvable).render(context) + return try VariableNode(variable: resolvable, token: token).render(context) } } } diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index ae32479..8e3f5c9 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -6,6 +6,7 @@ class ForNode : NodeType { let nodes:[NodeType] let emptyNodes: [NodeType] let `where`: Expression? + let token: Token? class func parse(_ parser:TokenParser, token:Token) throws -> NodeType { let components = token.components() @@ -42,15 +43,16 @@ class ForNode : NodeType { } else { `where` = nil } - return ForNode(resolvable: filter, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`) + return ForNode(resolvable: filter, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`, token: token) } - init(resolvable: Resolvable, loopVariables: [String], nodes:[NodeType], emptyNodes:[NodeType], where: Expression? = nil) { + init(resolvable: Resolvable, loopVariables: [String], nodes:[NodeType], emptyNodes:[NodeType], where: Expression? = nil, token: Token? = nil) { self.resolvable = resolvable self.loopVariables = loopVariables self.nodes = nodes self.emptyNodes = emptyNodes self.where = `where` + self.token = token } func push(value: Any, context: Context, closure: () throws -> (Result)) rethrows -> Result { diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index 706744a..39a3606 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -182,6 +182,7 @@ final class IfCondition { class IfNode : NodeType { let conditions: [IfCondition] + let token: Token? class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { var components = token.components() @@ -193,27 +194,27 @@ class IfNode : NodeType { IfCondition(expression: expression, nodes: nodes) ] - var token = parser.nextToken() - while let current = token, current.contents.hasPrefix("elif") { + var nextToken = parser.nextToken() + while let current = nextToken, current.contents.hasPrefix("elif") { var components = current.components() components.removeFirst() let expression = try parseExpression(components: components, tokenParser: parser, token: current) let nodes = try parser.parse(until(["endif", "elif", "else"])) - token = parser.nextToken() + nextToken = parser.nextToken() conditions.append(IfCondition(expression: expression, nodes: nodes)) } - if let current = token, current.contents == "else" { + if let current = nextToken, current.contents == "else" { conditions.append(IfCondition(expression: nil, nodes: try parser.parse(until(["endif"])))) - token = parser.nextToken() + nextToken = parser.nextToken() } - guard let current = token, current.contents == "endif" else { + guard let current = nextToken, current.contents == "endif" else { throw TemplateSyntaxError("`endif` was not found.") } - return IfNode(conditions: conditions) + return IfNode(conditions: conditions, token: token) } class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType { @@ -240,11 +241,12 @@ class IfNode : NodeType { return IfNode(conditions: [ IfCondition(expression: expression, nodes: trueNodes), IfCondition(expression: nil, nodes: falseNodes), - ]) + ], token: token) } - init(conditions: [IfCondition]) { + init(conditions: [IfCondition], token: Token? = nil) { self.conditions = conditions + self.token = token } func render(_ context: Context) throws -> String { diff --git a/Sources/Include.swift b/Sources/Include.swift index 2699312..bb85a2d 100644 --- a/Sources/Include.swift +++ b/Sources/Include.swift @@ -3,7 +3,7 @@ import PathKit class IncludeNode : NodeType { let templateName: Variable - let token: Token + let token: Token? class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { let bits = token.components() diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index 4097c97..189e581 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -51,7 +51,7 @@ extension Collection { class ExtendsNode : NodeType { let templateName: Variable let blocks: [String:BlockNode] - let token: Token + let token: Token? class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { let bits = token.components() @@ -112,6 +112,7 @@ class ExtendsNode : NodeType { class BlockNode : NodeType { let name: String let nodes: [NodeType] + let token: Token? class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { let bits = token.components() @@ -123,12 +124,13 @@ class BlockNode : NodeType { let blockName = bits[1] let nodes = try parser.parse(until(["endblock"])) _ = parser.nextToken() - return BlockNode(name:blockName, nodes:nodes) + return BlockNode(name:blockName, nodes:nodes, token: token) } - init(name: String, nodes: [NodeType]) { + init(name: String, nodes: [NodeType], token: Token) { self.name = name self.nodes = nodes + self.token = token } func render(_ context: Context) throws -> String { diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 8d350e7..f2af91e 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -187,4 +187,21 @@ extension String { return String(self[first..) -> (content: String, number: Int, offset: String.IndexDistance) { + var lineNumber: Int = 0 + var offset = 0 + var lineContent = "" + + for line in components(separatedBy: CharacterSet.newlines) { + lineNumber += 1 + lineContent = line + if let rangeOfLine = self.range(of: line), rangeOfLine.contains(range.lowerBound) { + offset = distance(from: rangeOfLine.lowerBound, to: + range.lowerBound) + break + } + } + + return (lineContent, lineNumber, offset) + } } diff --git a/Sources/Node.swift b/Sources/Node.swift index 1d2020d..10ee16a 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -3,18 +3,38 @@ import Foundation public protocol NodeType { /// Render the node in the given context func render(_ context:Context) throws -> String + + /// Reference to this node's token + var token: Token? { get } } /// Render the collection of nodes in the given context public func renderNodes(_ nodes:[NodeType], _ context:Context) throws -> String { - return try nodes.map { try $0.render(context) }.joined(separator: "") + return try nodes.map({ + do { + return try $0.render(context) + } catch { + if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil, let token = $0.token { + if let contentsRange = context.environment.template?.templateString.range(of: token.contents, range: token.range) { + syntaxError.lexeme = Token.block(value: token.contents, at: contentsRange) + } else { + syntaxError.lexeme = token + } + throw syntaxError + } else { + throw error + } + } + }).joined(separator: "") } public class SimpleNode : NodeType { public let handler:(Context) throws -> String + public let token: Token? - public init(handler: @escaping (Context) throws -> String) { + public init(token: Token, handler: @escaping (Context) throws -> String) { + self.token = token self.handler = handler } @@ -26,9 +46,11 @@ public class SimpleNode : NodeType { public class TextNode : NodeType { public let text:String + public let token: Token? public init(text:String) { self.text = text + self.token = nil } public func render(_ context:Context) throws -> String { @@ -44,13 +66,16 @@ public protocol Resolvable { public class VariableNode : NodeType { public let variable: Resolvable + public var token: Token? - public init(variable: Resolvable) { + public init(variable: Resolvable, token: Token? = nil) { self.variable = variable + self.token = token } - public init(variable: String) { + public init(variable: String, token: Token? = nil) { self.variable = Variable(variable) + self.token = token } public func render(_ context: Context) throws -> String { diff --git a/Sources/NowTag.swift b/Sources/NowTag.swift index 17a62a6..6d354ed 100644 --- a/Sources/NowTag.swift +++ b/Sources/NowTag.swift @@ -4,6 +4,7 @@ import Foundation class NowNode : NodeType { let format:Variable + let token: Token? class func parse(_ parser:TokenParser, token:Token) throws -> NodeType { var format:Variable? @@ -16,11 +17,12 @@ class NowNode : NodeType { format = Variable(components[1]) } - return NowNode(format:format) + return NowNode(format:format, token: token) } - init(format:Variable?) { + init(format:Variable?, token: Token? = nil) { self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"") + self.token = token } func render(_ context: Context) throws -> String { diff --git a/Sources/Parser.swift b/Sources/Parser.swift index f59d205..a4d2d88 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -40,7 +40,8 @@ public class TokenParser { case .text(let text, _): nodes.append(TextNode(text: text)) case .variable: - nodes.append(VariableNode(variable: try compileFilter(token.contents, containedIn: token))) + let filter = try compileFilter(token.contents, containedIn: token) + nodes.append(VariableNode(variable: filter, token: token)) case .block: if let parse_until = parse_until , parse_until(self, token) { prependToken(token) @@ -54,8 +55,8 @@ public class TokenParser { nodes.append(node) } catch { if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil { - syntaxError.lexeme = token - throw syntaxError + syntaxError.lexeme = token + throw syntaxError } else { throw error } @@ -105,10 +106,12 @@ public class TokenParser { do { return try FilterExpression(token: filterToken, parser: self) } catch { - if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil, - let filterTokenRange = environment.template?.templateString.range(of: filterToken, range: containingToken.range) { - - syntaxError.lexeme = Token.block(value: filterToken, at: filterTokenRange) + if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil { + if let filterTokenRange = environment.template?.templateString.range(of: filterToken, range: containingToken.range) { + syntaxError.lexeme = Token.block(value: filterToken, at: filterTokenRange) + } else { + syntaxError.lexeme = containingToken + } throw syntaxError } else { throw error diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 3b33a15..62e99b5 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -38,7 +38,7 @@ func testEnvironment() { var error = TemplateSyntaxError(description) error.lexeme = Token.block(value: token, at: template.templateString.range(of: token)!) let context = ErrorReporterContext(template: template) - error = environment.errorReporter.contextAwareError(error, context: context) as! TemplateSyntaxError + error = environment.errorReporter.contextAwareError(error, at: error.lexeme?.range, context: context) as! TemplateSyntaxError return error } @@ -122,6 +122,86 @@ func testEnvironment() { let error = expectedFilterError(token: "name|unknown", template: template) try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) } + + } + + $0.context("given rendering error") { + $0.it("reports rendering error in variable filter") { + let template: Template = "{{ name|throw }}" + + var environment = environment + let filterExtension = Extension() + filterExtension.registerFilter("throw") { (value: Any?) in + throw TemplateSyntaxError("Filter rendering error") + } + environment.extensions += [filterExtension] + + let error = expectedSyntaxError(token: "name|throw", template: template, description: "Filter rendering error") + try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) + } + + $0.it("reports rendering error in filter tag") { + let template: Template = "{% filter throw %}Test{% endfilter %}" + + var environment = environment + let filterExtension = Extension() + filterExtension.registerFilter("throw") { (value: Any?) in + throw TemplateSyntaxError("Filter rendering error") + } + environment.extensions += [filterExtension] + + let error = expectedSyntaxError(token: "filter throw", template: template, description: "Filter rendering error") + try expect(try environment.renderTemplate(string: template.templateString, context: [:])).toThrow(error) + } + + $0.it("reports rendering error in simple tag") { + let template: Template = "{% simpletag %}" + + var environment = environment + let tagExtension = Extension() + tagExtension.registerSimpleTag("simpletag") { context in + throw TemplateSyntaxError("simpletag error") + } + environment.extensions += [tagExtension] + + let error = expectedSyntaxError(token: "simpletag", template: template, description: "simpletag error") + try expect(try environment.renderTemplate(string: template.templateString, context: [:])).toThrow(error) + } + + $0.it("reporsts passing argument to simple filter") { + let template: Template = "{{ name|uppercase:5 }}" + + let error = expectedSyntaxError(token: "name|uppercase:5", template: template, description: "cannot invoke filter with an argument") + try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "kyle"])).toThrow(error) + } + + $0.it("reports rendering error in custom tag") { + let template: Template = "{% customtag %}" + + var environment = environment + let tagExtension = Extension() + tagExtension.registerTag("customtag") { parser, token in + return ErrorNode(token: token) + } + environment.extensions += [tagExtension] + + let error = expectedSyntaxError(token: "customtag", template: template, description: "Custom Error") + try expect(try environment.renderTemplate(string: template.templateString, context: [:])).toThrow(error) + } + + $0.it("reports rendering error in for body") { + let template: Template = "{% for item in array %}{% customtag %}{% endfor %}" + + var environment = environment + let tagExtension = Extension() + tagExtension.registerTag("customtag") { parser, token in + return ErrorNode(token: token) + } + environment.extensions += [tagExtension] + + let error = expectedSyntaxError(token: "customtag", template: template, description: "Custom Error") + try expect(try environment.renderTemplate(string: template.templateString, context: ["array": ["a"]])).toThrow(error) + } } $0.context("given related templates") { diff --git a/Tests/StencilTests/NodeSpec.swift b/Tests/StencilTests/NodeSpec.swift index 431d225..1adfa26 100644 --- a/Tests/StencilTests/NodeSpec.swift +++ b/Tests/StencilTests/NodeSpec.swift @@ -3,6 +3,11 @@ import Spectre class ErrorNode : NodeType { + let token: Token? + init(token: Token? = nil) { + self.token = token + } + func render(_ context: Context) throws -> String { throw TemplateSyntaxError("Custom Error") } diff --git a/Tests/StencilTests/StencilSpec.swift b/Tests/StencilTests/StencilSpec.swift index 427cd98..099a407 100644 --- a/Tests/StencilTests/StencilSpec.swift +++ b/Tests/StencilTests/StencilSpec.swift @@ -2,7 +2,8 @@ import Spectre import Stencil -fileprivate class CustomNode : NodeType { +fileprivate struct CustomNode : NodeType { + let token: Token? func render(_ context:Context) throws -> String { return "Hello World" } @@ -24,7 +25,7 @@ func testStencil() { } exampleExtension.registerTag("customtag") { parser, token in - return CustomNode() + return CustomNode(token: token) } let environment = Environment(extensions: [exampleExtension]) From 9a28142fa6809190c38981550068b7557846e263 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sun, 24 Dec 2017 15:34:17 +0100 Subject: [PATCH 12/81] reporting error with its parent context --- Sources/Context.swift | 4 ++ Sources/Errors.swift | 84 +++++++++++++++++++----- Sources/Include.swift | 14 +++- Sources/Inheritence.swift | 18 +++-- Sources/Lexer.swift | 6 +- Tests/StencilTests/EnvironmentSpec.swift | 79 +++++++++++++++++----- 6 files changed, 161 insertions(+), 44 deletions(-) diff --git a/Sources/Context.swift b/Sources/Context.swift index 60bec17..a976e1e 100644 --- a/Sources/Context.swift +++ b/Sources/Context.swift @@ -3,6 +3,10 @@ public class Context { var dictionaries: [[String: Any?]] public let environment: Environment + + public var errorReporter: ErrorReporter { + return environment.errorReporter + } init(dictionary: [String: Any]? = nil, environment: Environment? = nil) { if let dictionary = dictionary { diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 32450cd..0cb40eb 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -19,15 +19,56 @@ public class TemplateDoesNotExist: Error, CustomStringConvertible { } public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { - public let description:String - var lexeme: Lexeme? + public let reason: String + public private(set) var description: String + + public internal(set) var template: Template? + public internal(set) var parentError: Error? + + var lexeme: Lexeme? { + didSet { + description = TemplateSyntaxError.description(reason: reason, lexeme: lexeme, template: template) + } + } - public init(_ description:String) { - self.description = description + static func description(reason: String, lexeme: Lexeme?, template: Template?) -> String { + if let template = template, let range = lexeme?.range { + let templateName = template.name.map({ "\($0):" }) ?? "" + let tokenContent = template.templateString.substring(with: range) + let line = template.templateString.rangeLine(range) + let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(tokenContent.length - 1, 0))))" + + return "\(templateName)\(line.number):\(line.offset): error: \(reason)\n" + + "\(line.content)\n" + + "\(highlight)\n" + } else { + return reason + } + } + + init(reason: String, lexeme: Lexeme? = nil, template: Template? = nil, parentError: Error? = nil) { + self.reason = reason + self.parentError = parentError + self.template = template + self.lexeme = lexeme + self.description = TemplateSyntaxError.description(reason: reason, lexeme: lexeme, template: template) + } + + public init(_ description: String) { + self.init(reason: description) } public static func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool { - return lhs.description == rhs.description + guard lhs.description == rhs.description else { return false } + + 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 + } } } @@ -47,7 +88,7 @@ public class ErrorReporterContext { public protocol ErrorReporter: class { var context: ErrorReporterContext! { get set } func reportError(_ error: Error) -> Error - func contextAwareError(_ error: Error, at range: Range?, context: ErrorReporterContext) -> Error? + func renderError(_ error: Error) -> String } open class SimpleErrorReporter: ErrorReporter { @@ -55,21 +96,28 @@ open class SimpleErrorReporter: ErrorReporter { open func reportError(_ error: Error) -> Error { guard let context = context else { return error } - return contextAwareError(error, at: (error as? TemplateSyntaxError)?.lexeme?.range, context: context) ?? error + + return TemplateSyntaxError(reason: (error as? TemplateSyntaxError)?.reason ?? "\(error)", + lexeme: (error as? TemplateSyntaxError)?.lexeme, + template: context.template, + parentError: (error as? TemplateSyntaxError)?.parentError + ) } - // TODO: add stack trace using parent context - open func contextAwareError(_ error: Error, at range: Range?, context: ErrorReporterContext) -> Error? { - guard let range = range, range != .unknown else { return nil } - let templateName = context.template.name.map({ "\($0):" }) ?? "" - let tokenContent = context.template.templateString.substring(with: range) - let line = context.template.templateString.rangeLine(range) - let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(tokenContent.length - 1, 0))))" - let description = "\(templateName)\(line.number):\(line.offset): error: \(error)\n\(line.content)\n\(highlight)" - let error = TemplateSyntaxError(description) - return error + open func renderError(_ error: Error) -> String { + guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription } + + var descriptions = [templateError.description] + + var currentError: TemplateSyntaxError? = templateError + while let parentError = currentError?.parentError { + descriptions.append(renderError(parentError)) + currentError = parentError as? TemplateSyntaxError + } + + return descriptions.reversed().joined(separator: "\n") } - + } extension Range where Bound == String.Index { diff --git a/Sources/Include.swift b/Sources/Include.swift index bb85a2d..58bde91 100644 --- a/Sources/Include.swift +++ b/Sources/Include.swift @@ -27,9 +27,17 @@ class IncludeNode : NodeType { let template = try context.environment.loadTemplate(name: templateName) - return try context.environment.pushTemplate(template, token: token) { - try context.push { - return try template.render(context) + do { + return try context.environment.pushTemplate(template, token: token) { + try context.push { + return try template.render(context) + } + } + } catch { + if let parentError = error as? TemplateSyntaxError { + throw TemplateSyntaxError(reason: parentError.reason, parentError: parentError) + } else { + throw error } } } diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index 189e581..7457c37 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -100,9 +100,17 @@ class ExtendsNode : NodeType { blockContext = BlockContext(blocks: blocks) } - return try context.environment.pushTemplate(template, token: token) { - try context.push(dictionary: [BlockContext.contextKey: blockContext]) { - return try template.render(context) + do { + return try context.environment.pushTemplate(template, token: token) { + try context.push(dictionary: [BlockContext.contextKey: blockContext]) { + return try template.render(context) + } + } + } catch { + if let parentError = error as? TemplateSyntaxError { + throw TemplateSyntaxError(reason: parentError.reason, parentError: parentError) + } else { + throw error } } } @@ -135,10 +143,12 @@ class BlockNode : NodeType { func render(_ context: Context) throws -> String { if let blockContext = context[BlockContext.contextKey] as? BlockContext, let node = blockContext.pop(name) { - let newContext: [String: Any] = [ + let newContext: [String: Any] + newContext = [ BlockContext.contextKey: blockContext, "block": ["super": try self.render(context)] ] + return try context.push(dictionary: newContext) { return try node.render(context) } diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index f2af91e..d9f972f 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -187,9 +187,9 @@ extension String { return String(self[first..) -> (content: String, number: Int, offset: String.IndexDistance) { - var lineNumber: Int = 0 - var offset = 0 + public func rangeLine(_ range: Range) -> (content: String, number: UInt, offset: String.IndexDistance) { + var lineNumber: UInt = 0 + var offset: Int = 0 var lineContent = "" for line in components(separatedBy: CharacterSet.newlines) { diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 62e99b5..738ed86 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -35,11 +35,8 @@ func testEnvironment() { } func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { - var error = TemplateSyntaxError(description) - error.lexeme = Token.block(value: token, at: template.templateString.range(of: token)!) - let context = ErrorReporterContext(template: template) - error = environment.errorReporter.contextAwareError(error, at: error.lexeme?.range, context: context) as! TemplateSyntaxError - return error + let lexeme = Token.block(value: token, at: template.templateString.range(of: token)!) + return TemplateSyntaxError(reason: description, lexeme: lexeme, template: template, parentError: nil) } $0.it("reports syntax error on invalid for tag syntax") { @@ -202,6 +199,20 @@ func testEnvironment() { let error = expectedSyntaxError(token: "customtag", template: template, description: "Custom Error") try expect(try environment.renderTemplate(string: template.templateString, context: ["array": ["a"]])).toThrow(error) } + + $0.it("reports rendering error in block") { + let template: Template = "{% block some %}{% customtag %}{% endblock %}" + + var environment = environment + let tagExtension = Extension() + tagExtension.registerTag("customtag") { parser, token in + return ErrorNode(token: token) + } + environment.extensions += [tagExtension] + + let error = expectedSyntaxError(token: "customtag", template: template, description: "Custom Error") + try expect(try environment.renderTemplate(string: template.templateString, context: ["array": ["a"]])).toThrow(error) + } } $0.context("given related templates") { @@ -210,25 +221,61 @@ func testEnvironment() { let environment = Environment(loader: loader) $0.it("reports syntax error in included template") { - let template: Template = "{% include \"invalid-include.html\"%}" - environment.errorReporter.context = ErrorReporterContext(template: template) - - let context = Context(dictionary: ["target": "World"], environment: environment) - + let template: Template = "{% include \"invalid-include.html\" %}" let includedTemplate = try environment.loadTemplate(name: "invalid-include.html") - let error = expectedSyntaxError(token: "target|unknown", template: includedTemplate, description: "Unknown filter 'unknown'") - try expect(try template.render(context)).toThrow(error) + let parentError = expectedSyntaxError(token: "target|unknown", template: includedTemplate, description: "Unknown filter 'unknown'") + var error = expectedSyntaxError(token: "include \"invalid-include.html\"", template: template, description: "Unknown filter 'unknown'") + error.parentError = parentError + + try expect(environment.renderTemplate(string: template.templateString, context: ["target": "World"])).toThrow(error) } $0.it("reports syntax error in extended template") { let template = try environment.loadTemplate(name: "invalid-child-super.html") - let context = Context(dictionary: ["target": "World"], environment: environment) - let baseTemplate = try environment.loadTemplate(name: "invalid-base.html") - let error = expectedSyntaxError(token: "target|unknown", template: baseTemplate, description: "Unknown filter 'unknown'") - try expect(try template.render(context)).toThrow(error) + let parentError = expectedSyntaxError(token: "target|unknown", template: baseTemplate, description: "Unknown filter 'unknown'") + var error = expectedSyntaxError(token: "extends \"invalid-base.html\"", template: template, description: "Unknown filter 'unknown'") + error.parentError = parentError + + try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) + } + + $0.it("reports runtime error in included template") { + var environment = environment + let filterExtension = Extension() + filterExtension.registerFilter("unknown", filter: { (_: Any?) in + throw TemplateSyntaxError("filter error") + }) + environment.extensions += [filterExtension] + + let template: Template = "{% include \"invalid-include.html\" %}" + let includedTemplate = try environment.loadTemplate(name: "invalid-include.html") + + let parentError = expectedSyntaxError(token: "target|unknown", template: includedTemplate, description: "filter error") + var error = expectedSyntaxError(token: "include \"invalid-include.html\"", template: template, description: "filter error") + error.parentError = parentError + + try expect(environment.renderTemplate(string: template.templateString, context: ["target": "World"])).toThrow(error) + } + + $0.it("reports runtime error in extended template") { + var environment = environment + let filterExtension = Extension() + filterExtension.registerFilter("throw", filter: { (_: Any?) in + throw TemplateSyntaxError("filter error") + }) + environment.extensions += [filterExtension] + + let template = try environment.loadTemplate(name: "invalid-runtime-child-super.html") + let baseTemplate = try environment.loadTemplate(name: "invalid-runtime-base.html") + + let parentError = expectedSyntaxError(token: "target|throw", template: baseTemplate, description: "filter error") + var error = expectedSyntaxError(token: "extends \"invalid-runtime-base.html\"", template: template, description: "filter error") + error.parentError = parentError + + try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) } } From c4866178541aa63bdda1e848cd9860b1619082e4 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 25 Dec 2017 00:34:13 +0100 Subject: [PATCH 13/81] fixed reporting errors in child templates --- Sources/Errors.swift | 2 +- Sources/Inheritence.swift | 79 +++++++++++++++--------- Tests/StencilTests/EnvironmentSpec.swift | 65 +++++++++++++------ 3 files changed, 95 insertions(+), 51 deletions(-) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 0cb40eb..ab5eda3 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -99,7 +99,7 @@ open class SimpleErrorReporter: ErrorReporter { return TemplateSyntaxError(reason: (error as? TemplateSyntaxError)?.reason ?? "\(error)", lexeme: (error as? TemplateSyntaxError)?.lexeme, - template: context.template, + template: (error as? TemplateSyntaxError)?.template ?? context.template, parentError: (error as? TemplateSyntaxError)?.parentError ) } diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index 7457c37..55f94ef 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -1,25 +1,23 @@ class BlockContext { class var contextKey: String { return "block_context" } - var blocks: [String: [BlockNode]] + // contains mapping of block names to their nodes and templates where they are defined + var blocks: [String: [(BlockNode, Template?)]] - init(blocks: [String: BlockNode]) { - self.blocks = [:] - blocks.forEach { (key, value) in - self.blocks[key] = [value] - } + init(blocks: [String: (BlockNode, Template?)]) { + self.blocks = blocks.mapValues({ [$0] }) } - func push(_ block: BlockNode, forKey blockName: String) { + func pushBlock(_ block: BlockNode, named blockName: String, definedIn template: Template?) { if var blocks = blocks[blockName] { - blocks.append(block) + blocks.append((block, template)) self.blocks[blockName] = blocks } else { - self.blocks[blockName] = [block] + self.blocks[blockName] = [(block, template)] } } - func pop(_ blockName: String) -> BlockNode? { + func popBlock(named blockName: String) -> (node: BlockNode, template: Template?)? { if var blocks = blocks[blockName] { let block = blocks.removeFirst() if blocks.isEmpty { @@ -87,28 +85,33 @@ class ExtendsNode : NodeType { throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string") } - let template = try context.environment.loadTemplate(name: templateName) - + let baseTemplate = try context.environment.loadTemplate(name: templateName) + let template = context.environment.template + let blockContext: BlockContext - if let context = context[BlockContext.contextKey] as? BlockContext { - blockContext = context - - for (key, value) in blocks { - blockContext.push(value, forKey: key) + if let _blockContext = context[BlockContext.contextKey] as? BlockContext { + blockContext = _blockContext + for (name, block) in blocks { + blockContext.pushBlock(block, named: name, definedIn: template) } } else { - blockContext = BlockContext(blocks: blocks) + blockContext = BlockContext(blocks: blocks.mapValues({ ($0, template) })) } do { - return try context.environment.pushTemplate(template, token: token) { + // pushes base template and renders it's content + // block_context contains all blocks from child templates + return try context.environment.pushTemplate(baseTemplate, token: token) { try context.push(dictionary: [BlockContext.contextKey: blockContext]) { - return try template.render(context) + return try baseTemplate.render(context) } } } catch { - if let parentError = error as? TemplateSyntaxError { - throw TemplateSyntaxError(reason: parentError.reason, parentError: parentError) + // if error template is already set (see catch in BlockNode) + // and it happend in the same template as current template + // there is no need to wrap it in another error + if let error = error as? TemplateSyntaxError, error.template !== context.environment.template { + throw TemplateSyntaxError(reason: error.reason, parentError: error) } else { throw error } @@ -142,15 +145,31 @@ class BlockNode : NodeType { } func render(_ context: Context) throws -> String { - if let blockContext = context[BlockContext.contextKey] as? BlockContext, let node = blockContext.pop(name) { + if let blockContext = context[BlockContext.contextKey] as? BlockContext, let child = blockContext.popBlock(named: name) { + // node is a block node from child template that extends this node (has the same name) let newContext: [String: Any] - newContext = [ - BlockContext.contextKey: blockContext, - "block": ["super": try self.render(context)] - ] - - return try context.push(dictionary: newContext) { - return try node.render(context) + newContext = [ + BlockContext.contextKey: blockContext, + // render current node so that it's content can be used as part of node that extends it + "block": ["super": try self.render(context)] + ] + // render extension node + do { + return try context.push(dictionary: newContext) { + return try child.node.render(context) + } + } 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 { + error.template = error.template ?? child.template + error.lexeme = error.lexeme ?? child.node.token + + throw error + } else { + throw error + } } } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 738ed86..4d98f7b 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -228,18 +228,8 @@ func testEnvironment() { var error = expectedSyntaxError(token: "include \"invalid-include.html\"", template: template, description: "Unknown filter 'unknown'") error.parentError = parentError - try expect(environment.renderTemplate(string: template.templateString, context: ["target": "World"])).toThrow(error) - } - - $0.it("reports syntax error in extended template") { - let template = try environment.loadTemplate(name: "invalid-child-super.html") - let baseTemplate = try environment.loadTemplate(name: "invalid-base.html") - - let parentError = expectedSyntaxError(token: "target|unknown", template: baseTemplate, description: "Unknown filter 'unknown'") - var error = expectedSyntaxError(token: "extends \"invalid-base.html\"", template: template, description: "Unknown filter 'unknown'") - error.parentError = parentError - try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) + try expect(environment.renderTemplate(string: template.templateString, context: ["target": "World"])).toThrow(error) } $0.it("reports runtime error in included template") { @@ -259,24 +249,59 @@ func testEnvironment() { try expect(environment.renderTemplate(string: template.templateString, context: ["target": "World"])).toThrow(error) } - - $0.it("reports runtime error in extended template") { + + $0.it("reports syntax error in base template") { + let template = try environment.loadTemplate(name: "invalid-child-super.html") + let baseTemplate = try environment.loadTemplate(name: "invalid-base.html") + + let parentError = expectedSyntaxError(token: "target|unknown", template: baseTemplate, description: "Unknown filter 'unknown'") + var error = expectedSyntaxError(token: "extends \"invalid-base.html\"", template: template, description: "Unknown filter 'unknown'") + error.parentError = parentError + + try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) + } + + $0.it("reports runtime error in base template") { var environment = environment let filterExtension = Extension() - filterExtension.registerFilter("throw", filter: { (_: Any?) in + filterExtension.registerFilter("unknown", filter: { (_: Any?) in + throw TemplateSyntaxError("filter error") + }) + environment.extensions += [filterExtension] + + let template = try environment.loadTemplate(name: "invalid-child-super.html") + let baseTemplate = try environment.loadTemplate(name: "invalid-base.html") + + let parentError = expectedSyntaxError(token: "target|unknown", template: baseTemplate, description: "filter error") + var error = expectedSyntaxError(token: "extends \"invalid-base.html\"", template: template, description: "filter error") + error.parentError = parentError + + try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) + } + + $0.it("reports syntax error in child template") { + let template = Template.init(templateString: "{% extends \"base.html\" %}\n" + + "{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil) + let error = expectedSyntaxError(token: "target|unknown", template: template, description: "Unknown filter 'unknown'") + + try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) + } + + $0.it("reports runtime error in child template") { + var environment = environment + let filterExtension = Extension() + filterExtension.registerFilter("unknown", filter: { (_: Any?) in throw TemplateSyntaxError("filter error") }) environment.extensions += [filterExtension] - let template = try environment.loadTemplate(name: "invalid-runtime-child-super.html") - let baseTemplate = try environment.loadTemplate(name: "invalid-runtime-base.html") - - let parentError = expectedSyntaxError(token: "target|throw", template: baseTemplate, description: "filter error") - var error = expectedSyntaxError(token: "extends \"invalid-runtime-base.html\"", template: template, description: "filter error") - error.parentError = parentError + let template = Template.init(templateString: "{% extends \"base.html\" %}\n" + + "{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil) + let error = expectedSyntaxError(token: "{{ target|unknown }}", template: template, description: "filter error") try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) } + } } From bb3f33724bad62e488887c1119ea6e41fac903b4 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 25 Dec 2017 01:10:58 +0100 Subject: [PATCH 14/81] unified setting higlighting range for errors --- Sources/Errors.swift | 21 ++++++++--------- Sources/Lexer.swift | 1 + Sources/Node.swift | 10 +++----- Tests/StencilTests/EnvironmentSpec.swift | 29 +++++------------------- 4 files changed, 19 insertions(+), 42 deletions(-) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index ab5eda3..76c7559 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -32,18 +32,15 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { } static func description(reason: String, lexeme: Lexeme?, template: Template?) -> String { - if let template = template, let range = lexeme?.range { - let templateName = template.name.map({ "\($0):" }) ?? "" - let tokenContent = template.templateString.substring(with: range) - let line = template.templateString.rangeLine(range) - let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(tokenContent.length - 1, 0))))" - - return "\(templateName)\(line.number):\(line.offset): error: \(reason)\n" - + "\(line.content)\n" - + "\(highlight)\n" - } else { - return reason - } + guard let template = template, let lexeme = lexeme else { return reason } + let templateName = template.name.map({ "\($0):" }) ?? "" + let range = template.templateString.range(of: lexeme.contents, range: lexeme.range) ?? lexeme.range + let line = template.templateString.rangeLine(range) + let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(lexeme.contents.length - 1, 0))))" + + return "\(templateName)\(line.number):\(line.offset): error: \(reason)\n" + + "\(line.content)\n" + + "\(highlight)\n" } init(reason: String, lexeme: Lexeme? = nil, template: Template? = nil, parentError: Error? = nil) { diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index d9f972f..9a7052b 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -77,6 +77,7 @@ struct Lexer { } protocol Lexeme { + var contents: String { get } var range: Range { get } } diff --git a/Sources/Node.swift b/Sources/Node.swift index 10ee16a..5656b79 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -15,13 +15,9 @@ public func renderNodes(_ nodes:[NodeType], _ context:Context) throws -> String do { return try $0.render(context) } catch { - if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil, let token = $0.token { - if let contentsRange = context.environment.template?.templateString.range(of: token.contents, range: token.range) { - syntaxError.lexeme = Token.block(value: token.contents, at: contentsRange) - } else { - syntaxError.lexeme = token - } - throw syntaxError + if var error = error as? TemplateSyntaxError { + error.lexeme = error.lexeme ?? $0.token + throw error } else { throw error } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 4d98f7b..67d2152 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -41,41 +41,25 @@ func testEnvironment() { $0.it("reports syntax error on invalid for tag syntax") { let template: Template = "Hello {% for name in %}{{ name }}, {% endfor %}!" - let error = expectedSyntaxError( - token: "{% for name in %}", - template: template, - description: "'for' statements should use the following syntax 'for x in y where condition'." - ) + let error = expectedSyntaxError(token: "for name in", template: template, description: "'for' statements should use the following syntax 'for x in y where condition'.") try expect(try environment.renderTemplate(string: template.templateString, context:["names": ["Bob", "Alice"]])).toThrow(error) } $0.it("reports syntax error on missing endfor") { let template: Template = "{% for name in names %}{{ name }}" - let error = expectedSyntaxError( - token: "{% for name in names %}", - template: template, - description: "`endfor` was not found." - ) + let error = expectedSyntaxError(token: "for name in names", template: template, description: "`endfor` was not found.") try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) } $0.it("reports syntax error on unknown tag") { let template: Template = "{% for name in names %}{{ name }}{% end %}" - let error = expectedSyntaxError( - token: "{% end %}", - template: template, - description: "Unknown template tag 'end'" - ) + let error = expectedSyntaxError(token: "end", template: template, description: "Unknown template tag 'end'") try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) } $0.context("given unknown filter") { func expectedFilterError(token: String, template: Template) -> TemplateSyntaxError { - return expectedSyntaxError( - token: token, - template: template, - description: "Unknown filter 'unknown'" - ) + return expectedSyntaxError(token: token, template: template, description: "Unknown filter 'unknown'") } $0.it("reports syntax error in for tag") { @@ -110,7 +94,7 @@ func testEnvironment() { $0.it("reports syntax error in filter tag") { let template: Template = "{% filter unknown %}Text{% endfilter %}" - let error = expectedFilterError(token: "{% filter unknown %}", template: template) + let error = expectedFilterError(token: "filter unknown", template: template) try expect(try environment.renderTemplate(string: template.templateString, context: [:])).toThrow(error) } @@ -228,7 +212,6 @@ func testEnvironment() { var error = expectedSyntaxError(token: "include \"invalid-include.html\"", template: template, description: "Unknown filter 'unknown'") error.parentError = parentError - try expect(environment.renderTemplate(string: template.templateString, context: ["target": "World"])).toThrow(error) } @@ -297,7 +280,7 @@ func testEnvironment() { let template = Template.init(templateString: "{% extends \"base.html\" %}\n" + "{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil) - let error = expectedSyntaxError(token: "{{ target|unknown }}", template: template, description: "filter error") + let error = expectedSyntaxError(token: "target|unknown", template: template, description: "filter error") try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) } From ea7e1efac72809ae3948a6721ae70d7636bc0be1 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 25 Dec 2017 01:55:37 +0100 Subject: [PATCH 15/81] fixed highlighting of errors happening in {{ block.super }} --- Sources/Inheritence.swift | 29 +++++++++++++++++------- Tests/StencilTests/EnvironmentSpec.swift | 2 +- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index 55f94ef..c143a22 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -146,13 +146,27 @@ class BlockNode : NodeType { func render(_ context: Context) throws -> String { if let blockContext = context[BlockContext.contextKey] as? BlockContext, let child = blockContext.popBlock(named: name) { - // node is a block node from child template that extends this node (has the same name) - let newContext: [String: Any] - newContext = [ - BlockContext.contextKey: blockContext, - // render current node so that it's content can be used as part of node that extends it - "block": ["super": try self.render(context)] - ] + // child node is a block node from child template that extends this node (has the same name) + + var newContext: [String: Any] = [BlockContext.contextKey: blockContext] + + if let blockSuperNode = child.node.nodes.first(where: { + if case .variable(let variable, _)? = $0.token, variable == "block.super" { return true } + else { return false} + }) { + do { + // 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)] + } catch { + let baseError = context.errorReporter.reportError(error) + throw TemplateSyntaxError( + reason: (baseError as? TemplateSyntaxError)?.reason ?? "\(baseError)", + lexeme: blockSuperNode.token, + template: child.template, + parentError: baseError) + } + } + // render extension node do { return try context.push(dictionary: newContext) { @@ -165,7 +179,6 @@ class BlockNode : NodeType { if var error = error as? TemplateSyntaxError { error.template = error.template ?? child.template error.lexeme = error.lexeme ?? child.node.token - throw error } else { throw error diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 67d2152..1b6fdb8 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -256,7 +256,7 @@ func testEnvironment() { let baseTemplate = try environment.loadTemplate(name: "invalid-base.html") let parentError = expectedSyntaxError(token: "target|unknown", template: baseTemplate, description: "filter error") - var error = expectedSyntaxError(token: "extends \"invalid-base.html\"", template: template, description: "filter error") + var error = expectedSyntaxError(token: "block.super", template: template, description: "filter error") error.parentError = parentError try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) From 218822fcb0482009e817e3bd80cc07ca182e9855 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 25 Dec 2017 01:56:46 +0100 Subject: [PATCH 16/81] updated CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce60106..95d780b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Added support for resolving superclass properties for not-NSObject subclasses - The `{% for %}` tag can now iterate over tuples, structures and classes via their stored properties. +- Drastic improvements in error reporting ### Bug Fixes From 8d68edd7258a64b1a665d2a99628d4a5f043b419 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Tue, 26 Dec 2017 15:28:46 +0100 Subject: [PATCH 17/81] replaced Lexeme protocol with Token --- Sources/Errors.swift | 54 +++++++++--------------- Sources/Inheritence.swift | 4 +- Sources/Lexer.swift | 13 ++---- Sources/Node.swift | 2 +- Sources/Parser.swift | 10 ++--- Sources/Tokenizer.swift | 2 +- Tests/StencilTests/EnvironmentSpec.swift | 4 +- 7 files changed, 35 insertions(+), 54 deletions(-) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 76c7559..d13a0ec 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -20,35 +20,16 @@ public class TemplateDoesNotExist: Error, CustomStringConvertible { public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { public let reason: String - public private(set) var description: String - + public var description: String { return reason } + public internal(set) var token: Token? public internal(set) var template: Template? public internal(set) var parentError: Error? - - var lexeme: Lexeme? { - didSet { - description = TemplateSyntaxError.description(reason: reason, lexeme: lexeme, template: template) - } - } - static func description(reason: String, lexeme: Lexeme?, template: Template?) -> String { - guard let template = template, let lexeme = lexeme else { return reason } - let templateName = template.name.map({ "\($0):" }) ?? "" - let range = template.templateString.range(of: lexeme.contents, range: lexeme.range) ?? lexeme.range - let line = template.templateString.rangeLine(range) - let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(lexeme.contents.length - 1, 0))))" - - return "\(templateName)\(line.number):\(line.offset): error: \(reason)\n" - + "\(line.content)\n" - + "\(highlight)\n" - } - - init(reason: String, lexeme: Lexeme? = nil, template: Template? = nil, parentError: Error? = nil) { + public init(reason: String, token: Token? = nil, template: Template? = nil, parentError: Error? = nil) { self.reason = reason self.parentError = parentError self.template = template - self.lexeme = lexeme - self.description = TemplateSyntaxError.description(reason: reason, lexeme: lexeme, template: template) + self.token = token } public init(_ description: String) { @@ -95,7 +76,7 @@ open class SimpleErrorReporter: ErrorReporter { guard let context = context else { return error } return TemplateSyntaxError(reason: (error as? TemplateSyntaxError)?.reason ?? "\(error)", - lexeme: (error as? TemplateSyntaxError)?.lexeme, + token: (error as? TemplateSyntaxError)?.token, template: (error as? TemplateSyntaxError)?.template ?? context.template, parentError: (error as? TemplateSyntaxError)?.parentError ) @@ -104,7 +85,21 @@ open class SimpleErrorReporter: ErrorReporter { open func renderError(_ error: Error) -> String { guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription } - var descriptions = [templateError.description] + let description: String + if let template = templateError.template, let token = templateError.token { + let templateName = template.name.map({ "\($0):" }) ?? "" + 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))))" + + description = "\(templateName)\(line.number):\(line.offset): error: \(templateError.reason)\n" + + "\(line.content)\n" + + "\(highlight)\n" + } else { + description = templateError.reason + } + + var descriptions = [description] var currentError: TemplateSyntaxError? = templateError while let parentError = currentError?.parentError { @@ -127,13 +122,4 @@ extension String { var range: Range { return startIndex..=3.2) - return count - #else - return characters.count - #endif - } - } diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index c143a22..9662540 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -161,7 +161,7 @@ class BlockNode : NodeType { let baseError = context.errorReporter.reportError(error) throw TemplateSyntaxError( reason: (baseError as? TemplateSyntaxError)?.reason ?? "\(baseError)", - lexeme: blockSuperNode.token, + token: blockSuperNode.token, template: child.template, parentError: baseError) } @@ -178,7 +178,7 @@ class BlockNode : NodeType { // unless it's already set if var error = error as? TemplateSyntaxError { error.template = error.template ?? child.template - error.lexeme = error.lexeme ?? child.node.token + error.token = error.token ?? child.node.token throw error } else { throw error diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 9a7052b..30d70f0 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -56,7 +56,7 @@ struct Lexer { return tokens } - func lexemeLine(_ lexeme: Lexeme) -> (content: String, number: Int, offset: String.IndexDistance) { + func tokenLine(_ token: Token) -> (content: String, number: Int, offset: String.IndexDistance) { var lineNumber: Int = 0 var offset = 0 var lineContent = "" @@ -64,9 +64,9 @@ struct Lexer { for line in templateString.components(separatedBy: CharacterSet.newlines) { lineNumber += 1 lineContent = line - if let rangeOfLine = templateString.range(of: line), rangeOfLine.contains(lexeme.range.lowerBound) { + if let rangeOfLine = templateString.range(of: line), rangeOfLine.contains(token.range.lowerBound) { offset = templateString.distance(from: rangeOfLine.lowerBound, to: - lexeme.range.lowerBound) + token.range.lowerBound) break } } @@ -76,11 +76,6 @@ struct Lexer { } -protocol Lexeme { - var contents: String { get } - var range: Range { get } -} - class Scanner { let originalContent: String var content: String @@ -111,7 +106,7 @@ class Scanner { let result = content.substring(to: index) if returnUntil { - range = range.lowerBound.. String return try $0.render(context) } catch { if var error = error as? TemplateSyntaxError { - error.lexeme = error.lexeme ?? $0.token + error.token = error.token ?? $0.token throw error } else { throw error diff --git a/Sources/Parser.swift b/Sources/Parser.swift index a4d2d88..dc42f91 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -54,8 +54,8 @@ public class TokenParser { let node = try parser(self, token) nodes.append(node) } catch { - if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil { - syntaxError.lexeme = token + if var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil { + syntaxError.token = token throw syntaxError } else { throw error @@ -106,11 +106,11 @@ public class TokenParser { do { return try FilterExpression(token: filterToken, parser: self) } catch { - if var syntaxError = error as? TemplateSyntaxError, syntaxError.lexeme == nil { + if var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil { if let filterTokenRange = environment.template?.templateString.range(of: filterToken, range: containingToken.range) { - syntaxError.lexeme = Token.block(value: filterToken, at: filterTokenRange) + syntaxError.token = Token.block(value: filterToken, at: filterTokenRange) } else { - syntaxError.lexeme = containingToken + syntaxError.token = containingToken } throw syntaxError } else { diff --git a/Sources/Tokenizer.swift b/Sources/Tokenizer.swift index 60f621b..55482ed 100644 --- a/Sources/Tokenizer.swift +++ b/Sources/Tokenizer.swift @@ -40,7 +40,7 @@ extension String { } } -public enum Token : Equatable, Lexeme { +public enum Token : Equatable { /// A token representing a piece of text. case text(value: String, at: Range) diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 1b6fdb8..0e20e76 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -35,8 +35,8 @@ func testEnvironment() { } func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { - let lexeme = Token.block(value: token, at: template.templateString.range(of: token)!) - return TemplateSyntaxError(reason: description, lexeme: lexeme, template: template, parentError: nil) + let token = Token.block(value: token, at: template.templateString.range(of: token)!) + return TemplateSyntaxError(reason: description, token: token, template: template, parentError: nil) } $0.it("reports syntax error on invalid for tag syntax") { From 7756522317d8d076410f7a530fa5d7ac9da66b29 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Tue, 26 Dec 2017 15:51:17 +0100 Subject: [PATCH 18/81] fixed error on swift 3.1 --- Sources/Inheritence.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index 9662540..dba09cf 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -4,8 +4,8 @@ class BlockContext { // contains mapping of block names to their nodes and templates where they are defined var blocks: [String: [(BlockNode, Template?)]] - init(blocks: [String: (BlockNode, Template?)]) { - self.blocks = blocks.mapValues({ [$0] }) + init(blocks: [String: [(BlockNode, Template?)]]) { + self.blocks = blocks } func pushBlock(_ block: BlockNode, named blockName: String, definedIn template: Template?) { @@ -95,7 +95,9 @@ class ExtendsNode : NodeType { blockContext.pushBlock(block, named: name, definedIn: template) } } else { - blockContext = BlockContext(blocks: blocks.mapValues({ ($0, template) })) + var blocks = [String: [(BlockNode, Template?)]]() + self.blocks.forEach { blocks[$0.key] = [($0.value, template)] } + blockContext = BlockContext(blocks: blocks) } do { From ed885f462a3865c28f167f138ce444cfe63c8cfe Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Tue, 26 Dec 2017 20:48:43 +0100 Subject: [PATCH 19/81] refactored environment tests --- Tests/StencilTests/EnvironmentSpec.swift | 299 +++++++++++++---------- 1 file changed, 172 insertions(+), 127 deletions(-) diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 0e20e76..24f2136 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -5,7 +5,13 @@ import PathKit func testEnvironment() { describe("Environment") { - let environment = Environment(loader: ExampleLoader()) + var environment: Environment! + var template: Template! + + $0.before { + environment = Environment(loader: ExampleLoader()) + template = nil + } $0.it("can load a template from a name") { let template = try environment.loadTemplate(name: "example.html") @@ -33,256 +39,270 @@ func testEnvironment() { try expect(result) == "here" } - + func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { let token = Token.block(value: token, at: template.templateString.range(of: token)!) return TemplateSyntaxError(reason: description, token: token, template: template, parentError: nil) } - - $0.it("reports syntax error on invalid for tag syntax") { - let template: Template = "Hello {% for name in %}{{ name }}, {% endfor %}!" - let error = expectedSyntaxError(token: "for name in", template: template, description: "'for' statements should use the following syntax 'for x in y where condition'.") - try expect(try environment.renderTemplate(string: template.templateString, context:["names": ["Bob", "Alice"]])).toThrow(error) + + func expectError(reason: String, token: String) throws { + let expectedError = expectedSyntaxError(token: token, template: template, description: reason) + + let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"])) + .toThrow(expectedError) + try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError) } - - $0.it("reports syntax error on missing endfor") { - let template: Template = "{% for name in names %}{{ name }}" - let error = expectedSyntaxError(token: "for name in names", template: template, description: "`endfor` was not found.") - try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) - } - - $0.it("reports syntax error on unknown tag") { - let template: Template = "{% for name in names %}{{ name }}{% end %}" - let error = expectedSyntaxError(token: "end", template: template, description: "Unknown template tag 'end'") - try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) + + $0.context("given syntax error") { + + $0.it("reports syntax error on invalid for tag syntax") { + template = "Hello {% for name in %}{{ name }}, {% endfor %}!" + try expectError(reason: "'for' statements should use the following syntax 'for x in y where condition'.", token: "for name in") + } + + $0.it("reports syntax error on missing endfor") { + template = "{% for name in names %}{{ name }}" + try expectError(reason: "`endfor` was not found.", token: "for name in names") + } + + $0.it("reports syntax error on unknown tag") { + template = "{% for name in names %}{{ name }}{% end %}" + try expectError(reason: "Unknown template tag 'end'", token: "end") + } + } $0.context("given unknown filter") { - func expectedFilterError(token: String, template: Template) -> TemplateSyntaxError { - return expectedSyntaxError(token: token, template: template, description: "Unknown filter 'unknown'") - } $0.it("reports syntax error in for tag") { - let template: Template = "{% for name in names|unknown %}{{ name }}{% endfor %}" - let error = expectedFilterError(token: "names|unknown", template: template) - try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) + template = "{% for name in names|unknown %}{{ name }}{% endfor %}" + try expectError(reason: "Unknown filter 'unknown'", token: "names|unknown") } $0.it("reports syntax error in for-where tag") { - let template: Template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}" - let error = expectedFilterError(token: "name|unknown", template: template) - try expect(try environment.renderTemplate(string: template.templateString, context: ["names": ["Bob", "Alice"]])).toThrow(error) + template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}" + try expectError(reason: "Unknown filter 'unknown'", token: "name|unknown") } $0.it("reports syntax error in if tag") { - let template: Template = "{% if name|unknown %}{{ name }}{% endif %}" - let error = expectedFilterError(token: "name|unknown", template: template) - try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) + template = "{% if name|unknown %}{{ name }}{% endif %}" + try expectError(reason: "Unknown filter 'unknown'", token: "name|unknown") } $0.it("reports syntax error in elif tag") { - let template: Template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}" - let error = expectedFilterError(token: "name|unknown", template: template) - try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) + template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}" + try expectError(reason: "Unknown filter 'unknown'", token: "name|unknown") } $0.it("reports syntax error in ifnot tag") { - let template: Template = "{% ifnot name|unknown %}{{ name }}{% endif %}" - let error = expectedFilterError(token: "name|unknown", template: template) - try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) + template = "{% ifnot name|unknown %}{{ name }}{% endif %}" + try expectError(reason: "Unknown filter 'unknown'", token: "name|unknown") } $0.it("reports syntax error in filter tag") { - let template: Template = "{% filter unknown %}Text{% endfilter %}" - let error = expectedFilterError(token: "filter unknown", template: template) - try expect(try environment.renderTemplate(string: template.templateString, context: [:])).toThrow(error) + template = "{% filter unknown %}Text{% endfilter %}" + try expectError(reason: "Unknown filter 'unknown'", token: "filter unknown") } $0.it("reports syntax error in variable tag") { - let template: Template = "{{ name|unknown }}" - let error = expectedFilterError(token: "name|unknown", template: template) - try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) + template = "{{ name|unknown }}" + try expectError(reason: "Unknown filter 'unknown'", token: "name|unknown") } } $0.context("given rendering error") { + $0.it("reports rendering error in variable filter") { - let template: Template = "{{ name|throw }}" - - var environment = environment let filterExtension = Extension() filterExtension.registerFilter("throw") { (value: Any?) in - throw TemplateSyntaxError("Filter rendering error") + throw TemplateSyntaxError("filter error") } environment.extensions += [filterExtension] - - let error = expectedSyntaxError(token: "name|throw", template: template, description: "Filter rendering error") - try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "Bob"])).toThrow(error) + + template = Template(templateString: "{{ name|throw }}", environment: environment) + try expectError(reason: "filter error", token: "name|throw") } $0.it("reports rendering error in filter tag") { - let template: Template = "{% filter throw %}Test{% endfilter %}" - - var environment = environment let filterExtension = Extension() filterExtension.registerFilter("throw") { (value: Any?) in - throw TemplateSyntaxError("Filter rendering error") + throw TemplateSyntaxError("filter error") } environment.extensions += [filterExtension] - - let error = expectedSyntaxError(token: "filter throw", template: template, description: "Filter rendering error") - try expect(try environment.renderTemplate(string: template.templateString, context: [:])).toThrow(error) + + template = Template(templateString: "{% filter throw %}Test{% endfilter %}", environment: environment) + try expectError(reason: "filter error", token: "filter throw") } $0.it("reports rendering error in simple tag") { - let template: Template = "{% simpletag %}" - - var environment = environment let tagExtension = Extension() tagExtension.registerSimpleTag("simpletag") { context in throw TemplateSyntaxError("simpletag error") } environment.extensions += [tagExtension] - - let error = expectedSyntaxError(token: "simpletag", template: template, description: "simpletag error") - try expect(try environment.renderTemplate(string: template.templateString, context: [:])).toThrow(error) + + template = Template(templateString: "{% simpletag %}", environment: environment) + try expectError(reason: "simpletag error", token: "simpletag") } $0.it("reporsts passing argument to simple filter") { - let template: Template = "{{ name|uppercase:5 }}" - - let error = expectedSyntaxError(token: "name|uppercase:5", template: template, description: "cannot invoke filter with an argument") - try expect(try environment.renderTemplate(string: template.templateString, context: ["name": "kyle"])).toThrow(error) + template = "{{ name|uppercase:5 }}" + try expectError(reason: "cannot invoke filter with an argument", token: "name|uppercase:5") } $0.it("reports rendering error in custom tag") { - let template: Template = "{% customtag %}" - - var environment = environment let tagExtension = Extension() tagExtension.registerTag("customtag") { parser, token in return ErrorNode(token: token) } environment.extensions += [tagExtension] - let error = expectedSyntaxError(token: "customtag", template: template, description: "Custom Error") - try expect(try environment.renderTemplate(string: template.templateString, context: [:])).toThrow(error) + template = Template(templateString: "{% customtag %}", environment: environment) + try expectError(reason: "Custom Error", token: "customtag") } $0.it("reports rendering error in for body") { - let template: Template = "{% for item in array %}{% customtag %}{% endfor %}" - - var environment = environment let tagExtension = Extension() tagExtension.registerTag("customtag") { parser, token in return ErrorNode(token: token) } environment.extensions += [tagExtension] - - let error = expectedSyntaxError(token: "customtag", template: template, description: "Custom Error") - try expect(try environment.renderTemplate(string: template.templateString, context: ["array": ["a"]])).toThrow(error) + + template = Template(templateString: "{% for name in names %}{% customtag %}{% endfor %}", environment: environment) + try expectError(reason: "Custom Error", token: "customtag") } $0.it("reports rendering error in block") { - let template: Template = "{% block some %}{% customtag %}{% endblock %}" - - var environment = environment let tagExtension = Extension() tagExtension.registerTag("customtag") { parser, token in return ErrorNode(token: token) } environment.extensions += [tagExtension] - - let error = expectedSyntaxError(token: "customtag", template: template, description: "Custom Error") - try expect(try environment.renderTemplate(string: template.templateString, context: ["array": ["a"]])).toThrow(error) + + template = Template(templateString: "{% block some %}{% customtag %}{% endblock %}", environment: environment) + try expectError(reason: "Custom Error", token: "customtag") } } - $0.context("given related templates") { + $0.context("given included template") { let path = Path(#file) + ".." + "fixtures" let loader = FileSystemLoader(paths: [path]) - let environment = Environment(loader: loader) + var environment = Environment(loader: loader) + var template: Template! + var includedTemplate: Template! + + $0.before { + environment = Environment(loader: loader) + template = nil + includedTemplate = nil + } + + func expectError(reason: String, token: String, includedToken: String) throws { + var expectedError = expectedSyntaxError(token: token, template: template, description: reason) + expectedError.parentError = expectedSyntaxError(token: includedToken, template: includedTemplate, description: reason) + + let error = try expect(environment.render(template: template, context: ["target": "World"])) + .toThrow(expectedError) + try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError) + } $0.it("reports syntax error in included template") { - let template: Template = "{% include \"invalid-include.html\" %}" - let includedTemplate = try environment.loadTemplate(name: "invalid-include.html") - - let parentError = expectedSyntaxError(token: "target|unknown", template: includedTemplate, description: "Unknown filter 'unknown'") - var error = expectedSyntaxError(token: "include \"invalid-include.html\"", template: template, description: "Unknown filter 'unknown'") - error.parentError = parentError - - try expect(environment.renderTemplate(string: template.templateString, context: ["target": "World"])).toThrow(error) + template = Template(templateString: "{% include \"invalid-include.html\" %}", environment: environment) + includedTemplate = try environment.loadTemplate(name: "invalid-include.html") + + try expectError(reason: "Unknown filter 'unknown'", + token: "include \"invalid-include.html\"", + includedToken: "target|unknown") } $0.it("reports runtime error in included template") { - var environment = environment let filterExtension = Extension() filterExtension.registerFilter("unknown", filter: { (_: Any?) in throw TemplateSyntaxError("filter error") }) environment.extensions += [filterExtension] - let template: Template = "{% include \"invalid-include.html\" %}" - let includedTemplate = try environment.loadTemplate(name: "invalid-include.html") + template = Template(templateString: "{% include \"invalid-include.html\" %}", environment: environment) + includedTemplate = try environment.loadTemplate(name: "invalid-include.html") + + try expectError(reason: "filter error", + token: "include \"invalid-include.html\"", + includedToken: "target|unknown") + } + + } + + $0.context("given base and child templates") { + let path = Path(#file) + ".." + "fixtures" + let loader = FileSystemLoader(paths: [path]) + var environment: Environment! + var childTemplate: Template! + var baseTemplate: Template! + + $0.before { + environment = Environment(loader: loader) + childTemplate = nil + baseTemplate = nil + } + + func expectError(reason: String, childToken: String, baseToken: String?) throws { + var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason) + if let baseToken = baseToken { + expectedError.parentError = expectedSyntaxError(token: baseToken, template: baseTemplate, description: reason) + } - let parentError = expectedSyntaxError(token: "target|unknown", template: includedTemplate, description: "filter error") - var error = expectedSyntaxError(token: "include \"invalid-include.html\"", template: template, description: "filter error") - error.parentError = parentError - - try expect(environment.renderTemplate(string: template.templateString, context: ["target": "World"])).toThrow(error) + let error = try expect(environment.render(template: childTemplate, context: ["target": "World"])) + .toThrow(expectedError) + try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError) } $0.it("reports syntax error in base template") { - let template = try environment.loadTemplate(name: "invalid-child-super.html") - let baseTemplate = try environment.loadTemplate(name: "invalid-base.html") - - let parentError = expectedSyntaxError(token: "target|unknown", template: baseTemplate, description: "Unknown filter 'unknown'") - var error = expectedSyntaxError(token: "extends \"invalid-base.html\"", template: template, description: "Unknown filter 'unknown'") - error.parentError = parentError - - try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) + childTemplate = try environment.loadTemplate(name: "invalid-child-super.html") + baseTemplate = try environment.loadTemplate(name: "invalid-base.html") + + try expectError(reason: "Unknown filter 'unknown'", + childToken: "extends \"invalid-base.html\"", + baseToken: "target|unknown") } - + $0.it("reports runtime error in base template") { - var environment = environment let filterExtension = Extension() filterExtension.registerFilter("unknown", filter: { (_: Any?) in throw TemplateSyntaxError("filter error") }) environment.extensions += [filterExtension] - let template = try environment.loadTemplate(name: "invalid-child-super.html") - let baseTemplate = try environment.loadTemplate(name: "invalid-base.html") + childTemplate = try environment.loadTemplate(name: "invalid-child-super.html") + baseTemplate = try environment.loadTemplate(name: "invalid-base.html") - let parentError = expectedSyntaxError(token: "target|unknown", template: baseTemplate, description: "filter error") - var error = expectedSyntaxError(token: "block.super", template: template, description: "filter error") - error.parentError = parentError - - try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) + try expectError(reason: "filter error", + childToken: "block.super", + baseToken: "target|unknown") } $0.it("reports syntax error in child template") { - let template = Template.init(templateString: "{% extends \"base.html\" %}\n" + + childTemplate = Template(templateString: "{% extends \"base.html\" %}\n" + "{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil) - let error = expectedSyntaxError(token: "target|unknown", template: template, description: "Unknown filter 'unknown'") - try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) + try expectError(reason: "Unknown filter 'unknown'", + childToken: "target|unknown", + baseToken: nil) } $0.it("reports runtime error in child template") { - var environment = environment let filterExtension = Extension() filterExtension.registerFilter("unknown", filter: { (_: Any?) in throw TemplateSyntaxError("filter error") }) environment.extensions += [filterExtension] - - let template = Template.init(templateString: "{% extends \"base.html\" %}\n" + + + childTemplate = Template(templateString: "{% extends \"base.html\" %}\n" + "{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil) - let error = expectedSyntaxError(token: "target|unknown", template: template, description: "filter error") - - try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) + + try expectError(reason: "filter error", + childToken: "target|unknown", + baseToken: nil) } } @@ -290,6 +310,31 @@ func testEnvironment() { } } +private extension Expectation { + @discardableResult + func toThrow(_ error: T) throws -> T { + var thrownError: Error? = nil + + do { + _ = try expression() + } catch { + thrownError = error + } + + if let thrownError = thrownError { + if let thrownError = thrownError as? T { + if error != thrownError { + throw failure("\(thrownError) is not \(error)") + } + return thrownError + } else { + throw failure("\(thrownError) is not \(error)") + } + } else { + throw failure("expression did not throw an error") + } + } +} fileprivate class ExampleLoader: Loader { func loadTemplate(name: String, environment: Environment) throws -> Template { From abeb30bb1c6c13c814a03e7a638e4975cd683d02 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Tue, 26 Dec 2017 21:20:09 +0100 Subject: [PATCH 20/81] fix rendering templates created from string literals --- Sources/Environment.swift | 2 ++ Sources/Template.swift | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/Environment.swift b/Sources/Environment.swift index aafab8a..ce2d2c5 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -43,6 +43,8 @@ public struct Environment { } func render(template: Template, context: [String: Any]?) throws -> String { + // update temaplte environment as it cen be created from string literal with default environment + template.environment = self errorReporter.context = ErrorReporterContext(template: template) do { return try template.render(context) diff --git a/Sources/Template.swift b/Sources/Template.swift index db570e0..03176b2 100644 --- a/Sources/Template.swift +++ b/Sources/Template.swift @@ -8,7 +8,7 @@ let NSFileNoSuchFileError = 4 /// A class representing a template open class Template: ExpressibleByStringLiteral { let templateString: String - let environment: Environment + internal(set) var environment: Environment let tokens: [Token] /// The name of the loaded Template if the Template was loaded from a Loader From cb124319ec32e0c79dd38c25c532662e4368a9ba Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Tue, 26 Dec 2017 21:32:28 +0100 Subject: [PATCH 21/81] removed unneeded changes --- Sources/ForTag.swift | 19 ++++++++++--------- Sources/IfTag.swift | 14 +++++++------- Sources/Lexer.swift | 18 ------------------ Sources/Parser.swift | 15 ++++++++------- 4 files changed, 25 insertions(+), 41 deletions(-) diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index 8e3f5c9..fcb5d68 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -28,21 +28,22 @@ class ForNode : NodeType { let forNodes = try parser.parse(until(["endfor", "empty"])) - if let token = parser.nextToken() { - if token.contents == "empty" { - emptyNodes = try parser.parse(until(["endfor"])) - _ = parser.nextToken() - } - } else { - throw TemplateSyntaxError("`endfor` was not found.") - } - let `where`: Expression? if components.count >= 6 { `where` = try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser, token: token) } else { `where` = nil } + + guard let token = parser.nextToken() else { + throw TemplateSyntaxError("`endfor` was not found.") + } + + if token.contents == "empty" { + emptyNodes = try parser.parse(until(["endfor"])) + _ = parser.nextToken() + } + return ForNode(resolvable: filter, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`, token: token) } diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index 39a3606..07e08f9 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -226,18 +226,18 @@ class IfNode : NodeType { var trueNodes = [NodeType]() var falseNodes = [NodeType]() + let expression = try parseExpression(components: components, tokenParser: parser, token: token) falseNodes = try parser.parse(until(["endif", "else"])) - if let token = parser.nextToken() { - if token.contents == "else" { - trueNodes = try parser.parse(until(["endif"])) - _ = parser.nextToken() - } - } else { + guard let token = parser.nextToken() else { throw TemplateSyntaxError("`endif` was not found.") } - let expression = try parseExpression(components: components, tokenParser: parser, token: token) + if token.contents == "else" { + trueNodes = try parser.parse(until(["endif"])) + _ = parser.nextToken() + } + return IfNode(conditions: [ IfCondition(expression: expression, nodes: trueNodes), IfCondition(expression: nil, nodes: falseNodes), diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 30d70f0..c6cc15c 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -56,24 +56,6 @@ struct Lexer { return tokens } - func tokenLine(_ token: Token) -> (content: String, number: Int, offset: String.IndexDistance) { - var lineNumber: Int = 0 - var offset = 0 - var lineContent = "" - - for line in templateString.components(separatedBy: CharacterSet.newlines) { - lineNumber += 1 - lineContent = line - if let rangeOfLine = templateString.range(of: line), rangeOfLine.contains(token.range.lowerBound) { - offset = templateString.distance(from: rangeOfLine.lowerBound, to: - token.range.lowerBound) - break - } - } - - return (lineContent, lineNumber, offset) - } - } class Scanner { diff --git a/Sources/Parser.swift b/Sources/Parser.swift index dc42f91..e2a583c 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -54,9 +54,9 @@ public class TokenParser { let node = try parser(self, token) nodes.append(node) } catch { - if var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil { - syntaxError.token = token - throw syntaxError + if var error = error as? TemplateSyntaxError { + error.token = error.token ?? token + throw error } else { throw error } @@ -106,13 +106,14 @@ public class TokenParser { do { return try FilterExpression(token: filterToken, parser: self) } catch { - if var syntaxError = error as? TemplateSyntaxError, syntaxError.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 if let filterTokenRange = environment.template?.templateString.range(of: filterToken, range: containingToken.range) { - syntaxError.token = Token.block(value: filterToken, at: filterTokenRange) + error.token = Token.variable(value: filterToken, at: filterTokenRange) } else { - syntaxError.token = containingToken + error.token = containingToken } - throw syntaxError + throw error } else { throw error } From ac2fd56e8e8e52ec47eccce56bf8638f6ff2e31c Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Wed, 27 Dec 2017 02:31:47 +0100 Subject: [PATCH 22/81] storing full sourcemap in token, refactored error reporting --- Sources/Environment.swift | 25 +------- Sources/Errors.swift | 73 ++++++------------------ Sources/Include.swift | 10 ++-- Sources/Inheritence.swift | 56 +++++++++--------- Sources/Lexer.swift | 32 +++++++++-- Sources/Parser.swift | 8 ++- Sources/Template.swift | 2 +- Sources/Tokenizer.swift | 54 ++++++++++++------ Tests/StencilTests/EnvironmentSpec.swift | 26 ++++----- Tests/StencilTests/FilterSpec.swift | 4 +- Tests/StencilTests/ForNodeSpec.swift | 6 +- Tests/StencilTests/IfNodeSpec.swift | 12 ++-- Tests/StencilTests/IncludeSpec.swift | 2 +- Tests/StencilTests/LexerSpec.swift | 20 +++---- Tests/StencilTests/ParserSpec.swift | 7 +-- 15 files changed, 149 insertions(+), 188 deletions(-) diff --git a/Sources/Environment.swift b/Sources/Environment.swift index ce2d2c5..5fb9e19 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -45,30 +45,7 @@ public struct Environment { func render(template: Template, context: [String: Any]?) throws -> String { // update temaplte environment as it cen be created from string literal with default environment template.environment = self - errorReporter.context = ErrorReporterContext(template: template) - do { - return try template.render(context) - } catch { - throw errorReporter.reportError(error) - } - } - - var template: Template? { - return errorReporter.context?.template - } - - public func pushTemplate(_ 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) - } + return try template.render(context) } } diff --git a/Sources/Errors.swift b/Sources/Errors.swift index d13a0ec..4b7cafe 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -22,13 +22,15 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { public let reason: String public var description: String { return reason } public internal(set) var token: Token? - public internal(set) var template: Template? - public internal(set) var parentError: Error? + public internal(set) var stackTrace: [Token] + 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.parentError = parentError - self.template = template + self.stackTrace = stackTrace self.token = token } @@ -37,77 +39,34 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { } public static func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool { - guard lhs.description == rhs.description else { return false } - - 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 - } + return lhs.description == rhs.description && lhs.token == rhs.token && lhs.stackTrace == rhs.stackTrace } } -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 { - var context: ErrorReporterContext! { get set } - func reportError(_ error: Error) -> Error func renderError(_ error: Error) -> String } 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 { guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription } - let description: String - if let template = templateError.template, let token = templateError.token { - let templateName = template.name.map({ "\($0):" }) ?? "" - let range = template.templateString.range(of: token.contents, range: token.range) ?? token.range - let line = template.templateString.rangeLine(range) + func describe(token: Token) -> String { + let templateName = token.sourceMap.filename ?? "" + let line = token.sourceMap.line 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" + "\(highlight)\n" - } else { - description = templateError.reason } - var descriptions = [description] - - var currentError: TemplateSyntaxError? = templateError - while let parentError = currentError?.parentError { - descriptions.append(renderError(parentError)) - currentError = parentError as? TemplateSyntaxError - } - - return descriptions.reversed().joined(separator: "\n") + var descriptions = templateError.stackTrace.reduce([]) { $0 + [describe(token: $1)] } + let description = templateError.token.map(describe(token:)) ?? templateError.reason + descriptions.append(description) + return descriptions.joined(separator: "\n") } } diff --git a/Sources/Include.swift b/Sources/Include.swift index 58bde91..8ebec9f 100644 --- a/Sources/Include.swift +++ b/Sources/Include.swift @@ -28,14 +28,12 @@ class IncludeNode : NodeType { let template = try context.environment.loadTemplate(name: templateName) do { - return try context.environment.pushTemplate(template, token: token) { - try context.push { - return try template.render(context) - } + return try context.push { + return try template.render(context) } } catch { - if let parentError = error as? TemplateSyntaxError { - throw TemplateSyntaxError(reason: parentError.reason, parentError: parentError) + if let error = error as? TemplateSyntaxError { + throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens) } else { throw error } diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index dba09cf..8cb7db2 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -2,22 +2,22 @@ class BlockContext { class var contextKey: String { return "block_context" } // 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 } - func pushBlock(_ block: BlockNode, named blockName: String, definedIn template: Template?) { + func pushBlock(_ block: BlockNode, named blockName: String) { if var blocks = blocks[blockName] { - blocks.append((block, template)) + blocks.append(block) self.blocks[blockName] = blocks } 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] { let block = blocks.removeFirst() if blocks.isEmpty { @@ -86,34 +86,31 @@ class ExtendsNode : NodeType { } let baseTemplate = try context.environment.loadTemplate(name: templateName) - let template = context.environment.template let blockContext: BlockContext if let _blockContext = context[BlockContext.contextKey] as? BlockContext { blockContext = _blockContext for (name, block) in blocks { - blockContext.pushBlock(block, named: name, definedIn: template) + blockContext.pushBlock(block, named: name) } } else { - var blocks = [String: [(BlockNode, Template?)]]() - self.blocks.forEach { blocks[$0.key] = [($0.value, template)] } + var blocks = [String: [BlockNode]]() + self.blocks.forEach { blocks[$0.key] = [$0.value] } blockContext = BlockContext(blocks: blocks) } do { // pushes base template and renders it's content // block_context contains all blocks from child templates - return try context.environment.pushTemplate(baseTemplate, token: token) { - try context.push(dictionary: [BlockContext.contextKey: blockContext]) { - return try baseTemplate.render(context) - } + return try context.push(dictionary: [BlockContext.contextKey: blockContext]) { + return try baseTemplate.render(context) } } catch { // if error template is already set (see catch in BlockNode) // and it happend in the same template as current template // there is no need to wrap it in another error - if let error = error as? TemplateSyntaxError, error.template !== context.environment.template { - throw TemplateSyntaxError(reason: error.reason, parentError: error) + if let error = error as? TemplateSyntaxError, error.templateName != token?.sourceMap.filename { + throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens) } else { throw error } @@ -152,7 +149,7 @@ class BlockNode : NodeType { 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 } 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 newContext["block"] = ["super": try self.render(context)] } catch { - let baseError = context.errorReporter.reportError(error) - throw TemplateSyntaxError( - reason: (baseError as? TemplateSyntaxError)?.reason ?? "\(baseError)", - token: blockSuperNode.token, - template: child.template, - parentError: baseError) + if let error = error as? TemplateSyntaxError { + throw TemplateSyntaxError( + reason: error.reason, + token: blockSuperNode.token, + stackTrace: error.allTokens) + } else { + throw TemplateSyntaxError( + reason: "\(error)", + token: blockSuperNode.token, + stackTrace: []) + } } } // render extension node do { return try context.push(dictionary: newContext) { - return try child.node.render(context) + return try child.render(context) } } 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 { - error.template = error.template ?? child.template - error.token = error.token ?? child.node.token + error.token = error.token ?? child.token throw error } else { throw error diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index c6cc15c..caebac4 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -1,9 +1,11 @@ import Foundation struct Lexer { + let templateName: String? let templateString: String - init(templateString: String) { + init(templateName: String? = nil, templateString: String) { + self.templateName = templateName self.templateString = templateString } @@ -16,14 +18,28 @@ struct Lexer { } 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("{%") { - 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("{#") { - 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. @@ -41,6 +57,7 @@ struct Lexer { while !scanner.isEmpty { if let text = scanner.scan(until: ["{{", "{%", "{#"]) { if !text.1.isEmpty { + let line = templateString.rangeLine(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) tokens.append(createToken(string: result, at: scanner.range)) } else { + let line = templateString.rangeLine(scanner.range) tokens.append(createToken(string: scanner.content, at: scanner.range)) scanner.content = "" } @@ -165,7 +183,7 @@ extension String { return String(self[first..) -> (content: String, number: UInt, offset: String.IndexDistance) { + public func rangeLine(_ range: Range) -> RangeLine { var lineNumber: UInt = 0 var offset: Int = 0 var lineContent = "" @@ -183,3 +201,5 @@ extension String { return (lineContent, lineNumber, offset) } } + +public typealias RangeLine = (content: String, number: UInt, offset: String.IndexDistance) diff --git a/Sources/Parser.swift b/Sources/Parser.swift index e2a583c..40bdab8 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -107,9 +107,11 @@ public class TokenParser { return try FilterExpression(token: filterToken, parser: self) } catch { 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 - if let filterTokenRange = environment.template?.templateString.range(of: filterToken, range: containingToken.range) { - error.token = Token.variable(value: filterToken, at: filterTokenRange) + // find offset of filter in the containing token so that only filter is highligted, not the whole token + if let filterTokenRange = containingToken.contents.range(of: filterToken) { + 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 { error.token = containingToken } diff --git a/Sources/Template.swift b/Sources/Template.swift index 03176b2..0bf1c78 100644 --- a/Sources/Template.swift +++ b/Sources/Template.swift @@ -20,7 +20,7 @@ open class Template: ExpressibleByStringLiteral { self.name = name self.templateString = templateString - let lexer = Lexer(templateString: templateString) + let lexer = Lexer(templateName: name, templateString: templateString) tokens = lexer.tokenize() } diff --git a/Sources/Tokenizer.swift b/Sources/Tokenizer.swift index 55482ed..7f23f0e 100644 --- a/Sources/Tokenizer.swift +++ b/Sources/Tokenizer.swift @@ -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 { /// A token representing a piece of text. - case text(value: String, at: Range) + case text(value: String, at: SourceMap) /// A token representing a variable. - case variable(value: String, at: Range) + case variable(value: String, at: SourceMap) /// A token representing a comment. - case comment(value: String, at: Range) + case comment(value: String, at: SourceMap) /// A token representing a template block. - case block(value: String, at: Range) + case block(value: String, at: SourceMap) /// Returns the underlying value as an array seperated by spaces public func components() -> [String] { @@ -74,29 +90,29 @@ public enum Token : Equatable { } } - public var range: Range { + public var sourceMap: SourceMap { switch self { - case .block(_, let range), - .variable(_, let range), - .text(_, let range), - .comment(_, let range): - return range + case .block(_, let sourceMap), + .variable(_, let sourceMap), + .text(_, let sourceMap), + .comment(_, let sourceMap): + return sourceMap } } - + } public func == (lhs: Token, rhs: Token) -> Bool { switch (lhs, rhs) { - case (.text(let lhsValue), .text(let rhsValue)): - return lhsValue == rhsValue - case (.variable(let lhsValue), .variable(let rhsValue)): - return lhsValue == rhsValue - case (.block(let lhsValue), .block(let rhsValue)): - return lhsValue == rhsValue - case (.comment(let lhsValue), .comment(let rhsValue)): - return lhsValue == rhsValue + case let (.text(lhsValue, lhsAt), .text(rhsValue, rhsAt)): + return lhsValue == rhsValue && lhsAt == rhsAt + case let (.variable(lhsValue, lhsAt), .variable(rhsValue, rhsAt)): + return lhsValue == rhsValue && lhsAt == rhsAt + case let (.block(lhsValue, lhsAt), .block(rhsValue, rhsAt)): + return lhsValue == rhsValue && lhsAt == rhsAt + case let (.comment(lhsValue, lhsAt), .comment(rhsValue, rhsAt)): + return lhsValue == rhsValue && lhsAt == rhsAt default: return false } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 24f2136..ca097e1 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -41,15 +41,17 @@ func testEnvironment() { } func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { - let token = Token.block(value: token, at: template.templateString.range(of: token)!) - return TemplateSyntaxError(reason: description, token: token, template: template, parentError: nil) + let range = template.templateString.range(of: token)! + 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 { let expectedError = expectedSyntaxError(token: token, template: template, description: reason) - let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"])) - .toThrow(expectedError) + let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"])).toThrow() as TemplateSyntaxError 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 { 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"])) - .toThrow(expectedError) + .toThrow() as TemplateSyntaxError 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 { var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason) 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"])) - .toThrow(expectedError) + .toThrow() as TemplateSyntaxError try expect(environment.errorReporter.renderError(error)) == environment.errorReporter.renderError(expectedError) } @@ -312,7 +313,7 @@ func testEnvironment() { private extension Expectation { @discardableResult - func toThrow(_ error: T) throws -> T { + func toThrow() throws -> T { var thrownError: Error? = nil do { @@ -323,12 +324,9 @@ private extension Expectation { if let thrownError = thrownError { if let thrownError = thrownError as? T { - if error != thrownError { - throw failure("\(thrownError) is not \(error)") - } return thrownError } else { - throw failure("\(thrownError) is not \(error)") + throw failure("\(thrownError) is not \(T.self)") } } else { throw failure("expression did not throw an error") diff --git a/Tests/StencilTests/FilterSpec.swift b/Tests/StencilTests/FilterSpec.swift index 2501610..7898cc0 100644 --- a/Tests/StencilTests/FilterSpec.swift +++ b/Tests/StencilTests/FilterSpec.swift @@ -62,7 +62,7 @@ func testFilter() { } 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") { @@ -91,7 +91,7 @@ func testFilter() { describe("capitalize filter") { - let template = Template(templateString: "{{ name|capitalize }}") + let template = Template(templateString: "{{ name|capitalize }}") $0.it("capitalizes a string") { let result = try template.render(Context(dictionary: ["name": "kyle"])) diff --git a/Tests/StencilTests/ForNodeSpec.swift b/Tests/StencilTests/ForNodeSpec.swift index 8d41c1d..1177b35 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -169,11 +169,9 @@ func testForNode() { } $0.it("handles invalid input") { - let tokens: [Token] = [ - .block(value: "for i", at: .unknown), - ] + let tokens: [Token] = [.block(value: "for i", at: .unknown)] 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) } diff --git a/Tests/StencilTests/IfNodeSpec.swift b/Tests/StencilTests/IfNodeSpec.swift index 25119eb..9fada9f 100644 --- a/Tests/StencilTests/IfNodeSpec.swift +++ b/Tests/StencilTests/IfNodeSpec.swift @@ -179,22 +179,18 @@ func testIfNode() { } $0.it("throws an error when parsing an if block without an endif") { - let tokens: [Token] = [ - .block(value: "if value", at: .unknown), - ] + let tokens: [Token] = [.block(value: "if value", at: .unknown)] 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) } $0.it("throws an error when parsing an ifnot without an endif") { - let tokens: [Token] = [ - .block(value: "ifnot value", at: .unknown), - ] + let tokens: [Token] = [.block(value: "ifnot value", at: .unknown)] 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) } } diff --git a/Tests/StencilTests/IncludeSpec.swift b/Tests/StencilTests/IncludeSpec.swift index bf95c18..153d7f6 100644 --- a/Tests/StencilTests/IncludeSpec.swift +++ b/Tests/StencilTests/IncludeSpec.swift @@ -14,7 +14,7 @@ func testInclude() { 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") + 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) } diff --git a/Tests/StencilTests/LexerSpec.swift b/Tests/StencilTests/LexerSpec.swift index b74df68..270cb4b 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", 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") { @@ -17,7 +17,7 @@ func testLexer() { let tokens = lexer.tokenize() 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") { @@ -25,7 +25,7 @@ func testLexer() { let tokens = lexer.tokenize() 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") { @@ -34,18 +34,18 @@ func testLexer() { let tokens = lexer.tokenize() 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") { - let templateString = "My name is {{ name }}." + let templateString = "My name is {{ myname }}." let lexer = Lexer(templateString: templateString) let tokens = lexer.tokenize() 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[1]) == Token.variable(value: "name", at: templateString.range(of: "{{ name }}")!) - try expect(tokens[2]) == Token.text(value: ".", at: templateString.range(of: ".")!) + 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: "myname", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "myname")!))) + try expect(tokens[2]) == Token.text(value: ".", at: SourceMap(line: templateString.rangeLine(templateString.range(of: ".")!))) } $0.it("can tokenize two variables without being greedy") { @@ -54,8 +54,8 @@ func testLexer() { let tokens = lexer.tokenize() try expect(tokens.count) == 2 - 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 }}")!) + 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: SourceMap(line: templateString.rangeLine(templateString.range(of: "name")!))) } $0.it("can tokenize an unclosed block") { diff --git a/Tests/StencilTests/ParserSpec.swift b/Tests/StencilTests/ParserSpec.swift index facd07a..25c485f 100644 --- a/Tests/StencilTests/ParserSpec.swift +++ b/Tests/StencilTests/ParserSpec.swift @@ -52,11 +52,10 @@ func testTokenParser() { } $0.it("errors when parsing an unknown tag") { - let parser = TokenParser(tokens: [ - .block(value: "unknown", at: .unknown), - ], environment: Environment()) + let tokens: [Token] = [.block(value: "unknown", at: .unknown)] + let parser = TokenParser(tokens: tokens, 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)) } } } From a165a6715f4f315ece845214b2d98ff365ff1053 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Wed, 27 Dec 2017 13:29:13 +0100 Subject: [PATCH 23/81] fixed typos --- Sources/Environment.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Environment.swift b/Sources/Environment.swift index 5fb9e19..44757e7 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -43,7 +43,7 @@ public struct Environment { } func render(template: Template, context: [String: Any]?) throws -> String { - // update temaplte environment as it cen be created from string literal with default environment + // update template environment as it can be created from string literal with default environment template.environment = self return try template.render(context) } From 4bfdb73175a795d6a3f7b1301df06510fc422d5e Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Wed, 27 Dec 2017 13:32:03 +0100 Subject: [PATCH 24/81] removed unneeded code --- Sources/Errors.swift | 12 ------------ Sources/Lexer.swift | 26 +++++++++----------------- 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 4b7cafe..03f2bb1 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -70,15 +70,3 @@ open class SimpleErrorReporter: ErrorReporter { } } - -extension Range where Bound == String.Index { - internal static var unknown: Range { - return "".range - } -} - -extension String { - var range: Range { - return startIndex.. Date: Wed, 27 Dec 2017 18:48:37 +0100 Subject: [PATCH 25/81] removed trailing witespaces --- Sources/Environment.swift | 6 +++--- Sources/Errors.swift | 18 +++++++++--------- Sources/Expression.swift | 12 ++++++------ Sources/Extension.swift | 6 +++--- Sources/ForTag.swift | 10 +++++----- Sources/IfTag.swift | 2 +- Sources/Inheritence.swift | 8 ++++---- Sources/Lexer.swift | 16 ++++++++-------- Sources/Node.swift | 2 +- Sources/Parser.swift | 4 ++-- Sources/Tokenizer.swift | 8 ++++---- Sources/Variable.swift | 10 +++++----- 12 files changed, 51 insertions(+), 51 deletions(-) diff --git a/Sources/Environment.swift b/Sources/Environment.swift index 44757e7..5ccbfa5 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -9,7 +9,7 @@ public struct Environment { extensions: [Extension]? = nil, templateClass: Template.Type = Template.self, errorReporter: ErrorReporter = SimpleErrorReporter()) { - + self.templateClass = templateClass self.errorReporter = errorReporter self.loader = loader @@ -41,11 +41,11 @@ public struct Environment { let template = templateClass.init(templateString: string, environment: self) return try render(template: template, context: context) } - + func render(template: Template, context: [String: Any]?) throws -> String { // update template environment as it can be created from string literal with default environment template.environment = self return try template.render(context) } - + } diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 03f2bb1..4c01dfe 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -27,21 +27,21 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { var allTokens: [Token] { return stackTrace + (token.map({ [$0] }) ?? []) } - + public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) { self.reason = reason self.stackTrace = stackTrace self.token = token } - + public init(_ description: String) { self.init(reason: description) } - + public static func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool { return lhs.description == rhs.description && lhs.token == rhs.token && lhs.stackTrace == rhs.stackTrace } - + } public protocol ErrorReporter: class { @@ -49,24 +49,24 @@ public protocol ErrorReporter: class { } open class SimpleErrorReporter: ErrorReporter { - + open func renderError(_ error: Error) -> String { guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription } - + func describe(token: Token) -> String { let templateName = token.sourceMap.filename ?? "" let line = token.sourceMap.line let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(token.contents.characters.count - 1, 0))))" - + return "\(templateName)\(line.number):\(line.offset): error: \(templateError.reason)\n" + "\(line.content)\n" + "\(highlight)\n" } - + var descriptions = templateError.stackTrace.reduce([]) { $0 + [describe(token: $1)] } let description = templateError.token.map(describe(token:)) ?? templateError.reason descriptions.append(description) return descriptions.joined(separator: "\n") } - + } diff --git a/Sources/Expression.swift b/Sources/Expression.swift index 1f41afe..afea2b1 100644 --- a/Sources/Expression.swift +++ b/Sources/Expression.swift @@ -88,21 +88,21 @@ final class NotExpression: Expression, PrefixOperator, CustomStringConvertible { final class InExpression: Expression, InfixOperator, CustomStringConvertible { let lhs: Expression let rhs: Expression - + init(lhs: Expression, rhs: Expression) { self.lhs = lhs self.rhs = rhs } - + var description: String { return "(\(lhs) in \(rhs))" } - + func evaluate(context: Context) throws -> Bool { if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression { let lhsValue = try lhs.variable.resolve(context) let rhsValue = try rhs.variable.resolve(context) - + if let lhs = lhsValue as? AnyHashable, let rhs = rhsValue as? [AnyHashable] { return rhs.contains(lhs) } else if let lhs = lhsValue as? String, let rhs = rhsValue as? String { @@ -111,10 +111,10 @@ final class InExpression: Expression, InfixOperator, CustomStringConvertible { return true } } - + return false } - + } final class OrExpression: Expression, InfixOperator, CustomStringConvertible { diff --git a/Sources/Extension.swift b/Sources/Extension.swift index 6e77aad..bfa9454 100644 --- a/Sources/Extension.swift +++ b/Sources/Extension.swift @@ -42,9 +42,9 @@ class DefaultExtension: Extension { registerTag("for", parser: ForNode.parse) registerTag("if", parser: IfNode.parse) registerTag("ifnot", parser: IfNode.parse_ifnot) -#if !os(Linux) - registerTag("now", parser: NowNode.parse) -#endif + #if !os(Linux) + registerTag("now", parser: NowNode.parse) + #endif registerTag("include", parser: IncludeNode.parse) registerTag("extends", parser: ExtendsNode.parse) registerTag("block", parser: BlockNode.parse) diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index fcb5d68..6685d0b 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -12,8 +12,8 @@ class ForNode : NodeType { let components = token.components() 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 syntax 'for x in y where condition'.") + (components.count == 4 || (components.count >= 6 && components[4] == "where")) else { + throw TemplateSyntaxError("'for' statements should use the following syntax 'for x in y where condition'.") } let loopVariables = components[1].characters @@ -38,7 +38,7 @@ class ForNode : NodeType { guard let token = parser.nextToken() else { throw TemplateSyntaxError("`endfor` was not found.") } - + if token.contents == "empty" { emptyNodes = try parser.parse(until(["endfor"])) _ = parser.nextToken() @@ -134,14 +134,14 @@ class ForNode : NodeType { "last": index == (count - 1), "counter": index + 1, "counter0": index, - ] + ] return try context.push(dictionary: ["forloop": forContext]) { return try push(value: item, context: context) { try renderNodes(nodes, context) } } - }.joined(separator: "") + }.joined(separator: "") } return try context.push { diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index 07e08f9..ec7952c 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -53,7 +53,7 @@ enum IfToken { case .variable(_): return 0 case .end: - return 0 + return 0 } } diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index 8cb7db2..9d70e72 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -16,7 +16,7 @@ class BlockContext { self.blocks[blockName] = [block] } } - + func popBlock(named blockName: String) -> BlockNode? { if var blocks = blocks[blockName] { let block = blocks.removeFirst() @@ -86,7 +86,7 @@ class ExtendsNode : NodeType { } let baseTemplate = try context.environment.loadTemplate(name: templateName) - + let blockContext: BlockContext if let _blockContext = context[BlockContext.contextKey] as? BlockContext { blockContext = _blockContext @@ -148,7 +148,7 @@ class BlockNode : NodeType { // child node is a block node from child template that extends this node (has the same name) var newContext: [String: Any] = [BlockContext.contextKey: blockContext] - + if let blockSuperNode = child.nodes.first(where: { if case .variable(let variable, _)? = $0.token, variable == "block.super" { return true } else { return false} @@ -170,7 +170,7 @@ class BlockNode : NodeType { } } } - + // render extension node do { return try context.push(dictionary: newContext) { diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index badad26..6642f18 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -47,7 +47,7 @@ struct Lexer { "{{": "}}", "{%": "%}", "{#": "#}", - ] + ] while !scanner.isEmpty { if let text = scanner.scan(until: ["{{", "{%", "{#"]) { @@ -66,14 +66,14 @@ struct Lexer { return tokens } - + } class Scanner { let originalContent: String var content: String var range: Range - + init(_ content: String) { self.originalContent = content self.content = content @@ -83,7 +83,7 @@ class Scanner { var isEmpty: Bool { return content.isEmpty } - + func scan(until: String, returnUntil: Bool = false) -> String { var index = content.startIndex @@ -94,7 +94,7 @@ class Scanner { range = range.upperBound..) -> RangeLine { var lineNumber: UInt = 0 var offset: Int = 0 var lineContent = "" - + for line in components(separatedBy: CharacterSet.newlines) { lineNumber += 1 lineContent = line @@ -189,7 +189,7 @@ extension String { break } } - + return (lineContent, lineNumber, offset) } } diff --git a/Sources/Node.swift b/Sources/Node.swift index 79a3f34..1e8195f 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -3,7 +3,7 @@ import Foundation public protocol NodeType { /// Render the node in the given context func render(_ context:Context) throws -> String - + /// Reference to this node's token var token: Token? { get } } diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 40bdab8..09c5e18 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -101,7 +101,7 @@ public class TokenParser { throw TemplateSyntaxError("Unknown filter '\(name)'") } - + public func compileFilter(_ filterToken: String, containedIn containingToken: Token) throws -> Resolvable { do { return try FilterExpression(token: filterToken, parser: self) @@ -121,7 +121,7 @@ public class TokenParser { } } } - + @available(*, deprecated, message: "Use compileFilter(_:containedIn:)") public func compileFilter(_ token: String) throws -> Resolvable { return try FilterExpression(token: token, parser: self) diff --git a/Sources/Tokenizer.swift b/Sources/Tokenizer.swift index 7f23f0e..23947be 100644 --- a/Sources/Tokenizer.swift +++ b/Sources/Tokenizer.swift @@ -43,14 +43,14 @@ 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 } @@ -89,7 +89,7 @@ public enum Token : Equatable { return value } } - + public var sourceMap: SourceMap { switch self { case .block(_, let sourceMap), diff --git a/Sources/Variable.swift b/Sources/Variable.swift index bc40900..ee81467 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -95,11 +95,11 @@ public struct Variable : Equatable, Resolvable { current = array.count } } else if let object = current as? NSObject { // NSKeyValueCoding -#if os(Linux) - return nil -#else - current = object.value(forKey: bit) -#endif + #if os(Linux) + return nil + #else + current = object.value(forKey: bit) + #endif } else if let value = current { current = Mirror(reflecting: value).getValue(for: bit) if current == nil { From d6766b43da8c32f6c5cbfc14b8d3cb2a15f00276 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Wed, 27 Dec 2017 19:29:17 +0100 Subject: [PATCH 26/81] minor code refactoring --- Sources/Errors.swift | 11 +++++ Sources/ForTag.swift | 9 ++--- Sources/Inheritence.swift | 84 +++++++++++++++++++-------------------- Sources/Node.swift | 7 +--- Sources/Parser.swift | 7 +--- 5 files changed, 58 insertions(+), 60 deletions(-) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 4c01dfe..407a9e2 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -44,6 +44,17 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { } +extension Error { + func withToken(_ token: Token?) -> Error { + if var error = self as? TemplateSyntaxError { + error.token = error.token ?? token + return error + } else { + return TemplateSyntaxError(reason: "\(self)", token: token) + } + } +} + public protocol ErrorReporter: class { func renderError(_ error: Error) -> String } diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index 6685d0b..1b6b1d2 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -24,10 +24,6 @@ class ForNode : NodeType { let variable = components[3] let filter = try parser.compileFilter(variable, containedIn: token) - var emptyNodes = [NodeType]() - - let forNodes = try parser.parse(until(["endfor", "empty"])) - let `where`: Expression? if components.count >= 6 { `where` = try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser, token: token) @@ -35,10 +31,13 @@ class ForNode : NodeType { `where` = nil } + let forNodes = try parser.parse(until(["endfor", "empty"])) + guard let token = parser.nextToken() else { throw TemplateSyntaxError("`endfor` was not found.") } + var emptyNodes = [NodeType]() if token.contents == "empty" { emptyNodes = try parser.parse(until(["endfor"])) _ = parser.nextToken() @@ -90,7 +89,7 @@ class ForNode : NodeType { var values: [Any] if let dictionary = resolved as? [String: Any], !dictionary.isEmpty { - values = dictionary.map { ($0.key, $0.value) } as [(String, Any)] + values = dictionary.map { ($0.key, $0.value) } } else if let array = resolved as? [Any] { values = array } else if let range = resolved as? CountableClosedRange { diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index 9d70e72..db2d67f 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -4,11 +4,12 @@ class BlockContext { // contains mapping of block names to their nodes and templates where they are defined var blocks: [String: [BlockNode]] - init(blocks: [String: [BlockNode]]) { - self.blocks = blocks + init(blocks: [String: BlockNode]) { + self.blocks = [:] + blocks.forEach { self.blocks[$0.key] = [$0.value] } } - func pushBlock(_ block: BlockNode, named blockName: String) { + func push(_ block: BlockNode, forKey blockName: String) { if var blocks = blocks[blockName] { blocks.append(block) self.blocks[blockName] = blocks @@ -17,7 +18,7 @@ class BlockContext { } } - func popBlock(named blockName: String) -> BlockNode? { + func pop(_ blockName: String) -> BlockNode? { if var blocks = blocks[blockName] { let block = blocks.removeFirst() if blocks.isEmpty { @@ -88,14 +89,12 @@ class ExtendsNode : NodeType { let baseTemplate = try context.environment.loadTemplate(name: templateName) let blockContext: BlockContext - if let _blockContext = context[BlockContext.contextKey] as? BlockContext { - blockContext = _blockContext + if let currentBlockContext = context[BlockContext.contextKey] as? BlockContext { + blockContext = currentBlockContext for (name, block) in blocks { - blockContext.pushBlock(block, named: name) + blockContext.push(block, forKey: name) } } else { - var blocks = [String: [BlockNode]]() - self.blocks.forEach { blocks[$0.key] = [$0.value] } blockContext = BlockContext(blocks: blocks) } @@ -144,48 +143,47 @@ class BlockNode : NodeType { } func render(_ context: Context) throws -> String { - if let blockContext = context[BlockContext.contextKey] as? BlockContext, let child = blockContext.popBlock(named: name) { - // child node is a block node from child template that extends this node (has the same name) - - var newContext: [String: Any] = [BlockContext.contextKey: blockContext] - - if let blockSuperNode = child.nodes.first(where: { - if case .variable(let variable, _)? = $0.token, variable == "block.super" { return true } - else { return false} - }) { - do { - // 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)] - } catch { - if let error = error as? TemplateSyntaxError { - throw TemplateSyntaxError( - reason: error.reason, - token: blockSuperNode.token, - stackTrace: error.allTokens) - } else { - throw TemplateSyntaxError( - reason: "\(error)", - token: blockSuperNode.token, - stackTrace: []) - } - } - } - + if let blockContext = context[BlockContext.contextKey] as? BlockContext, let child = blockContext.pop(name) { + let childContext = try self.childContext(child, blockContext: blockContext, context: context) // render extension node do { - return try context.push(dictionary: newContext) { + return try context.push(dictionary: childContext) { return try child.render(context) } } catch { - if var error = error as? TemplateSyntaxError { - error.token = error.token ?? child.token - throw error - } else { - throw error - } + throw error.withToken(child.token) } } return try renderNodes(nodes, context) } + + // child node is a block node from child template that extends this node (has the same name) + func childContext(_ child: BlockNode, blockContext: BlockContext, context: Context) throws -> [String: Any?] { + var childContext: [String: Any?] = [BlockContext.contextKey: blockContext] + + if let blockSuperNode = child.nodes.first(where: { + if case .variable(let variable, _)? = $0.token, variable == "block.super" { return true } + else { return false} + }) { + do { + // render base node so that its content can be used as part of child node that extends it + childContext["block"] = ["super": try self.render(context)] + } catch { + if let error = error as? TemplateSyntaxError { + throw TemplateSyntaxError( + reason: error.reason, + token: blockSuperNode.token, + stackTrace: error.allTokens) + } else { + throw TemplateSyntaxError( + reason: "\(error)", + token: blockSuperNode.token, + stackTrace: []) + } + } + } + return childContext + } + } diff --git a/Sources/Node.swift b/Sources/Node.swift index 1e8195f..84a114f 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -15,12 +15,7 @@ public func renderNodes(_ nodes:[NodeType], _ context:Context) throws -> String do { return try $0.render(context) } catch { - if var error = error as? TemplateSyntaxError { - error.token = error.token ?? $0.token - throw error - } else { - throw error - } + throw error.withToken($0.token) } }).joined(separator: "") } diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 09c5e18..4a0b2c6 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -54,12 +54,7 @@ public class TokenParser { let node = try parser(self, token) nodes.append(node) } catch { - if var error = error as? TemplateSyntaxError { - error.token = error.token ?? token - throw error - } else { - throw error - } + throw error.withToken(token) } } case .comment: From b54292788f618a8d0779f9c684c73e956cc758e1 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Thu, 28 Dec 2017 21:15:33 +0100 Subject: [PATCH 27/81] fixed swift 3 compiler crash --- Sources/Lexer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 6642f18..bf4d9f8 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -194,4 +194,4 @@ extension String { } } -public typealias RangeLine = (content: String, number: UInt, offset: String.IndexDistance) +public typealias RangeLine = (content: String, number: UInt, offset: Int) From 4f1a5b3e3d0f65bdf86042387959890a740273bd Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sun, 12 Aug 2018 22:25:25 +0100 Subject: [PATCH 28/81] store reference to token when parsing range variable --- Sources/Parser.swift | 2 +- Sources/Variable.swift | 13 ++++++++++++- Tests/StencilTests/VariableSpec.swift | 8 +++++--- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 5dde40c..fd984a4 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -148,7 +148,7 @@ public class TokenParser { } public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable { - return try RangeVariable(token, parser: self) + return try RangeVariable(token, parser: self, containedIn: containingToken) ?? compileFilter(token, containedIn: containingToken) } diff --git a/Sources/Variable.swift b/Sources/Variable.swift index 1c54a55..1da7439 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -50,7 +50,7 @@ public struct Variable : Equatable, Resolvable { // Split the lookup string and resolve references if possible fileprivate func lookup(_ context: Context) throws -> [String] { - var keyPath = KeyPath(variable, in: context) + let keyPath = KeyPath(variable, in: context) return try keyPath.parse() } @@ -138,6 +138,7 @@ public struct RangeVariable: Resolvable { public let from: Resolvable public let to: Resolvable + @available(*, deprecated, message: "Use init?(_:parser:containedIn:)") public init?(_ token: String, parser: TokenParser) throws { let components = token.components(separatedBy: "...") guard components.count == 2 else { @@ -148,6 +149,16 @@ public struct RangeVariable: Resolvable { self.to = try parser.compileFilter(components[1]) } + public init?(_ token: String, parser: TokenParser, containedIn containingToken: Token) throws { + let components = token.components(separatedBy: "...") + guard components.count == 2 else { + return nil + } + + self.from = try parser.compileFilter(components[0], containedIn: containingToken) + self.to = try parser.compileFilter(components[1], containedIn: containingToken) + } + public func resolve(_ context: Context) throws -> Any? { let fromResolved = try from.resolve(context) let toResolved = try to.resolve(context) diff --git a/Tests/StencilTests/VariableSpec.swift b/Tests/StencilTests/VariableSpec.swift index 7b386bc..ac85f1b 100644 --- a/Tests/StencilTests/VariableSpec.swift +++ b/Tests/StencilTests/VariableSpec.swift @@ -292,7 +292,9 @@ func testVariable() { }() func makeVariable(_ token: String) throws -> RangeVariable? { - return try RangeVariable(token, parser: TokenParser(tokens: [], environment: context.environment)) + let token = Token.variable(value: token, at: .unknown) + let parser = TokenParser(tokens: [token], environment: context.environment) + return try RangeVariable(token.contents, parser: parser, containedIn: token) } $0.it("can resolve closed range as array") { @@ -321,11 +323,11 @@ func testVariable() { } $0.it("throws is left range value is missing") { - try expect(makeVariable("...1")).toThrow() + try expect(makeVariable("...1")).toThrow() } $0.it("throws is right range value is missing") { - try expect(makeVariable("1...")).toThrow() + try expect(makeVariable("1...")).toThrow() } } From b9702afbd46d676fbe5d4e141703a389a9b81457 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 13 Aug 2018 20:00:27 +0100 Subject: [PATCH 29/81] fixed indetnations --- Sources/ForTag.swift | 6 ++-- Sources/Lexer.swift | 3 +- Sources/Node.swift | 4 +-- Sources/Parser.swift | 21 ++++++------ Tests/StencilTests/ForNodeSpec.swift | 50 ++++++++++++++-------------- Tests/StencilTests/LexerSpec.swift | 2 +- 6 files changed, 42 insertions(+), 44 deletions(-) diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index d5bfee7..3432b95 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -31,8 +31,8 @@ class ForNode : NodeType { let resolvable = try parser.compileResolvable(components[3], containedIn: token) let `where` = hasToken("where", at: 4) - ? try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser, token: token) - : nil + ? try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser, token: token) + : nil let forNodes = try parser.parse(until(["endfor", "empty"])) @@ -145,7 +145,7 @@ class ForNode : NodeType { try renderNodes(nodes, context) } } - }.joined(separator: "") + }.joined(separator: "") } return try context.push { diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 4c8f8a8..ec833d5 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -189,8 +189,7 @@ extension String { lineNumber += 1 lineContent = line if let rangeOfLine = self.range(of: line), rangeOfLine.contains(range.lowerBound) { - offset = distance(from: rangeOfLine.lowerBound, to: - range.lowerBound) + offset = distance(from: rangeOfLine.lowerBound, to: range.lowerBound) break } } diff --git a/Sources/Node.swift b/Sources/Node.swift index c0e644c..c4bb77a 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -11,13 +11,13 @@ public protocol NodeType { /// Render the collection of nodes in the given context public func renderNodes(_ nodes:[NodeType], _ context:Context) throws -> String { - return try nodes.map({ + return try nodes.map { do { return try $0.render(context) } catch { throw error.withToken($0.token) } - }).joined(separator: "") + }.joined(separator: "") } public class SimpleNode : NodeType { diff --git a/Sources/Parser.swift b/Sources/Parser.swift index fd984a4..81d9335 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -120,19 +120,18 @@ public class TokenParser { do { return try FilterExpression(token: filterToken, parser: self) } catch { - if var error = error as? TemplateSyntaxError, error.token == nil { - // find offset of filter in the containing token so that only filter is highligted, not the whole token - if let filterTokenRange = containingToken.contents.range(of: filterToken) { - 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 { - error.token = containingToken - } - throw error - } else { + guard var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil else { throw error } + // find offset of filter in the containing token so that only filter is highligted, not the whole token + if let filterTokenRange = containingToken.contents.range(of: filterToken) { + var rangeLine = containingToken.sourceMap.line + rangeLine.offset += containingToken.contents.distance(from: containingToken.contents.startIndex, to: filterTokenRange.lowerBound) + syntaxError.token = .variable(value: filterToken, at: SourceMap(filename: containingToken.sourceMap.filename, line: rangeLine)) + } else { + syntaxError.token = containingToken + } + throw syntaxError } } diff --git a/Tests/StencilTests/ForNodeSpec.swift b/Tests/StencilTests/ForNodeSpec.swift index 4fb1a04..ddb7692 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -13,7 +13,7 @@ func testForNode() { "two": "II", ], "tuples": [(1, 2, 3), (4, 5, 6)] - ]) + ]) $0.it("renders the given nodes for each item") { let nodes: [NodeType] = [VariableNode(variable: "item")] @@ -31,7 +31,7 @@ func testForNode() { $0.it("renders a context variable of type Array") { let any_context = Context(dictionary: [ "items": ([1, 2, 3] as [Any]) - ]) + ]) let nodes: [NodeType] = [VariableNode(variable: "item")] let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) @@ -54,17 +54,17 @@ func testForNode() { try expect(try node.render(context)) == "123" } -#if os(OSX) + #if os(OSX) $0.it("renders a context variable of type NSArray") { let nsarray_context = Context(dictionary: [ "items": NSArray(array: [1, 2, 3]) - ]) + ]) let nodes: [NodeType] = [VariableNode(variable: "item")] let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) try expect(try node.render(nsarray_context)) == "123" } -#endif + #endif $0.it("renders the given nodes while providing if the item is first in the context") { let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.first")] @@ -97,31 +97,31 @@ func testForNode() { } $0.it("renders the given nodes while filtering items using where expression") { - let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")] + let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")] let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown)) - let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`) - try expect(try node.render(context)) == "2132" + let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`) + try expect(try node.render(context)) == "2132" } $0.it("renders the given empty nodes when all items filtered out with where expression") { - let nodes: [NodeType] = [VariableNode(variable: "item")] - let emptyNodes: [NodeType] = [TextNode(text: "empty")] + let nodes: [NodeType] = [VariableNode(variable: "item")] + let emptyNodes: [NodeType] = [TextNode(text: "empty")] let `where` = try parseExpression(components: ["item", "==", "0"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown)) - let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`) - try expect(try node.render(context)) == "empty" + let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`) + try expect(try node.render(context)) == "empty" } $0.it("can render a filter with spaces") { let templateString = "{% for article in ars | default: a, b , articles %}" + "- {{ article.title }} by {{ article.author }}.\n" + - "{% endfor %}\n" + "{% endfor %}\n" let context = Context(dictionary: [ "articles": [ Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"), Article(title: "Memory Management with ARC", author: "Kyle Fuller"), ] - ]) + ]) let template = Template(templateString: templateString) let result = try template.render(context) @@ -129,7 +129,7 @@ func testForNode() { let fixture = "" + "- Migrating from OCUnit to XCTest by Kyle Fuller.\n" + "- Memory Management with ARC by Kyle Fuller.\n" + - "\n" + "\n" try expect(result) == fixture } @@ -184,7 +184,7 @@ func testForNode() { $0.it("can iterate over dictionary") { let templateString = "{% for key, value in dict %}" + "{{ key }}: {{ value }}," + - "{% endfor %}" + "{% endfor %}" let template = Template(templateString: templateString) let result = try template.render(context) @@ -197,7 +197,7 @@ func testForNode() { let nodes: [NodeType] = [ VariableNode(variable: "key"), TextNode(text: ","), - ] + ] let emptyNodes: [NodeType] = [TextNode(text: "empty")] let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key"], nodes: nodes, emptyNodes: emptyNodes, where: nil) let result = try node.render(context) @@ -212,7 +212,7 @@ func testForNode() { TextNode(text: "="), VariableNode(variable: "value"), TextNode(text: ","), - ] + ] let emptyNodes: [NodeType] = [TextNode(text: "empty")] let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key", "value"], nodes: nodes, emptyNodes: emptyNodes, where: nil) @@ -237,14 +237,14 @@ func testForNode() { 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) @@ -254,14 +254,14 @@ func testForNode() { $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) @@ -289,15 +289,15 @@ func testForNode() { 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) diff --git a/Tests/StencilTests/LexerSpec.swift b/Tests/StencilTests/LexerSpec.swift index 9fe8df0..2a9f1e1 100644 --- a/Tests/StencilTests/LexerSpec.swift +++ b/Tests/StencilTests/LexerSpec.swift @@ -77,7 +77,7 @@ func testLexer() { "%}{{\n" + "name\n" + "}}{%\n" + - "endif %}." + "endif %}." let lexer = Lexer(templateString: templateString) From 71ad162268028ac3acaec1dab04aa8e748c3126f Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 13 Aug 2018 20:02:07 +0100 Subject: [PATCH 30/81] more indentations fixed --- Sources/ForTag.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index 3432b95..284498f 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -46,10 +46,10 @@ class ForNode : NodeType { _ = parser.nextToken() } - return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`, token: token) + return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes: emptyNodes, where: `where`, token: token) } - init(resolvable: Resolvable, loopVariables: [String], nodes:[NodeType], emptyNodes:[NodeType], where: Expression? = nil, token: Token? = nil) { + init(resolvable: Resolvable, loopVariables: [String], nodes: [NodeType], emptyNodes: [NodeType], where: Expression? = nil, token: Token? = nil) { self.resolvable = resolvable self.loopVariables = loopVariables self.nodes = nodes From 92ebfe59b1fda57ec4f5acd17d6924d6bd1a3110 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 13 Aug 2018 23:05:33 +0100 Subject: [PATCH 31/81] removed unneeded error reporter references --- Sources/Context.swift | 4 ---- Sources/Environment.swift | 5 +---- Tests/StencilTests/EnvironmentSpec.swift | 9 ++++++--- Tests/StencilTests/FilterSpec.swift | 4 ++-- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/Sources/Context.swift b/Sources/Context.swift index a976e1e..157f230 100644 --- a/Sources/Context.swift +++ b/Sources/Context.swift @@ -4,10 +4,6 @@ public class Context { public let environment: Environment - public var errorReporter: ErrorReporter { - return environment.errorReporter - } - init(dictionary: [String: Any]? = nil, environment: Environment? = nil) { if let dictionary = dictionary { dictionaries = [dictionary] diff --git a/Sources/Environment.swift b/Sources/Environment.swift index 5ccbfa5..2778a5d 100644 --- a/Sources/Environment.swift +++ b/Sources/Environment.swift @@ -3,15 +3,12 @@ public struct Environment { public var extensions: [Extension] public var loader: Loader? - public var errorReporter: ErrorReporter public init(loader: Loader? = nil, extensions: [Extension]? = nil, - templateClass: Template.Type = Template.self, - errorReporter: ErrorReporter = SimpleErrorReporter()) { + templateClass: Template.Type = Template.self) { self.templateClass = templateClass - self.errorReporter = errorReporter self.loader = loader self.extensions = (extensions ?? []) + [DefaultExtension()] } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 8fd4684..5dc6796 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -54,7 +54,8 @@ func testEnvironment() { let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"]), file: file, line: line, function: function).toThrow() as TemplateSyntaxError - try expect(environment.errorReporter.renderError(error), file: file, line: line, function: function) == environment.errorReporter.renderError(expectedError) + let reporter = SimpleErrorReporter() + try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError) } $0.context("given syntax error") { @@ -209,7 +210,8 @@ func testEnvironment() { let error = try expect(environment.render(template: template, context: ["target": "World"]), file: file, line: line, function: function).toThrow() as TemplateSyntaxError - try expect(environment.errorReporter.renderError(error), file: file, line: line, function: function) == environment.errorReporter.renderError(expectedError) + let reporter = SimpleErrorReporter() + try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError) } $0.it("reports syntax error in included template") { @@ -259,7 +261,8 @@ func testEnvironment() { } let error = try expect(environment.render(template: childTemplate, context: ["target": "World"]), file: file, line: line, function: function).toThrow() as TemplateSyntaxError - try expect(environment.errorReporter.renderError(error), file: file, line: line, function: function) == environment.errorReporter.renderError(expectedError) + let reporter = SimpleErrorReporter() + try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError) } $0.it("reports syntax error in base template") { diff --git a/Tests/StencilTests/FilterSpec.swift b/Tests/StencilTests/FilterSpec.swift index 710d5ae..1de6a8e 100644 --- a/Tests/StencilTests/FilterSpec.swift +++ b/Tests/StencilTests/FilterSpec.swift @@ -232,8 +232,8 @@ func testFilter() { let error = try expect(environment.render(template: template, context: [:]), file: file, line: line, function: function).toThrow() as TemplateSyntaxError - - try expect(environment.errorReporter.renderError(error), file: file, line: line, function: function) == environment.errorReporter.renderError(expectedError) + let reporter = SimpleErrorReporter() + try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError) } $0.it("made for unknown filter") { From 96a004eb3426d3c25df6f88774d1bf773f9af026 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Tue, 14 Aug 2018 00:56:10 +0100 Subject: [PATCH 32/81] replace implicitly unwrapped optional with fatalError --- Tests/StencilTests/EnvironmentSpec.swift | 4 +++- Tests/StencilTests/FilterSpec.swift | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 5dc6796..aa68c3a 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -41,7 +41,9 @@ func testEnvironment() { } func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { - let range = template.templateString.range(of: token)! + guard let range = template.templateString.range(of: token) else { + fatalError("Can't find '\(token)' in '\(template)'") + } let rangeLine = template.templateString.rangeLine(range) let sourceMap = SourceMap(filename: template.name, line: rangeLine) let token = Token.block(value: token, at: sourceMap) diff --git a/Tests/StencilTests/FilterSpec.swift b/Tests/StencilTests/FilterSpec.swift index 1de6a8e..6c9139f 100644 --- a/Tests/StencilTests/FilterSpec.swift +++ b/Tests/StencilTests/FilterSpec.swift @@ -218,7 +218,9 @@ func testFilter() { var filterExtension: Extension! func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { - let range = template.templateString.range(of: token)! + guard let range = template.templateString.range(of: token) else { + fatalError("Can't find '\(token)' in '\(template)'") + } let rangeLine = template.templateString.rangeLine(range) let sourceMap = SourceMap(filename: template.name, line: rangeLine) let token = Token.block(value: token, at: sourceMap) From 6a4959cea0aa2667eeb5c20d9196d242ea172fcd Mon Sep 17 00:00:00 2001 From: David Jennes Date: Sun, 26 Aug 2018 23:17:13 +0200 Subject: [PATCH 33/81] Release 0.12.0 --- CHANGELOG.md | 2 +- Stencil.podspec.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c8662c..f1f5678 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Stencil Changelog -## Master +## 0.12.0 ### Enhancements diff --git a/Stencil.podspec.json b/Stencil.podspec.json index a173c05..7483651 100644 --- a/Stencil.podspec.json +++ b/Stencil.podspec.json @@ -1,6 +1,6 @@ { "name": "Stencil", - "version": "0.11.0", + "version": "0.12.0", "summary": "Stencil is a simple and powerful template language for Swift.", "homepage": "https://stencil.fuller.li", "license": { @@ -13,7 +13,7 @@ "social_media_url": "https://twitter.com/kylefuller", "source": { "git": "https://github.com/stencilproject/Stencil.git", - "tag": "0.11.0" + "tag": "0.12.0" }, "source_files": [ "Sources/*.swift" From 42972a1c10f548e2e2161ded1cd27c1947a9ab65 Mon Sep 17 00:00:00 2001 From: David Jennes Date: Sun, 26 Aug 2018 23:24:29 +0200 Subject: [PATCH 34/81] Reset Changelog --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1f5678..ca5849c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Stencil Changelog +## Master + + ### Bug Fixes + + _None_ + + ### Breaking Changes + + _None_ + + ### New Features + + _None_ + + ### Internal Changes + + _None_ + + ## 0.12.0 ### Enhancements From fc404b25d81f93ca48aa48a6fc50a9c0dce88128 Mon Sep 17 00:00:00 2001 From: David Jennes Date: Sun, 26 Aug 2018 23:27:06 +0200 Subject: [PATCH 35/81] Update the docs version number --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 20ccb77..bdcac0d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -58,9 +58,9 @@ author = 'Kyle Fuller' # built documents. # # The short X.Y version. -version = '0.7.0' +version = '0.12.0' # The full version, including alpha/beta/rc tags. -release = '0.7.0' +release = '0.12.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 2eeb7babd3bfcf1bf1843555a4d1445d07b1f226 Mon Sep 17 00:00:00 2001 From: David Jennes Date: Thu, 30 Aug 2018 13:29:45 +0200 Subject: [PATCH 36/81] Updated the PathKit dependency to 0.9.0 in the podspec --- Stencil.podspec.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Stencil.podspec.json b/Stencil.podspec.json index 7483651..3fdbbcf 100644 --- a/Stencil.podspec.json +++ b/Stencil.podspec.json @@ -26,7 +26,7 @@ "requires_arc": true, "dependencies": { "PathKit": [ - "~> 0.8.0" + "~> 0.9.0" ] } } From 99be5f0459d67aad8d91f0651a31b98765bca98f Mon Sep 17 00:00:00 2001 From: David Jennes Date: Thu, 30 Aug 2018 13:29:52 +0200 Subject: [PATCH 37/81] Changelog entry --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca5849c..8d5153b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,9 @@ ### Internal Changes - _None_ +- Updated the PathKit dependency to 0.9.0 in CocoaPods, to be in line with SPM. + [David Jennes](https://github.com/djbe) + [#227](https://github.com/stencilproject/Stencil/pull/227) ## 0.12.0 From 2ed5763fe4878706cfae40a6de1f848c920c6be8 Mon Sep 17 00:00:00 2001 From: David Jennes Date: Thu, 30 Aug 2018 13:47:19 +0200 Subject: [PATCH 38/81] Release 0.12.1 --- CHANGELOG.md | 16 ++-------------- Stencil.podspec.json | 2 +- docs/conf.py | 4 ++-- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d5153b..b782446 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,8 @@ # Stencil Changelog -## Master +## 0.12.1 - ### Bug Fixes - - _None_ - - ### Breaking Changes - - _None_ - - ### New Features - - _None_ - - ### Internal Changes +### Internal Changes - Updated the PathKit dependency to 0.9.0 in CocoaPods, to be in line with SPM. [David Jennes](https://github.com/djbe) diff --git a/Stencil.podspec.json b/Stencil.podspec.json index 3fdbbcf..3de5732 100644 --- a/Stencil.podspec.json +++ b/Stencil.podspec.json @@ -13,7 +13,7 @@ "social_media_url": "https://twitter.com/kylefuller", "source": { "git": "https://github.com/stencilproject/Stencil.git", - "tag": "0.12.0" + "tag": "0.12.1" }, "source_files": [ "Sources/*.swift" diff --git a/docs/conf.py b/docs/conf.py index bdcac0d..4d69c8d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -58,9 +58,9 @@ author = 'Kyle Fuller' # built documents. # # The short X.Y version. -version = '0.12.0' +version = '0.12.1' # The full version, including alpha/beta/rc tags. -release = '0.12.0' +release = '0.12.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From b476e50f89577f5848e8013dbf0a850abac892aa Mon Sep 17 00:00:00 2001 From: David Jennes Date: Thu, 30 Aug 2018 13:47:19 +0200 Subject: [PATCH 39/81] Forgot version bump --- Stencil.podspec.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Stencil.podspec.json b/Stencil.podspec.json index 3de5732..fceb79c 100644 --- a/Stencil.podspec.json +++ b/Stencil.podspec.json @@ -1,6 +1,6 @@ { "name": "Stencil", - "version": "0.12.0", + "version": "0.12.1", "summary": "Stencil is a simple and powerful template language for Swift.", "homepage": "https://stencil.fuller.li", "license": { From 1b85b816fd96118d491e8e2ada90dccc75b7573f Mon Sep 17 00:00:00 2001 From: David Jennes Date: Thu, 30 Aug 2018 13:58:30 +0200 Subject: [PATCH 40/81] Reset changelog --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b782446..2d00731 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Stencil Changelog +## Master + +### Bug Fixes + + _None_ + +### Breaking Changes + + _None_ + +### New Features + + _None_ + +### Internal Changes + + _None_ + + ## 0.12.1 ### Internal Changes From 00e71c1b4dc2ec6c0f87841a10e34299e6059fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Duperron?= Date: Sat, 8 Sep 2018 15:05:05 +0200 Subject: [PATCH 41/81] Fix typo in VariableSpec describing subscripting (#229) --- Tests/StencilTests/VariableSpec.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/StencilTests/VariableSpec.swift b/Tests/StencilTests/VariableSpec.swift index ac85f1b..66947e0 100644 --- a/Tests/StencilTests/VariableSpec.swift +++ b/Tests/StencilTests/VariableSpec.swift @@ -189,7 +189,7 @@ func testVariable() { try expect(result) == 2 } - $0.describe("Subrscripting") { + $0.describe("Subscripting") { $0.it("can resolve a property subscript via reflection") { try context.push(dictionary: ["property": "name"]) { let variable = Variable("article.author[property]") From acda1b0caff2afba64b88c645bfc3f2c31a2eef7 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 10 Sep 2018 11:39:19 +0100 Subject: [PATCH 42/81] process template lines when Lexer is created not when parsing each token. (#230) --- Sources/Errors.swift | 8 ++--- Sources/Lexer.swift | 43 ++++++++++++------------ Sources/Parser.swift | 6 ++-- Sources/Tokenizer.swift | 8 ++--- Tests/StencilTests/EnvironmentSpec.swift | 5 +-- Tests/StencilTests/FilterSpec.swift | 5 +-- Tests/StencilTests/LexerSpec.swift | 28 +++++++-------- 7 files changed, 52 insertions(+), 51 deletions(-) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 407a9e2..55b976a 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -66,11 +66,11 @@ open class SimpleErrorReporter: ErrorReporter { func describe(token: Token) -> String { let templateName = token.sourceMap.filename ?? "" - let line = token.sourceMap.line - let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(token.contents.characters.count - 1, 0))))" + let location = token.sourceMap.location + let highlight = "\(String(Array(repeating: " ", count: location.lineOffset)))^\(String(Array(repeating: "~", count: max(token.contents.characters.count - 1, 0))))" - return "\(templateName)\(line.number):\(line.offset): error: \(templateError.reason)\n" - + "\(line.content)\n" + return "\(templateName)\(location.lineNumber):\(location.lineOffset): error: \(templateError.reason)\n" + + "\(location.content)\n" + "\(highlight)\n" } diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index ec833d5..fdbc880 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -1,12 +1,20 @@ import Foundation +typealias Line = (content: String, number: UInt, range: Range) + struct Lexer { let templateName: String? let templateString: String + let lines: [Line] init(templateName: String? = nil, templateString: String) { self.templateName = templateName self.templateString = templateString + + self.lines = templateString.components(separatedBy: .newlines).enumerated().flatMap { + guard !$0.element.isEmpty else { return nil } + return (content: $0.element, number: UInt($0.offset + 1), templateString.range(of: $0.element)!) + } } func createToken(string: String, at range: Range) -> Token { @@ -25,8 +33,8 @@ struct Lexer { if string.hasPrefix("{{") || string.hasPrefix("{%") || string.hasPrefix("{#") { let value = strip() let range = templateString.range(of: value, range: range) ?? range - let line = templateString.rangeLine(range) - let sourceMap = SourceMap(filename: templateName, line: line) + let location = rangeLocation(range) + let sourceMap = SourceMap(filename: templateName, location: location) if string.hasPrefix("{{") { return .variable(value: value, at: sourceMap) @@ -37,8 +45,8 @@ struct Lexer { } } - let line = templateString.rangeLine(range) - let sourceMap = SourceMap(filename: templateName, line: line) + let location = rangeLocation(range) + let sourceMap = SourceMap(filename: templateName, location: location) return .text(value: string, at: sourceMap) } @@ -72,6 +80,14 @@ struct Lexer { return tokens } + func rangeLocation(_ range: Range) -> ContentLocation { + guard let line = self.lines.first(where: { $0.range.contains(range.lowerBound) }) else { + return ("", 0, 0) + } + let offset = templateString.distance(from: line.range.lowerBound, to: range.lowerBound) + return (line.content, line.number, offset) + } + } class Scanner { @@ -179,23 +195,6 @@ extension String { let last = findLastNot(character: character) ?? endIndex return String(self[first..) -> RangeLine { - var lineNumber: UInt = 0 - var offset: Int = 0 - var lineContent = "" - - for line in components(separatedBy: CharacterSet.newlines) { - lineNumber += 1 - lineContent = line - if let rangeOfLine = self.range(of: line), rangeOfLine.contains(range.lowerBound) { - offset = distance(from: rangeOfLine.lowerBound, to: range.lowerBound) - break - } - } - - return (lineContent, lineNumber, offset) - } } -public typealias RangeLine = (content: String, number: UInt, offset: Int) +public typealias ContentLocation = (content: String, lineNumber: UInt, lineOffset: Int) diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 81d9335..b36f160 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -125,9 +125,9 @@ public class TokenParser { } // find offset of filter in the containing token so that only filter is highligted, not the whole token if let filterTokenRange = containingToken.contents.range(of: filterToken) { - var rangeLine = containingToken.sourceMap.line - rangeLine.offset += containingToken.contents.distance(from: containingToken.contents.startIndex, to: filterTokenRange.lowerBound) - syntaxError.token = .variable(value: filterToken, at: SourceMap(filename: containingToken.sourceMap.filename, line: rangeLine)) + var location = containingToken.sourceMap.location + location.lineOffset += containingToken.contents.distance(from: containingToken.contents.startIndex, to: filterTokenRange.lowerBound) + syntaxError.token = .variable(value: filterToken, at: SourceMap(filename: containingToken.sourceMap.filename, location: location)) } else { syntaxError.token = containingToken } diff --git a/Sources/Tokenizer.swift b/Sources/Tokenizer.swift index a243f80..53f5205 100644 --- a/Sources/Tokenizer.swift +++ b/Sources/Tokenizer.swift @@ -57,17 +57,17 @@ extension String { public struct SourceMap: Equatable { public let filename: String? - public let line: RangeLine + public let location: ContentLocation - init(filename: String? = nil, line: RangeLine = ("", 0, 0)) { + init(filename: String? = nil, location: ContentLocation = ("", 0, 0)) { self.filename = filename - self.line = line + self.location = location } static let unknown = SourceMap() public static func ==(lhs: SourceMap, rhs: SourceMap) -> Bool { - return lhs.filename == rhs.filename && lhs.line == rhs.line + return lhs.filename == rhs.filename && lhs.location == rhs.location } } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index aa68c3a..4c8abed 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -44,8 +44,9 @@ func testEnvironment() { guard let range = template.templateString.range(of: token) else { fatalError("Can't find '\(token)' in '\(template)'") } - let rangeLine = template.templateString.rangeLine(range) - let sourceMap = SourceMap(filename: template.name, line: rangeLine) + let lexer = Lexer(templateString: template.templateString) + let location = lexer.rangeLocation(range) + let sourceMap = SourceMap(filename: template.name, location: location) let token = Token.block(value: token, at: sourceMap) return TemplateSyntaxError(reason: description, token: token, stackTrace: []) } diff --git a/Tests/StencilTests/FilterSpec.swift b/Tests/StencilTests/FilterSpec.swift index 6c9139f..fe40fc4 100644 --- a/Tests/StencilTests/FilterSpec.swift +++ b/Tests/StencilTests/FilterSpec.swift @@ -221,8 +221,9 @@ func testFilter() { guard let range = template.templateString.range(of: token) else { fatalError("Can't find '\(token)' in '\(template)'") } - let rangeLine = template.templateString.rangeLine(range) - let sourceMap = SourceMap(filename: template.name, line: rangeLine) + let lexer = Lexer(templateString: template.templateString) + let location = lexer.rangeLocation(range) + let sourceMap = SourceMap(filename: template.name, location: location) let token = Token.block(value: token, at: sourceMap) return TemplateSyntaxError(reason: description, token: token, stackTrace: []) } diff --git a/Tests/StencilTests/LexerSpec.swift b/Tests/StencilTests/LexerSpec.swift index 2a9f1e1..1babed9 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", at: SourceMap(line: ("Hello World", 1, 0))) + try expect(tokens.first) == .text(value: "Hello World", at: SourceMap(location: ("Hello World", 1, 0))) } $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", at: SourceMap(line: ("{# Comment #}", 1, 3))) + try expect(tokens.first) == .comment(value: "Comment", at: SourceMap(location: ("{# Comment #}", 1, 3))) } $0.it("can tokenize a variable") { @@ -25,7 +25,7 @@ func testLexer() { let tokens = lexer.tokenize() try expect(tokens.count) == 1 - try expect(tokens.first) == .variable(value: "Variable", at: SourceMap(line: ("{{ Variable }}", 1, 3))) + try expect(tokens.first) == .variable(value: "Variable", at: SourceMap(location: ("{{ Variable }}", 1, 3))) } $0.it("can tokenize unclosed tag by ignoring it") { @@ -34,7 +34,7 @@ func testLexer() { let tokens = lexer.tokenize() try expect(tokens.count) == 1 - try expect(tokens.first) == .text(value: "", at: SourceMap(line: ("{{ thing", 1, 0))) + try expect(tokens.first) == .text(value: "", at: SourceMap(location: ("{{ thing", 1, 0))) } $0.it("can tokenize a mixture of content") { @@ -43,9 +43,9 @@ func testLexer() { let tokens = lexer.tokenize() try expect(tokens.count) == 3 - 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: "myname", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "myname")!))) - try expect(tokens[2]) == Token.text(value: ".", at: SourceMap(line: templateString.rangeLine(templateString.range(of: ".")!))) + try expect(tokens[0]) == Token.text(value: "My name is ", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "My name is ")!))) + try expect(tokens[1]) == Token.variable(value: "myname", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "myname")!))) + try expect(tokens[2]) == Token.text(value: ".", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: ".")!))) } $0.it("can tokenize two variables without being greedy") { @@ -54,8 +54,8 @@ func testLexer() { let tokens = lexer.tokenize() try expect(tokens.count) == 2 - 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: SourceMap(line: templateString.rangeLine(templateString.range(of: "name")!))) + try expect(tokens[0]) == Token.variable(value: "thing", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "thing")!))) + try expect(tokens[1]) == Token.variable(value: "name", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "name")!))) } $0.it("can tokenize an unclosed block") { @@ -84,11 +84,11 @@ func testLexer() { let tokens = lexer.tokenize() try expect(tokens.count) == 5 - 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.block(value: "if name and name", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "{%")!))) - try expect(tokens[2]) == Token.variable(value: "name", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "name", options: [.backwards])!))) - try expect(tokens[3]) == Token.block(value: "endif", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "endif")!))) - try expect(tokens[4]) == Token.text(value: ".", at: SourceMap(line: templateString.rangeLine(templateString.range(of: ".")!))) + try expect(tokens[0]) == Token.text(value: "My name is ", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "My name is")!))) + try expect(tokens[1]) == Token.block(value: "if name and name", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "{%")!))) + try expect(tokens[2]) == Token.variable(value: "name", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "name", options: [.backwards])!))) + try expect(tokens[3]) == Token.block(value: "endif", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "endif")!))) + try expect(tokens[4]) == Token.text(value: ".", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: ".")!))) } } } From 9de8190988b27d952ff52a639e5d977ecddc0d9c Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Sat, 1 Sep 2018 19:21:20 +1000 Subject: [PATCH 43/81] upgrade Package to swift 4 --- Package.swift | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index abda948..01cf4c6 100644 --- a/Package.swift +++ b/Package.swift @@ -1,10 +1,22 @@ -// swift-tools-version:3.1 +// swift-tools-version:4.0 import PackageDescription let package = Package( name: "Stencil", + products: [ + .library(name: "Stencil", targets: ["Stencil"]), + ], dependencies: [ - .Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 9), - .Package(url: "https://github.com/kylef/Spectre.git", majorVersion: 0, minor: 8), + .package(url: "https://github.com/kylef/PathKit.git", from: "0.9.0"), + .package(url: "https://github.com/kylef/Spectre.git", from: "0.8.0"), + ], + targets: [ + .target(name: "Stencil", dependencies: [ + "PathKit", + ], path: "Sources"), + .testTarget(name: "StencilTests", dependencies: [ + "Stencil", + "Spectre", + ]) ] ) From 1098921dc822bb51a8b18ff842160cc5248c2667 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Sat, 1 Sep 2018 19:21:26 +1000 Subject: [PATCH 44/81] remove Swift 3 package --- Package@swift-3.swift | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 Package@swift-3.swift diff --git a/Package@swift-3.swift b/Package@swift-3.swift deleted file mode 100644 index 704b083..0000000 --- a/Package@swift-3.swift +++ /dev/null @@ -1,10 +0,0 @@ -// swift-tools-version:3.1 -import PackageDescription - -let package = Package( - name: "Stencil", - dependencies: [ - .Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 8), - .Package(url: "https://github.com/kylef/Spectre.git", majorVersion: 0, minor: 7), - ] -) From adb443229d866ecc77f99f29d2d94c87e16a668e Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Sat, 1 Sep 2018 19:21:49 +1000 Subject: [PATCH 45/81] add xcodeproject to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f6eb28e..2dcf7f5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ Packages/ Package.resolved Package.pins +*.xcodeproj From 420c0eacd71479b9c2a3efd7efdc3d22fc72b1a0 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Mon, 10 Sep 2018 20:59:02 +1000 Subject: [PATCH 46/81] update code to Swift 4.1 --- Sources/ForTag.swift | 2 +- Sources/Inheritence.swift | 6 +++--- Sources/KeyPath.swift | 2 +- Sources/Parser.swift | 12 ++++++------ Sources/Tokenizer.swift | 4 ++-- Sources/Variable.swift | 4 ++-- Tests/StencilTests/ForNodeSpec.swift | 6 +++--- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index 284498f..e2969a7 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -23,7 +23,7 @@ class ForNode : NodeType { throw TemplateSyntaxError("'for' statements should use the syntax: `for in [where ]`.") } - let loopVariables = components[1].characters + let loopVariables = components[1] .split(separator: ",") .map(String.init) .map { $0.trim(character: " ") } diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index db2d67f..c276d10 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -64,7 +64,7 @@ class ExtendsNode : NodeType { throw TemplateSyntaxError("'extends' cannot appear more than once in the same template") } - let blockNodes = parsedNodes.flatMap { $0 as? BlockNode } + let blockNodes = parsedNodes.compactMap { $0 as? BlockNode } let nodes = blockNodes.reduce([String: BlockNode]()) { (accumulator, node) -> [String: BlockNode] in var dict = accumulator @@ -159,8 +159,8 @@ class BlockNode : NodeType { } // child node is a block node from child template that extends this node (has the same name) - func childContext(_ child: BlockNode, blockContext: BlockContext, context: Context) throws -> [String: Any?] { - var childContext: [String: Any?] = [BlockContext.contextKey: blockContext] + func childContext(_ child: BlockNode, blockContext: BlockContext, context: Context) throws -> [String: Any] { + var childContext: [String: Any] = [BlockContext.contextKey: blockContext] if let blockSuperNode = child.nodes.first(where: { if case .variable(let variable, _)? = $0.token, variable == "block.super" { return true } diff --git a/Sources/KeyPath.swift b/Sources/KeyPath.swift index 445ef29..7728dcf 100644 --- a/Sources/KeyPath.swift +++ b/Sources/KeyPath.swift @@ -24,7 +24,7 @@ final class KeyPath { subscriptLevel = 0 } - for c in variable.characters { + for c in variable { switch c { case "." where subscriptLevel == 0: try foundSeparator() diff --git a/Sources/Parser.swift b/Sources/Parser.swift index b36f160..817b5fd 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -108,7 +108,7 @@ public class TokenParser { let filtersWithDistance = allFilters .map({ (filterName: $0, distance: $0.levenshteinDistance(name)) }) // do not suggest filters which names are shorter than the distance - .filter({ $0.filterName.characters.count > $0.distance }) + .filter({ $0.filterName.count > $0.distance }) guard let minDistance = filtersWithDistance.min(by: { $0.distance < $1.distance })?.distance else { return [] } @@ -167,10 +167,10 @@ extension String { // initialize v0 (the previous row of distances) // this row is A[0][i]: edit distance for an empty s // the distance is just the number of characters to delete from t - last = [Int](0...target.characters.count) - current = [Int](repeating: 0, count: target.characters.count + 1) + last = [Int](0...target.count) + current = [Int](repeating: 0, count: target.count + 1) - for i in 0.. 0 { - if let precedingChar = components.last?.characters.last, specialCharacters.characters.contains(precedingChar) { + if let precedingChar = components.last?.last, specialCharacters.contains(precedingChar) { components[components.count-1] += word } else if specialCharacters.contains(word) { components[components.count-1] += word @@ -25,7 +25,7 @@ extension String { } } - for character in self.characters { + for character in self { if character == "'" { singleQuoteCount += 1 } else if character == "\"" { doubleQuoteCount += 1 } diff --git a/Sources/Variable.swift b/Sources/Variable.swift index 1da7439..7c24e8a 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -9,7 +9,7 @@ class FilterExpression : Resolvable { let variable: Variable init(token: String, parser: TokenParser) throws { - let bits = token.characters.split(separator: "|").map({ String($0).trim(character: " ") }) + let bits = token.split(separator: "|").map({ String($0).trim(character: " ") }) if bits.isEmpty { throw TemplateSyntaxError("Variable tags must include at least 1 argument") } @@ -60,7 +60,7 @@ public struct Variable : Equatable, Resolvable { if (variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\"")) { // String literal - return String(variable[variable.characters.index(after: variable.startIndex) ..< variable.characters.index(before: variable.endIndex)]) + return String(variable[variable.index(after: variable.startIndex) ..< variable.index(before: variable.endIndex)]) } // Number literal diff --git a/Tests/StencilTests/ForNodeSpec.swift b/Tests/StencilTests/ForNodeSpec.swift index ddb7692..084dc51 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -189,7 +189,7 @@ func testForNode() { let template = Template(templateString: templateString) let result = try template.render(context) - let sortedResult = result.characters.split(separator: ",").map(String.init).sorted(by: <) + let sortedResult = result.split(separator: ",").map(String.init).sorted(by: <) try expect(sortedResult) == ["one: I", "two: II"] } @@ -202,7 +202,7 @@ func testForNode() { let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key"], nodes: nodes, emptyNodes: emptyNodes, where: nil) let result = try node.render(context) - let sortedResult = result.characters.split(separator: ",").map(String.init).sorted(by: <) + let sortedResult = result.split(separator: ",").map(String.init).sorted(by: <) try expect(sortedResult) == ["one", "two"] } @@ -218,7 +218,7 @@ func testForNode() { let result = try node.render(context) - let sortedResult = result.characters.split(separator: ",").map(String.init).sorted(by: <) + let sortedResult = result.split(separator: ",").map(String.init).sorted(by: <) try expect(sortedResult) == ["one=I", "two=II"] } From e6b12c09d33a0ff515d5d275a05f0062c7d3f251 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Sat, 1 Sep 2018 19:34:12 +1000 Subject: [PATCH 47/81] update travis builds to Swift 4.1 --- .travis.yml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index a18efef..91cea5e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,10 @@ matrix: include: - os: osx - osx_image: xcode8.3 - env: SWIFT_VERSION=3.1.1 - - os: osx - osx_image: xcode9 - env: SWIFT_VERSION=4.0 - - os: osx - osx_image: xcode9.1 - env: SWIFT_VERSION=4.0 + osx_image: xcode9.3 + env: SWIFT_VERSION=4.1 - os: linux - env: SWIFT_VERSION=3.1.1 - - os: linux - env: SWIFT_VERSION=4.0 + env: SWIFT_VERSION=4.1 language: generic sudo: required dist: trusty From 8bda4d5bbbe8e071deb59faa736d980644f50969 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Sun, 2 Sep 2018 13:10:41 +1000 Subject: [PATCH 48/81] add changelog entry --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d00731..dc7377c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,9 @@ ### Breaking Changes - _None_ +- Now requires Swift 4.1 or newer. + [Yonas Kolb](https://github.com/yonaskolb) + [#228](https://github.com/stencilproject/Stencil/pull/228) ### New Features From 8e9692c6964864e8f9d845f0afff73141b5346be Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Sun, 2 Sep 2018 13:12:20 +1000 Subject: [PATCH 49/81] add swift version to podspec --- Stencil.podspec.json | 1 + 1 file changed, 1 insertion(+) diff --git a/Stencil.podspec.json b/Stencil.podspec.json index fceb79c..d877c7a 100644 --- a/Stencil.podspec.json +++ b/Stencil.podspec.json @@ -23,6 +23,7 @@ "osx": "10.9", "tvos": "9.0" }, + "swift_version": "4.1", "requires_arc": true, "dependencies": { "PathKit": [ From 247a35fd2c5a208e1a768e25f8982fd1f6bf3baf Mon Sep 17 00:00:00 2001 From: David Jennes Date: Sun, 9 Sep 2018 20:42:25 +0200 Subject: [PATCH 50/81] Add CocoaPods version --- Stencil.podspec.json | 1 + 1 file changed, 1 insertion(+) diff --git a/Stencil.podspec.json b/Stencil.podspec.json index d877c7a..a0dde4c 100644 --- a/Stencil.podspec.json +++ b/Stencil.podspec.json @@ -23,6 +23,7 @@ "osx": "10.9", "tvos": "9.0" }, + "cocoapods_version": "1.4.0", "swift_version": "4.1", "requires_arc": true, "dependencies": { From 93ccc56540c07f8a18322c8d643a89a08b394ded Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Mon, 10 Sep 2018 20:57:34 +1000 Subject: [PATCH 51/81] update lexer to swift 4 --- Sources/Errors.swift | 2 +- Sources/Lexer.swift | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 55b976a..07ee702 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -67,7 +67,7 @@ open class SimpleErrorReporter: ErrorReporter { func describe(token: Token) -> String { let templateName = token.sourceMap.filename ?? "" let location = token.sourceMap.location - let highlight = "\(String(Array(repeating: " ", count: location.lineOffset)))^\(String(Array(repeating: "~", count: max(token.contents.characters.count - 1, 0))))" + let highlight = "\(String(Array(repeating: " ", count: location.lineOffset)))^\(String(Array(repeating: "~", count: max(token.contents.count - 1, 0))))" return "\(templateName)\(location.lineNumber):\(location.lineOffset): error: \(templateError.reason)\n" + "\(location.content)\n" diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index fdbc880..26d6a1a 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -11,7 +11,7 @@ struct Lexer { self.templateName = templateName self.templateString = templateString - self.lines = templateString.components(separatedBy: .newlines).enumerated().flatMap { + self.lines = templateString.components(separatedBy: .newlines).enumerated().compactMap { guard !$0.element.isEmpty else { return nil } return (content: $0.element, number: UInt($0.offset + 1), templateString.range(of: $0.element)!) } @@ -19,7 +19,7 @@ struct Lexer { func createToken(string: String, at range: Range) -> Token { func strip() -> String { - guard string.characters.count > 4 else { return "" } + guard string.count > 4 else { return "" } let start = string.index(string.startIndex, offsetBy: 2) let end = string.index(string.endIndex, offsetBy: -2) let trimmed = String(string[start.. Date: Tue, 11 Sep 2018 18:12:27 +0100 Subject: [PATCH 52/81] check for property via selector before using value(forKey:) --- CHANGELOG.md | 4 +++- Sources/Variable.swift | 4 +++- Tests/StencilTests/VariableSpec.swift | 6 ++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc7377c..3ccc740 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ ### Bug Fixes - _None_ +- Now accessing undefined keys in NSObject does not cause runtime crash and instead renders empty string. + [Ilya Puchka](https://github.com/ilyapuchka) + [#234](https://github.com/stencilproject/Stencil/pull/234) ### Breaking Changes diff --git a/Sources/Variable.swift b/Sources/Variable.swift index 7c24e8a..05b8738 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -104,7 +104,9 @@ public struct Variable : Equatable, Resolvable { #if os(Linux) return nil #else - current = object.value(forKey: bit) + if object.responds(to: Selector(bit)) { + current = object.value(forKey: bit) + } #endif } else if let value = current { current = Mirror(reflecting: value).getValue(for: bit) diff --git a/Tests/StencilTests/VariableSpec.swift b/Tests/StencilTests/VariableSpec.swift index 66947e0..8303481 100644 --- a/Tests/StencilTests/VariableSpec.swift +++ b/Tests/StencilTests/VariableSpec.swift @@ -148,6 +148,12 @@ func testVariable() { let result = try variable.resolve(context) as? String try expect(result) == "Foo" } + + $0.it("does not crash on KVO") { + let variable = Variable("object.fullname") + let result = try variable.resolve(context) as? String + try expect(result).to.beNil() + } #endif $0.it("can resolve a value via reflection") { From 8210fa57f14024bc4514dcd9d527b8dd3a847383 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Tue, 11 Sep 2018 18:14:51 +0100 Subject: [PATCH 53/81] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ccc740..275e440 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Bug Fixes -- Now accessing undefined keys in NSObject does not cause runtime crash and instead renders empty string. +- Now accessing undefined keys in NSObject does not cause runtime crash and instead renders empty string. [Ilya Puchka](https://github.com/ilyapuchka) [#234](https://github.com/stencilproject/Stencil/pull/234) From 1704cd2ddff0b5b0d8df2bd3f63d157e6308d27c Mon Sep 17 00:00:00 2001 From: David Jennes Date: Thu, 20 Sep 2018 01:29:38 +0200 Subject: [PATCH 54/81] Use auto equatable --- Sources/Errors.swift | 5 ----- Sources/Tokenizer.swift | 17 ----------------- Sources/Variable.swift | 4 ---- 3 files changed, 26 deletions(-) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 07ee702..787688a 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -37,11 +37,6 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible { public init(_ description: String) { self.init(reason: description) } - - public static func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool { - return lhs.description == rhs.description && lhs.token == rhs.token && lhs.stackTrace == rhs.stackTrace - } - } extension Error { diff --git a/Sources/Tokenizer.swift b/Sources/Tokenizer.swift index f0503ec..54e6891 100644 --- a/Sources/Tokenizer.swift +++ b/Sources/Tokenizer.swift @@ -114,21 +114,4 @@ public enum Token : Equatable { return sourceMap } } - -} - - -public func == (lhs: Token, rhs: Token) -> Bool { - switch (lhs, rhs) { - case let (.text(lhsValue, lhsAt), .text(rhsValue, rhsAt)): - return lhsValue == rhsValue && lhsAt == rhsAt - case let (.variable(lhsValue, lhsAt), .variable(rhsValue, rhsAt)): - return lhsValue == rhsValue && lhsAt == rhsAt - case let (.block(lhsValue, lhsAt), .block(rhsValue, rhsAt)): - return lhsValue == rhsValue && lhsAt == rhsAt - case let (.comment(lhsValue, lhsAt), .comment(rhsValue, rhsAt)): - return lhsValue == rhsValue && lhsAt == rhsAt - default: - return false - } } diff --git a/Sources/Variable.swift b/Sources/Variable.swift index 05b8738..d9620ed 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -128,10 +128,6 @@ public struct Variable : Equatable, Resolvable { } } -public func ==(lhs: Variable, rhs: Variable) -> Bool { - return lhs.variable == rhs.variable -} - /// A structure used to represet range of two integer values expressed as `from...to`. /// Values should be numbers (they will be converted to integers). /// Rendering this variable produces array from range `from...to`. From 0d4dee29b201601ef2f10354d4ae562452a63e92 Mon Sep 17 00:00:00 2001 From: David Jennes Date: Thu, 20 Sep 2018 02:12:12 +0200 Subject: [PATCH 55/81] Use multiline strings multi t t --- Sources/Errors.swift | 13 ++- Sources/Filters.swift | 8 +- Sources/Include.swift | 6 +- Sources/Parser.swift | 5 +- Tests/StencilTests/EnvironmentSpec.swift | 94 +++++++++------- Tests/StencilTests/FilterSpec.swift | 136 ++++++++++++++++++----- Tests/StencilTests/FilterTagSpec.swift | 8 +- Tests/StencilTests/ForNodeSpec.swift | 98 ++++++++++------ Tests/StencilTests/IncludeSpec.swift | 4 +- Tests/StencilTests/InheritenceSpec.swift | 15 ++- Tests/StencilTests/LexerSpec.swift | 19 ++-- Tests/StencilTests/StencilSpec.swift | 24 ++-- 12 files changed, 291 insertions(+), 139 deletions(-) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 787688a..a6191f9 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -62,11 +62,16 @@ open class SimpleErrorReporter: ErrorReporter { func describe(token: Token) -> String { let templateName = token.sourceMap.filename ?? "" let location = token.sourceMap.location - let highlight = "\(String(Array(repeating: " ", count: location.lineOffset)))^\(String(Array(repeating: "~", count: max(token.contents.count - 1, 0))))" + let highlight = """ + \(String(Array(repeating: " ", count: location.lineOffset)))\ + ^\(String(Array(repeating: "~", count: max(token.contents.count - 1, 0)))) + """ - return "\(templateName)\(location.lineNumber):\(location.lineOffset): error: \(templateError.reason)\n" - + "\(location.content)\n" - + "\(highlight)\n" + return """ + \(templateName)\(location.lineNumber):\(location.lineOffset): error: \(templateError.reason) + \(location.content) + \(highlight) + """ } var descriptions = templateError.stackTrace.reduce([]) { $0 + [describe(token: $1)] } diff --git a/Sources/Filters.swift b/Sources/Filters.swift index fece6eb..9832123 100644 --- a/Sources/Filters.swift +++ b/Sources/Filters.swift @@ -74,7 +74,9 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? { var indentWidth = 4 if arguments.count > 0 { guard let value = arguments[0] as? Int else { - throw TemplateSyntaxError("'indent' filter width argument must be an Integer (\(String(describing: arguments[0])))") + throw TemplateSyntaxError(""" + 'indent' filter width argument must be an Integer (\(String(describing: arguments[0]))) + """) } indentWidth = value } @@ -82,7 +84,9 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? { var indentationChar = " " if arguments.count > 1 { guard let value = arguments[1] as? String else { - throw TemplateSyntaxError("'indent' filter indentation argument must be a String (\(String(describing: arguments[1]))") + throw TemplateSyntaxError(""" + 'indent' filter indentation argument must be a String (\(String(describing: arguments[1])) + """) } indentationChar = value } diff --git a/Sources/Include.swift b/Sources/Include.swift index 6dd331c..d6287cc 100644 --- a/Sources/Include.swift +++ b/Sources/Include.swift @@ -10,7 +10,11 @@ class IncludeNode : NodeType { let bits = token.components() guard bits.count == 2 || bits.count == 3 else { - throw TemplateSyntaxError("'include' tag requires one argument, the template file to be included. A second optional argument can be used to specify the context that will be passed to the included file") + throw TemplateSyntaxError(""" + 'include' tag requires one argument, the template file to be included. \ + A second optional argument can be used to specify the context that will \ + be passed to the included file + """) } return IncludeNode(templateName: Variable(bits[1]), includeContext: bits.count == 3 ? bits[2] : nil, token: token) diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 817b5fd..5de93e8 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -98,7 +98,10 @@ public class TokenParser { if suggestedFilters.isEmpty { throw TemplateSyntaxError("Unknown filter '\(name)'.") } else { - throw TemplateSyntaxError("Unknown filter '\(name)'. Found similar filters: \(suggestedFilters.map({ "'\($0)'" }).joined(separator: ", ")).") + throw TemplateSyntaxError(""" + Unknown filter '\(name)'. \ + Found similar filters: \(suggestedFilters.map({ "'\($0)'" }).joined(separator: ", ")). + """) } } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 4c8abed..04ce6c1 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -7,7 +7,7 @@ func testEnvironment() { describe("Environment") { var environment: Environment! var template: Template! - + $0.before { environment = Environment(loader: ExampleLoader()) template = nil @@ -54,7 +54,7 @@ func testEnvironment() { func expectError(reason: String, token: String, file: String = #file, line: Int = #line, function: String = #function) throws { let expectedError = expectedSyntaxError(token: token, template: template, description: reason) - + let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"]), file: file, line: line, function: function).toThrow() as TemplateSyntaxError let reporter = SimpleErrorReporter() @@ -67,58 +67,58 @@ func testEnvironment() { template = "Hello {% for name in %}{{ name }}, {% endfor %}!" try expectError(reason: "'for' statements should use the syntax: `for in [where ]`.", token: "for name in") } - + $0.it("reports syntax error on missing endfor") { template = "{% for name in names %}{{ name }}" try expectError(reason: "`endfor` was not found.", token: "for name in names") } - + $0.it("reports syntax error on unknown tag") { template = "{% for name in names %}{{ name }}{% end %}" try expectError(reason: "Unknown template tag 'end'", token: "end") } } - + $0.context("given unknown filter") { $0.it("reports syntax error in for tag") { template = "{% for name in names|unknown %}{{ name }}{% endfor %}" try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "names|unknown") } - + $0.it("reports syntax error in for-where tag") { template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}" try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") } - + $0.it("reports syntax error in if tag") { template = "{% if name|unknown %}{{ name }}{% endif %}" try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") } - + $0.it("reports syntax error in elif tag") { template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}" try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") } - + $0.it("reports syntax error in ifnot tag") { template = "{% ifnot name|unknown %}{{ name }}{% endif %}" try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") } - + $0.it("reports syntax error in filter tag") { template = "{% filter unknown %}Text{% endfilter %}" try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "filter unknown") } - + $0.it("reports syntax error in variable tag") { template = "{{ name|unknown }}" try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") } } - + $0.context("given rendering error") { $0.it("reports rendering error in variable filter") { @@ -131,7 +131,7 @@ func testEnvironment() { template = Template(templateString: "{{ name|throw }}", environment: environment) try expectError(reason: "filter error", token: "name|throw") } - + $0.it("reports rendering error in filter tag") { let filterExtension = Extension() filterExtension.registerFilter("throw") { (value: Any?) in @@ -142,7 +142,7 @@ func testEnvironment() { template = Template(templateString: "{% filter throw %}Test{% endfilter %}", environment: environment) try expectError(reason: "filter error", token: "filter throw") } - + $0.it("reports rendering error in simple tag") { let tagExtension = Extension() tagExtension.registerSimpleTag("simpletag") { context in @@ -153,23 +153,23 @@ func testEnvironment() { template = Template(templateString: "{% simpletag %}", environment: environment) try expectError(reason: "simpletag error", token: "simpletag") } - + $0.it("reporsts passing argument to simple filter") { template = "{{ name|uppercase:5 }}" try expectError(reason: "cannot invoke filter with an argument", token: "name|uppercase:5") } - + $0.it("reports rendering error in custom tag") { let tagExtension = Extension() tagExtension.registerTag("customtag") { parser, token in return ErrorNode(token: token) } environment.extensions += [tagExtension] - + template = Template(templateString: "{% customtag %}", environment: environment) try expectError(reason: "Custom Error", token: "customtag") } - + $0.it("reports rendering error in for body") { let tagExtension = Extension() tagExtension.registerTag("customtag") { parser, token in @@ -180,7 +180,7 @@ func testEnvironment() { template = Template(templateString: "{% for name in names %}{% customtag %}{% endfor %}", environment: environment) try expectError(reason: "Custom Error", token: "customtag") } - + $0.it("reports rendering error in block") { let tagExtension = Extension() tagExtension.registerTag("customtag") { parser, token in @@ -192,48 +192,54 @@ func testEnvironment() { try expectError(reason: "Custom Error", token: "customtag") } } - + $0.context("given included template") { let path = Path(#file) + ".." + "fixtures" let loader = FileSystemLoader(paths: [path]) var environment = Environment(loader: loader) var template: Template! var includedTemplate: Template! - + $0.before { environment = Environment(loader: loader) template = nil includedTemplate = nil } - + func expectError(reason: String, token: String, includedToken: String, file: String = #file, line: Int = #line, function: String = #function) throws { var expectedError = expectedSyntaxError(token: token, template: template, description: reason) expectedError.stackTrace = [expectedSyntaxError(token: includedToken, template: includedTemplate, description: reason).token!] - + let error = try expect(environment.render(template: template, context: ["target": "World"]), file: file, line: line, function: function).toThrow() as TemplateSyntaxError let reporter = SimpleErrorReporter() try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError) } - + $0.it("reports syntax error in included template") { - template = Template(templateString: "{% include \"invalid-include.html\" %}", environment: environment) + template = Template(templateString: """ + {% include "invalid-include.html" %} + """, environment: environment) includedTemplate = try environment.loadTemplate(name: "invalid-include.html") try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", - token: "include \"invalid-include.html\"", + token: """ + include "invalid-include.html" + """, includedToken: "target|unknown") } - + $0.it("reports runtime error in included template") { let filterExtension = Extension() filterExtension.registerFilter("unknown", filter: { (_: Any?) in throw TemplateSyntaxError("filter error") }) environment.extensions += [filterExtension] - - template = Template(templateString: "{% include \"invalid-include.html\" %}", environment: environment) + + template = Template(templateString: """ + {% include "invalid-include.html" %} + """, environment: environment) includedTemplate = try environment.loadTemplate(name: "invalid-include.html") try expectError(reason: "filter error", @@ -242,7 +248,7 @@ func testEnvironment() { } } - + $0.context("given base and child templates") { let path = Path(#file) + ".." + "fixtures" let loader = FileSystemLoader(paths: [path]) @@ -255,7 +261,7 @@ func testEnvironment() { childTemplate = nil baseTemplate = nil } - + func expectError(reason: String, childToken: String, baseToken: String?, file: String = #file, line: Int = #line, function: String = #function) throws { var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason) @@ -271,12 +277,12 @@ func testEnvironment() { $0.it("reports syntax error in base template") { childTemplate = try environment.loadTemplate(name: "invalid-child-super.html") baseTemplate = try environment.loadTemplate(name: "invalid-base.html") - + try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", childToken: "extends \"invalid-base.html\"", baseToken: "target|unknown") } - + $0.it("reports runtime error in base template") { let filterExtension = Extension() filterExtension.registerFilter("unknown", filter: { (_: Any?) in @@ -291,16 +297,18 @@ func testEnvironment() { childToken: "block.super", baseToken: "target|unknown") } - + $0.it("reports syntax error in child template") { - childTemplate = Template(templateString: "{% extends \"base.html\" %}\n" + - "{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil) - + childTemplate = Template(templateString: """ + {% extends "base.html" %} + {% block body %}Child {{ target|unknown }}{% endblock %} + """, environment: environment, name: nil) + try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", childToken: "target|unknown", baseToken: nil) } - + $0.it("reports runtime error in child template") { let filterExtension = Extension() filterExtension.registerFilter("unknown", filter: { (_: Any?) in @@ -308,8 +316,10 @@ func testEnvironment() { }) environment.extensions += [filterExtension] - childTemplate = Template(templateString: "{% extends \"base.html\" %}\n" + - "{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil) + childTemplate = Template(templateString: """ + {% extends "base.html" %} + {% block body %}Child {{ target|unknown }}{% endblock %} + """, environment: environment, name: nil) try expectError(reason: "filter error", childToken: "target|unknown", @@ -325,13 +335,13 @@ extension Expectation { @discardableResult func toThrow() throws -> T { var thrownError: Error? = nil - + do { _ = try expression() } catch { thrownError = error } - + if let thrownError = thrownError { if let thrownError = thrownError as? T { return thrownError diff --git a/Tests/StencilTests/FilterSpec.swift b/Tests/StencilTests/FilterSpec.swift index fe40fc4..d798632 100644 --- a/Tests/StencilTests/FilterSpec.swift +++ b/Tests/StencilTests/FilterSpec.swift @@ -23,7 +23,9 @@ func testFilter() { } $0.it("allows you to register a custom filter which accepts single argument") { - let template = Template(templateString: "{{ name|repeat:'value1, \"value2\"' }}") + let template = Template(templateString: """ + {{ name|repeat:'value1, "value2"' }} + """) let repeatExtension = Extension() repeatExtension.registerFilter("repeat") { value, arguments in @@ -35,11 +37,15 @@ func testFilter() { } let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))) - try expect(result) == "Kyle Kyle with args value1, \"value2\"" + try expect(result) == """ + Kyle Kyle with args value1, "value2" + """ } $0.it("allows you to register a custom filter which accepts several arguments") { - let template = Template(templateString: "{{ name|repeat:'value\"1\"',\"value'2'\",'(key, value)' }}") + let template = Template(templateString: """ + {{ name|repeat:'value"1"',"value'2'",'(key, value)' }} + """) let repeatExtension = Extension() repeatExtension.registerFilter("repeat") { value, arguments in @@ -51,7 +57,9 @@ func testFilter() { } let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))) - try expect(result) == "Kyle Kyle with args 0: value\"1\", 1: value'2', 2: (key, value)" + try expect(result) == """ + Kyle Kyle with args 0: value"1", 1: value'2', 2: (key, value) + """ } $0.it("allows you to register a custom which throws") { @@ -78,7 +86,9 @@ func testFilter() { } $0.it("allows whitespace in expression") { - let template = Template(templateString: "{{ value | join : \", \" }}") + let template = Template(templateString: """ + {{ value | join : ", " }} + """) let result = try template.render(Context(dictionary: ["value": ["One", "Two"]])) try expect(result) == "One, Two" } @@ -114,25 +124,33 @@ func testFilter() { $0.it("transforms a string to be capitalized") { let template = Template(templateString: "{{ names|capitalize }}") let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]])) - try expect(result) == "[\"Kyle\", \"Kyle\"]" + try expect(result) == """ + ["Kyle", "Kyle"] + """ } $0.it("transforms a string to be uppercase") { let template = Template(templateString: "{{ names|uppercase }}") let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]])) - try expect(result) == "[\"KYLE\", \"KYLE\"]" + try expect(result) == """ + ["KYLE", "KYLE"] + """ } $0.it("transforms a string to be lowercase") { let template = Template(templateString: "{{ names|lowercase }}") let result = try template.render(Context(dictionary: ["names": ["Kyle", "Kyle"]])) - try expect(result) == "[\"kyle\", \"kyle\"]" + try expect(result) == """ + ["kyle", "kyle"] + """ } } } describe("default filter") { - let template = Template(templateString: "Hello {{ name|default:\"World\" }}") + let template = Template(templateString: """ + Hello {{ name|default:"World" }} + """) $0.it("shows the variable value") { let result = try template.render(Context(dictionary: ["name": "Kyle"])) @@ -145,7 +163,9 @@ func testFilter() { } $0.it("supports multiple defaults") { - let template = Template(templateString: "Hello {{ name|default:a,b,c,\"World\" }}") + let template = Template(templateString: """ + Hello {{ name|default:a,b,c,"World" }} + """) let result = try template.render(Context(dictionary: [:])) try expect(result) == "Hello World" } @@ -163,7 +183,9 @@ func testFilter() { } $0.it("checks for underlying nil value correctly") { - let template = Template(templateString: "Hello {{ user.name|default:\"anonymous\" }}") + let template = Template(templateString: """ + Hello {{ user.name|default:"anonymous" }} + """) let nilName: String? = nil let user: [String: Any?] = ["name": nilName] let result = try template.render(Context(dictionary: ["user": user])) @@ -172,7 +194,9 @@ func testFilter() { } describe("join filter") { - let template = Template(templateString: "{{ value|join:\", \" }}") + let template = Template(templateString: """ + {{ value|join:", " }} + """) $0.it("joins a collection of strings") { let result = try template.render(Context(dictionary: ["value": ["One", "Two"]])) @@ -185,30 +209,42 @@ func testFilter() { } $0.it("can join by non string") { - let template = Template(templateString: "{{ value|join:separator }}") + let template = Template(templateString: """ + {{ value|join:separator }} + """) let result = try template.render(Context(dictionary: ["value": ["One", "Two"], "separator": true])) try expect(result) == "OnetrueTwo" } $0.it("can join without arguments") { - let template = Template(templateString: "{{ value|join }}") + let template = Template(templateString: """ + {{ value|join }} + """) let result = try template.render(Context(dictionary: ["value": ["One", "Two"]])) try expect(result) == "OneTwo" } } describe("split filter") { - let template = Template(templateString: "{{ value|split:\", \" }}") + let template = Template(templateString: """ + {{ value|split:", " }} + """) $0.it("split a string into array") { let result = try template.render(Context(dictionary: ["value": "One, Two"])) - try expect(result) == "[\"One\", \"Two\"]" + try expect(result) == """ + ["One", "Two"] + """ } $0.it("can split without arguments") { - let template = Template(templateString: "{{ value|split }}") + let template = Template(templateString: """ + {{ value|split }} + """) let result = try template.render(Context(dictionary: ["value": "One, Two"])) - try expect(result) == "[\"One,\", \"Two\"]" + try expect(result) == """ + ["One,", "Two"] + """ } } @@ -268,27 +304,67 @@ func testFilter() { describe("indent filter") { $0.it("indents content") { - let template = Template(templateString: "{{ value|indent:2 }}") - let result = try template.render(Context(dictionary: ["value": "One\nTwo"])) - try expect(result) == "One\n Two" + let template = Template(templateString: """ + {{ value|indent:2 }} + """) + let result = try template.render(Context(dictionary: ["value": """ + One + Two + """])) + try expect(result) == """ + One + Two + """ } $0.it("can indent with arbitrary character") { - let template = Template(templateString: "{{ value|indent:2,\"\t\" }}") - let result = try template.render(Context(dictionary: ["value": "One\nTwo"])) - try expect(result) == "One\n\t\tTwo" + let template = Template(templateString: """ + {{ value|indent:2,"\t" }} + """) + let result = try template.render(Context(dictionary: ["value": """ + One + Two + """])) + try expect(result) == """ + One + \t\tTwo + """ } $0.it("can indent first line") { - let template = Template(templateString: "{{ value|indent:2,\" \",true }}") - let result = try template.render(Context(dictionary: ["value": "One\nTwo"])) - try expect(result) == " One\n Two" + let template = Template(templateString: """ + {{ value|indent:2," ",true }} + """) + let result = try template.render(Context(dictionary: ["value": """ + One + Two + """])) + try expect(result) == """ + One + Two + """ } $0.it("does not indent empty lines") { - let template = Template(templateString: "{{ value|indent }}") - let result = try template.render(Context(dictionary: ["value": "One\n\n\nTwo\n\n"])) - try expect(result) == "One\n\n\n Two\n\n" + let template = Template(templateString: """ + {{ value|indent }} + """) + let result = try template.render(Context(dictionary: ["value": """ + One + + + Two + + + """])) + try expect(result) == """ + One + + + Two + + + """ } } } diff --git a/Tests/StencilTests/FilterTagSpec.swift b/Tests/StencilTests/FilterTagSpec.swift index 5d59fdc..4899531 100644 --- a/Tests/StencilTests/FilterTagSpec.swift +++ b/Tests/StencilTests/FilterTagSpec.swift @@ -27,7 +27,9 @@ func testFilterTag() { return ($0 as! String).components(separatedBy: $1[0] as! String) }) let env = Environment(extensions: [ext]) - let result = try env.renderTemplate(string: "{% filter split:\",\"|join:\";\" %}{{ items|join:\",\" }}{% endfilter %}", context: ["items": [1, 2]]) + let result = try env.renderTemplate(string: """ + {% filter split:","|join:";" %}{{ items|join:"," }}{% endfilter %} + """, context: ["items": [1, 2]]) try expect(result) == "1;2" } @@ -38,7 +40,9 @@ func testFilterTag() { return ($0 as! String).replacingOccurrences(of: $1[0] as! String, with: $1[1] as! String) }) let env = Environment(extensions: [ext]) - let result = try env.renderTemplate(string: "{% filter replace:'\"',\"\" %}{{ items|join:\",\" }}{% endfilter %}", context: ["items": ["\"1\"", "\"2\""]]) + let result = try env.renderTemplate(string: """ + {% filter replace:'"',"" %}{{ items|join:"," }}{% endfilter %} + """, context: ["items": ["\"1\"", "\"2\""]]) try expect(result) == "1,2" } } diff --git a/Tests/StencilTests/ForNodeSpec.swift b/Tests/StencilTests/ForNodeSpec.swift index 084dc51..f292a15 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -112,9 +112,11 @@ func testForNode() { } $0.it("can render a filter with spaces") { - let templateString = "{% for article in ars | default: a, b , articles %}" + - "- {{ article.title }} by {{ article.author }}.\n" + - "{% endfor %}\n" + let templateString = """ + {% for article in ars | default: a, b , articles %}\ + - {{ article.title }} by {{ article.author }}. + {% endfor %} + """ let context = Context(dictionary: [ "articles": [ @@ -126,54 +128,70 @@ func testForNode() { let template = Template(templateString: templateString) let result = try template.render(context) - let fixture = "" + - "- Migrating from OCUnit to XCTest by Kyle Fuller.\n" + - "- Memory Management with ARC by Kyle Fuller.\n" + - "\n" + try expect(result) == """ + - Migrating from OCUnit to XCTest by Kyle Fuller. + - Memory Management with ARC by Kyle Fuller. - try expect(result) == fixture + """ } $0.context("given array of tuples") { $0.it("can iterate over all tuple values") { - let templateString = "{% for first,second,third in tuples %}" + - "{{ first }}, {{ second }}, {{ third }}\n" + - "{% endfor %}\n" + let templateString = """ + {% for first,second,third in tuples %}\ + {{ first }}, {{ second }}, {{ third }} + {% endfor %} + """ let template = Template(templateString: templateString) let result = try template.render(context) - let fixture = "1, 2, 3\n4, 5, 6\n\n" - try expect(result) == fixture + try expect(result) == """ + 1, 2, 3 + 4, 5, 6 + + """ } $0.it("can iterate with less number of variables") { - let templateString = "{% for first,second in tuples %}" + - "{{ first }}, {{ second }}\n" + - "{% endfor %}\n" + let templateString = """ + {% for first,second in tuples %}\ + {{ first }}, {{ second }} + {% endfor %} + """ let template = Template(templateString: templateString) let result = try template.render(context) - let fixture = "1, 2\n4, 5\n\n" - try expect(result) == fixture + try expect(result) == """ + 1, 2 + 4, 5 + + """ } $0.it("can use _ to skip variables") { - let templateString = "{% for first,_,third in tuples %}" + - "{{ first }}, {{ third }}\n" + - "{% endfor %}\n" + let templateString = """ + {% for first,_,third in tuples %}\ + {{ first }}, {{ third }} + {% endfor %} + """ let template = Template(templateString: templateString) let result = try template.render(context) - let fixture = "1, 3\n4, 6\n\n" - try expect(result) == fixture + try expect(result) == """ + 1, 3 + 4, 6 + + """ } $0.it("throws when number of variables is more than number of tuple values") { - let templateString = "{% for key,value,smth in dict %}" + - "{% endfor %}\n" + let templateString = """ + {% for key,value,smth in dict %} + {% endfor %} + """ let template = Template(templateString: templateString) try expect(template.render(context)).toThrow() @@ -182,9 +200,11 @@ func testForNode() { } $0.it("can iterate over dictionary") { - let templateString = "{% for key, value in dict %}" + - "{{ key }}: {{ value }}," + - "{% endfor %}" + let templateString = """ + {% for key, value in dict %}\ + {{ key }}: {{ value }},\ + {% endfor %} + """ let template = Template(templateString: templateString) let result = try template.render(context) @@ -248,7 +268,11 @@ func testForNode() { 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" + try expect(result) == """ + string=abc + number=123 + + """ } $0.it("can iterate tuple items") { @@ -266,7 +290,11 @@ func testForNode() { 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" + try expect(result) == """ + one=1 + two=dva + + """ } $0.it("can iterate over class properties") { @@ -297,11 +325,16 @@ func testForNode() { 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" + try expect(result) == """ + childString=child + baseString=base + baseInt=1 + + """ } $0.it("can iterate in range of variables") { @@ -313,7 +346,6 @@ func testForNode() { } - fileprivate struct Article { let title: String let author: String diff --git a/Tests/StencilTests/IncludeSpec.swift b/Tests/StencilTests/IncludeSpec.swift index a87dc85..cf51546 100644 --- a/Tests/StencilTests/IncludeSpec.swift +++ b/Tests/StencilTests/IncludeSpec.swift @@ -58,7 +58,9 @@ func testInclude() { } $0.it("successfully passes context") { - let template = Template(templateString: "{% include \"test.html\" child %}") + let template = Template(templateString: """ + {% include "test.html" child %} + """) let context = Context(dictionary: ["child": ["target": "World"]], environment: environment) let value = try template.render(context) try expect(value) == "Hello World!" diff --git a/Tests/StencilTests/InheritenceSpec.swift b/Tests/StencilTests/InheritenceSpec.swift index a58107a..68ef597 100644 --- a/Tests/StencilTests/InheritenceSpec.swift +++ b/Tests/StencilTests/InheritenceSpec.swift @@ -11,17 +11,26 @@ func testInheritence() { $0.it("can inherit from another template") { let template = try environment.loadTemplate(name: "child.html") - try expect(try template.render()) == "Super_Header Child_Header\nChild_Body" + try expect(try template.render()) == """ + Super_Header Child_Header + Child_Body + """ } $0.it("can inherit from another template inheriting from another template") { let template = try environment.loadTemplate(name: "child-child.html") - try expect(try template.render()) == "Super_Header Child_Header Child_Child_Header\nChild_Body" + try expect(try template.render()) == """ + Super_Header Child_Header Child_Child_Header + Child_Body + """ } $0.it("can inherit from a template that calls a super block") { let template = try environment.loadTemplate(name: "child-super.html") - try expect(try template.render()) == "Header\nChild_Body" + try expect(try template.render()) == """ + Header + Child_Body + """ } } } diff --git a/Tests/StencilTests/LexerSpec.swift b/Tests/StencilTests/LexerSpec.swift index 1babed9..fd3493d 100644 --- a/Tests/StencilTests/LexerSpec.swift +++ b/Tests/StencilTests/LexerSpec.swift @@ -69,15 +69,16 @@ func testLexer() { } $0.it("can tokenize with new lines") { - let templateString = - "My name is {%\n" + - " if name\n" + - " and\n" + - " name\n" + - "%}{{\n" + - "name\n" + - "}}{%\n" + - "endif %}." + let templateString = """ + My name is {% + if name + and + name + %}{{ + name + }}{% + endif %}. + """ let lexer = Lexer(templateString: templateString) diff --git a/Tests/StencilTests/StencilSpec.swift b/Tests/StencilTests/StencilSpec.swift index 099a407..137f5be 100644 --- a/Tests/StencilTests/StencilSpec.swift +++ b/Tests/StencilTests/StencilSpec.swift @@ -32,11 +32,13 @@ func testStencil() { $0.it("can render the README example") { - let templateString = "There are {{ articles.count }} articles.\n" + - "\n" + - "{% for article in articles %}" + - " - {{ article.title }} by {{ article.author }}.\n" + - "{% endfor %}\n" + let templateString = """ + There are {{ articles.count }} articles. + + {% for article in articles %}\ + - {{ article.title }} by {{ article.author }}. + {% endfor %} + """ let context = [ "articles": [ @@ -48,13 +50,13 @@ func testStencil() { let template = Template(templateString: templateString) let result = try template.render(context) - let fixture = "There are 2 articles.\n" + - "\n" + - " - Migrating from OCUnit to XCTest by Kyle Fuller.\n" + - " - Memory Management with ARC by Kyle Fuller.\n" + - "\n" + try expect(result) == """ + There are 2 articles. - try expect(result) == fixture + - Migrating from OCUnit to XCTest by Kyle Fuller. + - Memory Management with ARC by Kyle Fuller. + + """ } $0.it("can render a custom template tag") { From f9f6d95f2502a726f556b3e7d8d45a532aa006ee Mon Sep 17 00:00:00 2001 From: David Jennes Date: Thu, 20 Sep 2018 02:39:00 +0200 Subject: [PATCH 56/81] Changelog entry --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 275e440..ac88695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,11 +16,13 @@ ### New Features - _None_ +_None_ ### Internal Changes - _None_ +- Updated the codebase to use Swift 4 features. + [David Jennes](https://github.com/djbe) + [#239](https://github.com/stencilproject/Stencil/pull/239) ## 0.12.1 From 9c408d488eaf9c4ea41ca258875304ed186e53d7 Mon Sep 17 00:00:00 2001 From: David Jennes Date: Thu, 20 Sep 2018 02:39:09 +0200 Subject: [PATCH 57/81] Test on Xcode 10 and Linux Swift 4.2 --- .travis.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 91cea5e..9240617 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,15 @@ matrix: include: - os: osx - osx_image: xcode9.3 + osx_image: xcode9.4 env: SWIFT_VERSION=4.1 + - os: osx + osx_image: xcode10 + env: SWIFT_VERSION=4.2 - os: linux env: SWIFT_VERSION=4.1 + - os: linux + env: SWIFT_VERSION=4.2 language: generic sudo: required dist: trusty From fce4e85a632389588223b599b0c7d2c2d81fd153 Mon Sep 17 00:00:00 2001 From: David Jennes Date: Thu, 20 Sep 2018 02:59:00 +0200 Subject: [PATCH 58/81] Ensure the "for" iteration over a dictionary is consistent --- Sources/ForTag.swift | 2 +- Tests/StencilTests/ForNodeSpec.swift | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Sources/ForTag.swift b/Sources/ForTag.swift index e2969a7..0bc8775 100644 --- a/Sources/ForTag.swift +++ b/Sources/ForTag.swift @@ -93,7 +93,7 @@ class ForNode : NodeType { var values: [Any] if let dictionary = resolved as? [String: Any], !dictionary.isEmpty { - values = dictionary.map { ($0.key, $0.value) } + values = dictionary.sorted { $0.key < $1.key } } else if let array = resolved as? [Any] { values = array } else if let range = resolved as? CountableClosedRange { diff --git a/Tests/StencilTests/ForNodeSpec.swift b/Tests/StencilTests/ForNodeSpec.swift index f292a15..e362e91 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -209,8 +209,9 @@ func testForNode() { let template = Template(templateString: templateString) let result = try template.render(context) - let sortedResult = result.split(separator: ",").map(String.init).sorted(by: <) - try expect(sortedResult) == ["one: I", "two: II"] + try expect(result) == """ + one: I,two: II, + """ } $0.it("renders supports iterating over dictionary") { @@ -222,8 +223,9 @@ func testForNode() { let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key"], nodes: nodes, emptyNodes: emptyNodes, where: nil) let result = try node.render(context) - let sortedResult = result.split(separator: ",").map(String.init).sorted(by: <) - try expect(sortedResult) == ["one", "two"] + try expect(result) == """ + one,two, + """ } $0.it("renders supports iterating over dictionary") { @@ -235,11 +237,11 @@ func testForNode() { ] let emptyNodes: [NodeType] = [TextNode(text: "empty")] let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key", "value"], nodes: nodes, emptyNodes: emptyNodes, where: nil) - let result = try node.render(context) - let sortedResult = result.split(separator: ",").map(String.init).sorted(by: <) - try expect(sortedResult) == ["one=I", "two=II"] + try expect(result) == """ + one=I,two=II, + """ } $0.it("handles invalid input") { From 064b2f706c68594802b6f735421a1784938db60a Mon Sep 17 00:00:00 2001 From: David Jennes Date: Thu, 20 Sep 2018 03:01:07 +0200 Subject: [PATCH 59/81] Changelog entry --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac88695..8692727 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ - Now accessing undefined keys in NSObject does not cause runtime crash and instead renders empty string. [Ilya Puchka](https://github.com/ilyapuchka) [#234](https://github.com/stencilproject/Stencil/pull/234) +- `for` tag: When iterating over a dictionary the keys will now always be sorted (in an ascending order) to ensure consistent output generation. + [David Jennes](https://github.com/djbe) + [#240](https://github.com/stencilproject/Stencil/pull/240) ### Breaking Changes From 2c3962a3de585a0e250539ceef23b68ef64fa929 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Fri, 21 Sep 2018 22:07:28 +0300 Subject: [PATCH 60/81] Added support for brackets in boolean expressions (#165) * added support for brackets in boolean expressions * more descriptive error messages * use array slices * added test for nested expressions * removed brackets validation step * address code review comments * added doc comment * simplify expression spec * fixed docs --- CHANGELOG.md | 3 + Sources/IfTag.swift | 89 ++++++++++++++++++++----- Tests/StencilTests/ExpressionSpec.swift | 64 ++++++++++++++---- docs/builtins.rst | 13 ++++ 4 files changed, 140 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8692727..86d4ec9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ - Now requires Swift 4.1 or newer. [Yonas Kolb](https://github.com/yonaskolb) [#228](https://github.com/stencilproject/Stencil/pull/228) +- You can now use parentheses in boolean expressions to change operator precedence. + [Ilya Puchka](https://github.com/ilyapuchka) + [#165](https://github.com/stencilproject/Stencil/pull/165) ### New Features diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index e014812..997f4fb 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -38,10 +38,11 @@ func findOperator(name: String) -> Operator? { } -enum IfToken { - case infix(name: String, bindingPower: Int, op: InfixOperator.Type) - case prefix(name: String, bindingPower: Int, op: PrefixOperator.Type) +indirect enum IfToken { + case infix(name: String, bindingPower: Int, operatorType: InfixOperator.Type) + case prefix(name: String, bindingPower: Int, operatorType: PrefixOperator.Type) case variable(Resolvable) + case subExpression(Expression) case end var bindingPower: Int { @@ -52,6 +53,8 @@ enum IfToken { return bindingPower case .variable(_): return 0 + case .subExpression(_): + return 0 case .end: return 0 } @@ -66,6 +69,8 @@ enum IfToken { return op.init(expression: expression) case .variable(let variable): return VariableExpression(variable: variable) + case .subExpression(let expression): + return expression case .end: throw TemplateSyntaxError("'if' expression error: end") } @@ -80,6 +85,8 @@ enum IfToken { throw TemplateSyntaxError("'if' expression error: prefix operator '\(name)' was called with a left hand side") case .variable(let variable): throw TemplateSyntaxError("'if' expression error: variable '\(variable)' was called with a left hand side") + case .subExpression(_): + throw TemplateSyntaxError("'if' expression error: sub expression was called with a left hand side") case .end: throw TemplateSyntaxError("'if' expression error: end") } @@ -100,21 +107,71 @@ final class IfExpressionParser { let tokens: [IfToken] var position: Int = 0 - init(components: [String], tokenParser: TokenParser, token: Token) throws { - self.tokens = try components.map { component in - if let op = findOperator(name: component) { - switch op { - case .infix(let name, let bindingPower, let cls): - return .infix(name: name, bindingPower: bindingPower, op: cls) - case .prefix(let name, let bindingPower, let cls): - return .prefix(name: name, bindingPower: bindingPower, op: cls) - } - } + private init(tokens: [IfToken]) { + self.tokens = tokens + } + + static func parser(components: [String], tokenParser: TokenParser, token: Token) throws -> IfExpressionParser { + return try IfExpressionParser(components: ArraySlice(components), tokenParser: tokenParser, token: token) + } - return .variable(try tokenParser.compileResolvable(component, containedIn: token)) + private init(components: ArraySlice, tokenParser: TokenParser, token: Token) throws { + var parsedComponents = Set() + var bracketsBalance = 0 + self.tokens = try zip(components.indices, components).flatMap { (index, component) in + guard !parsedComponents.contains(index) else { return nil } + + if component == "(" { + bracketsBalance += 1 + let (expression, parsedCount) = try IfExpressionParser.subExpression( + from: components.suffix(from: index + 1), + tokenParser: tokenParser, + token: token + ) + parsedComponents.formUnion(Set(index...(index + parsedCount))) + return .subExpression(expression) + } else if component == ")" { + bracketsBalance -= 1 + if bracketsBalance < 0 { + throw TemplateSyntaxError("'if' expression error: missing opening bracket") + } + parsedComponents.insert(index) + return nil + } else { + parsedComponents.insert(index) + if let op = findOperator(name: component) { + switch op { + case .infix(let name, let bindingPower, let operatorType): + return .infix(name: name, bindingPower: bindingPower, operatorType: operatorType) + case .prefix(let name, let bindingPower, let operatorType): + return .prefix(name: name, bindingPower: bindingPower, operatorType: operatorType) + } + } + return .variable(try tokenParser.compileResolvable(component, containedIn: token)) + } } } + private static func subExpression(from components: ArraySlice, tokenParser: TokenParser, token: Token) throws -> (Expression, Int) { + var bracketsBalance = 1 + let subComponents = components + .prefix(while: { + if $0 == "(" { + bracketsBalance += 1 + } else if $0 == ")" { + bracketsBalance -= 1 + } + return bracketsBalance != 0 + }) + if bracketsBalance > 0 { + throw TemplateSyntaxError("'if' expression error: missing closing bracket") + } + + let expressionParser = try IfExpressionParser(components: subComponents, tokenParser: tokenParser, token: token) + let expression = try expressionParser.parse() + return (expression, subComponents.count) + } + var currentToken: IfToken { if tokens.count > position { return tokens[position] @@ -154,13 +211,11 @@ final class IfExpressionParser { } } - func parseExpression(components: [String], tokenParser: TokenParser, token: Token) throws -> Expression { - let parser = try IfExpressionParser(components: components, tokenParser: tokenParser, token: token) + let parser = try IfExpressionParser.parser(components: components, tokenParser: tokenParser, token: token) return try parser.parse() } - /// Represents an if condition and the associated nodes when the condition /// evaluates final class IfCondition { diff --git a/Tests/StencilTests/ExpressionSpec.swift b/Tests/StencilTests/ExpressionSpec.swift index e53ffab..e04408b 100644 --- a/Tests/StencilTests/ExpressionSpec.swift +++ b/Tests/StencilTests/ExpressionSpec.swift @@ -6,6 +6,11 @@ func testExpressions() { describe("Expression") { let parser = TokenParser(tokens: [], environment: Environment()) + func parseExpression(components: [String]) throws -> Expression { + let parser = try IfExpressionParser.parser(components: components, tokenParser: parser, token: .text(value: "", at: .unknown)) + return try parser.parse() + } + $0.describe("VariableExpression") { let expression = VariableExpression(variable: Variable("value")) @@ -105,19 +110,19 @@ func testExpressions() { $0.describe("expression parsing") { $0.it("can parse a variable expression") { - let expression = try parseExpression(components: ["value"], tokenParser: parser, token: .text(value: "", at: .unknown)) + let expression = try parseExpression(components: ["value"]) try expect(expression.evaluate(context: Context())).to.beFalse() try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue() } $0.it("can parse a not expression") { - let expression = try parseExpression(components: ["not", "value"], tokenParser: parser, token: .text(value: "", at: .unknown)) + let expression = try parseExpression(components: ["not", "value"]) try expect(expression.evaluate(context: Context())).to.beTrue() try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse() } $0.describe("and expression") { - let expression = try! parseExpression(components: ["lhs", "and", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) + let expression = try! parseExpression(components: ["lhs", "and", "rhs"]) $0.it("evaluates to false with lhs false") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse() @@ -137,7 +142,7 @@ func testExpressions() { } $0.describe("or expression") { - let expression = try! parseExpression(components: ["lhs", "or", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) + let expression = try! parseExpression(components: ["lhs", "or", "rhs"]) $0.it("evaluates to true with lhs true") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue() @@ -157,7 +162,7 @@ func testExpressions() { } $0.describe("equality expression") { - let expression = try! parseExpression(components: ["lhs", "==", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) + let expression = try! parseExpression(components: ["lhs", "==", "rhs"]) $0.it("evaluates to true with equal lhs/rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue() @@ -193,7 +198,7 @@ func testExpressions() { } $0.describe("inequality expression") { - let expression = try! parseExpression(components: ["lhs", "!=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) + let expression = try! parseExpression(components: ["lhs", "!=", "rhs"]) $0.it("evaluates to true with inequal lhs/rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue() @@ -205,7 +210,7 @@ func testExpressions() { } $0.describe("more than expression") { - let expression = try! parseExpression(components: ["lhs", ">", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) + let expression = try! parseExpression(components: ["lhs", ">", "rhs"]) $0.it("evaluates to true with lhs > rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue() @@ -217,7 +222,7 @@ func testExpressions() { } $0.describe("more than equal expression") { - let expression = try! parseExpression(components: ["lhs", ">=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) + let expression = try! parseExpression(components: ["lhs", ">=", "rhs"]) $0.it("evaluates to true with lhs == rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() @@ -229,7 +234,7 @@ func testExpressions() { } $0.describe("less than expression") { - let expression = try! parseExpression(components: ["lhs", "<", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) + let expression = try! parseExpression(components: ["lhs", "<", "rhs"]) $0.it("evaluates to true with lhs < rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue() @@ -241,7 +246,7 @@ func testExpressions() { } $0.describe("less than equal expression") { - let expression = try! parseExpression(components: ["lhs", "<=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) + let expression = try! parseExpression(components: ["lhs", "<=", "rhs"]) $0.it("evaluates to true with lhs == rhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() @@ -253,7 +258,7 @@ func testExpressions() { } $0.describe("multiple expression") { - let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"], tokenParser: parser, token: .text(value: "", at: .unknown)) + let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"]) $0.it("evaluates to true with one") { try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue() @@ -281,7 +286,7 @@ func testExpressions() { } $0.describe("in expression") { - let expression = try! parseExpression(components: ["lhs", "in", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown)) + let expression = try! parseExpression(components: ["lhs", "in", "rhs"]) $0.it("evaluates to true when rhs contains lhs") { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue() @@ -299,6 +304,41 @@ func testExpressions() { try expect(expression.evaluate(context: Context(dictionary: ["lhs": 3, "rhs": 1..<3]))).to.beFalse() } } + + $0.describe("sub expression") { + $0.it("evaluates correctly") { + let context = Context(dictionary: ["one": false, "two": false, "three": true, "four": true]) + + let expression = try! parseExpression(components: ["one", "and", "two", "or", "three", "and", "four"]) + let expressionWithBrackets = try! parseExpression(components: ["one", "and", "(", "(", "two", ")", "or", "(", "three", "and", "four", ")", ")"]) + + try expect(expression.evaluate(context: context)).to.beTrue() + try expect(expressionWithBrackets.evaluate(context: context)).to.beFalse() + + let notExpression = try! parseExpression(components: ["not", "one", "or", "three"]) + let notExpressionWithBrackets = try! parseExpression(components: ["not", "(", "one", "or", "three", ")"]) + + try expect(notExpression.evaluate(context: context)).to.beTrue() + try expect(notExpressionWithBrackets.evaluate(context: context)).to.beFalse() + } + + $0.it("fails when brackets are not balanced") { + try expect(parseExpression(components: ["(", "lhs", "and", "rhs"])) + .toThrow(TemplateSyntaxError("'if' expression error: missing closing bracket")) + try expect(parseExpression(components: [")", "lhs", "and", "rhs"])) + .toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket")) + try expect(parseExpression(components: ["lhs", "and", "rhs", ")"])) + .toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket")) + try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", "("])) + .toThrow(TemplateSyntaxError("'if' expression error: missing closing bracket")) + try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", ")"])) + .toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket")) + try expect(parseExpression(components: ["(", "lhs", "and", ")"])) + .toThrow(TemplateSyntaxError("'if' expression error: end")) + try expect(parseExpression(components: ["(", "and", "rhs", ")"])) + .toThrow(TemplateSyntaxError("'if' expression error: infix operator 'and' doesn't have a left hand side")) + } + } } } } diff --git a/docs/builtins.rst b/docs/builtins.rst index d4cc99a..0300783 100644 --- a/docs/builtins.rst +++ b/docs/builtins.rst @@ -149,6 +149,19 @@ Will be treated as: one or (two and three) +You can use parentheses to change operator precedence. For example: + +.. code-block:: html+django + + {% if (one or two) and three %} + +Will be treated as: + +.. code-block:: text + + (one or two) and three + + ``==`` operator """"""""""""""" From df2e1938915d21840f1b1dd5941abac0ef348dfa Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 22 Sep 2018 14:09:25 +0300 Subject: [PATCH 61/81] Allow conditions in variable node (#243) * use condition in variable node * added support for else expression * addressing code review comments --- CHANGELOG.md | 4 ++- Sources/Node.swift | 46 +++++++++++++++++++++++++++ Sources/Parser.swift | 3 +- Tests/StencilTests/VariableSpec.swift | 19 +++++++++++ 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86d4ec9..1e7a965 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,9 @@ ### New Features -_None_ +- Now you can conditionally render variables with `{{ variable if condition }}`, which is a shorthand for `{% if condition %}{{ variable }}{% endif %}`. You can also use `else` like `{{ variable1 if condition else variable2 }}`, which is a shorthand for `{% if condition %}{{ variable1 }}{% else %}{{ variable2 }}{% endif %}` + [Ilya Puchka](https://github.com/ilyapuchka) + [#243](https://github.com/stencilproject/Stencil/pull/243) ### Internal Changes diff --git a/Sources/Node.swift b/Sources/Node.swift index c4bb77a..dc7a72c 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -58,18 +58,64 @@ public protocol Resolvable { public class VariableNode : NodeType { public let variable: Resolvable public var token: Token? + let condition: Expression? + let elseExpression: Resolvable? + + class func parse(_ parser:TokenParser, token:Token) throws -> NodeType { + var components = token.components() + + func hasToken(_ token: String, at index: Int) -> Bool { + return components.count > (index + 1) && components[index] == token + } + + let condition: Expression? + let elseExpression: Resolvable? + + if hasToken("if", at: 1) { + let components = components.suffix(from: 2) + if let elseIndex = components.index(of: "else") { + condition = try parseExpression(components: Array(components.prefix(upTo: elseIndex)), tokenParser: parser, token: token) + let elseToken = components.suffix(from: elseIndex.advanced(by: 1)).joined(separator: " ") + elseExpression = try parser.compileResolvable(elseToken, containedIn: token) + } else { + condition = try parseExpression(components: Array(components), tokenParser: parser, token: token) + elseExpression = nil + } + } else { + condition = nil + elseExpression = nil + } + + let filter = try parser.compileResolvable(components[0], containedIn: token) + return VariableNode(variable: filter, token: token, condition: condition, elseExpression: elseExpression) + } public init(variable: Resolvable, token: Token? = nil) { self.variable = variable self.token = token + self.condition = nil + self.elseExpression = nil + } + + init(variable: Resolvable, token: Token? = nil, condition: Expression?, elseExpression: Resolvable?) { + self.variable = variable + self.token = token + self.condition = condition + self.elseExpression = elseExpression } public init(variable: String, token: Token? = nil) { self.variable = Variable(variable) self.token = token + self.condition = nil + self.elseExpression = nil } public func render(_ context: Context) throws -> String { + if let condition = self.condition, try condition.evaluate(context: context) == false { + return try elseExpression?.resolve(context).map(stringify) ?? "" + } + let result = try variable.resolve(context) return stringify(result) } diff --git a/Sources/Parser.swift b/Sources/Parser.swift index 5de93e8..1f5d50d 100644 --- a/Sources/Parser.swift +++ b/Sources/Parser.swift @@ -40,8 +40,7 @@ public class TokenParser { case .text(let text, _): nodes.append(TextNode(text: text)) case .variable: - let filter = try compileResolvable(token.contents, containedIn: token) - nodes.append(VariableNode(variable: filter, token: token)) + try nodes.append(VariableNode.parse(self, token: token)) case .block: if let parse_until = parse_until , parse_until(self, token) { prependToken(token) diff --git a/Tests/StencilTests/VariableSpec.swift b/Tests/StencilTests/VariableSpec.swift index 8303481..8d3c4ac 100644 --- a/Tests/StencilTests/VariableSpec.swift +++ b/Tests/StencilTests/VariableSpec.swift @@ -337,4 +337,23 @@ func testVariable() { } } + + describe("inline if expression") { + + $0.it("can conditionally render variable") { + let template: Template = "{{ variable if variable|uppercase == \"A\" }}" + try expect(template.render(Context(dictionary: ["variable": "a"]))) == "a" + try expect(template.render(Context(dictionary: ["variable": "b"]))) == "" + } + + $0.it("can render with else expression") { + let template: Template = "{{ variable if variable|uppercase == \"A\" else fallback|uppercase }}" + try expect(template.render(Context(dictionary: ["variable": "b", "fallback": "c"]))) == "C" + } + + $0.it("throws when used invalid condition") { + let template: Template = "{{ variable if variable \"A\" }}" + try expect(template.render(Context(dictionary: ["variable": "a"]))).toThrow() + } + } } From d238c25eef36a0a24bd02cd47f0171f26231491b Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 22 Sep 2018 16:41:45 +0300 Subject: [PATCH 62/81] Allow using collection accessors on strings (#245) * allow using collection accessors on strings * refactored resolving collection accessors * refactored to fileprivate function * Update Variable.swift * Update templates.rst --- CHANGELOG.md | 4 + Sources/Variable.swift | 34 +++++--- Tests/StencilTests/VariableSpec.swift | 114 ++++++++++++++++++-------- docs/templates.rst | 4 +- 4 files changed, 109 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e7a965..11c1762 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,10 @@ [Ilya Puchka](https://github.com/ilyapuchka) [#243](https://github.com/stencilproject/Stencil/pull/243) +- Now you can access string characters by index or get string length the same was as if it was an array, i.e. `{{ 'string'.first }}`, `{{ 'string'.last }}`, `{{ 'string'.1 }}`, `{{ 'string'.count }}`. + [Ilya Puchka](https://github.com/ilyapuchka) + [#245](https://github.com/stencilproject/Stencil/pull/245) + ### Internal Changes - Updated the codebase to use Swift 4 features. diff --git a/Sources/Variable.swift b/Sources/Variable.swift index d9620ed..eb30060 100644 --- a/Sources/Variable.swift +++ b/Sources/Variable.swift @@ -87,19 +87,9 @@ public struct Variable : Equatable, Resolvable { current = dictionary[bit] } } else if let array = current as? [Any] { - if let index = Int(bit) { - if index >= 0 && index < array.count { - current = array[index] - } else { - current = nil - } - } else if bit == "first" { - current = array.first - } else if bit == "last" { - current = array.last - } else if bit == "count" { - current = array.count - } + current = resolveCollection(array, bit: bit) + } else if let string = current as? String { + current = resolveCollection(string, bit: bit) } else if let object = current as? NSObject { // NSKeyValueCoding #if os(Linux) return nil @@ -128,6 +118,24 @@ public struct Variable : Equatable, Resolvable { } } +private func resolveCollection(_ collection: T, bit: String) -> Any? { + if let index = Int(bit) { + if index >= 0 && index < collection.count { + return collection[collection.index(collection.startIndex, offsetBy: index)] + } else { + return nil + } + } else if bit == "first" { + return collection.first + } else if bit == "last" { + return collection[collection.index(collection.endIndex, offsetBy: -1)] + } else if bit == "count" { + return collection.count + } else { + return nil + } +} + /// A structure used to represet range of two integer values expressed as `from...to`. /// Values should be numbers (they will be converted to integers). /// Rendering this variable produces array from range `from...to`. diff --git a/Tests/StencilTests/VariableSpec.swift b/Tests/StencilTests/VariableSpec.swift index 8d3c4ac..2d052bb 100644 --- a/Tests/StencilTests/VariableSpec.swift +++ b/Tests/StencilTests/VariableSpec.swift @@ -86,42 +86,98 @@ func testVariable() { try expect(result) == "Kyle" } - $0.it("can resolve an item from a dictionary") { - let variable = Variable("profiles.github") - let result = try variable.resolve(context) as? String - try expect(result) == "kylef" + $0.context("given string") { + $0.it("can resolve an item via it's index") { + let variable = Variable("name.0") + let result = try variable.resolve(context) as? Character + try expect(result) == "K" + + let variable1 = Variable("name.1") + let result1 = try variable1.resolve(context) as? Character + try expect(result1) == "y" + } + + $0.it("can resolve an item via unknown index") { + let variable = Variable("name.5") + let result = try variable.resolve(context) as? Character + try expect(result).to.beNil() + + let variable1 = Variable("name.-5") + let result1 = try variable1.resolve(context) as? Character + try expect(result1).to.beNil() + } + + $0.it("can resolve the first item") { + let variable = Variable("name.first") + let result = try variable.resolve(context) as? Character + try expect(result) == "K" + } + + $0.it("can resolve the last item") { + let variable = Variable("name.last") + let result = try variable.resolve(context) as? Character + try expect(result) == "e" + } + + $0.it("can get the characters count") { + let variable = Variable("name.count") + let result = try variable.resolve(context) as? Int + try expect(result) == 4 + } } - $0.it("can resolve an item from an array via it's index") { - let variable = Variable("contacts.0") - let result = try variable.resolve(context) as? String - try expect(result) == "Katie" + $0.context("given dictionary") { + $0.it("can resolve an item") { + let variable = Variable("profiles.github") + let result = try variable.resolve(context) as? String + try expect(result) == "kylef" + } + + $0.it("can get the count") { + let variable = Variable("profiles.count") + let result = try variable.resolve(context) as? Int + try expect(result) == 1 + } + } + + $0.context("given array") { + $0.it("can resolve an item via it's index") { + let variable = Variable("contacts.0") + let result = try variable.resolve(context) as? String + try expect(result) == "Katie" let variable1 = Variable("contacts.1") let result1 = try variable1.resolve(context) as? String try expect(result1) == "Carlton" - } + } - $0.it("can resolve an item from an array via unknown index") { - let variable = Variable("contacts.5") - let result = try variable.resolve(context) as? String - try expect(result).to.beNil() + $0.it("can resolve an item via unknown index") { + let variable = Variable("contacts.5") + let result = try variable.resolve(context) as? String + try expect(result).to.beNil() - let variable1 = Variable("contacts.-5") - let result1 = try variable1.resolve(context) as? String - try expect(result1).to.beNil() - } + let variable1 = Variable("contacts.-5") + let result1 = try variable1.resolve(context) as? String + try expect(result1).to.beNil() + } - $0.it("can resolve the first item from an array") { - let variable = Variable("contacts.first") - let result = try variable.resolve(context) as? String - try expect(result) == "Katie" - } + $0.it("can resolve the first item") { + let variable = Variable("contacts.first") + let result = try variable.resolve(context) as? String + try expect(result) == "Katie" + } - $0.it("can resolve the last item from an array") { - let variable = Variable("contacts.last") - let result = try variable.resolve(context) as? String - try expect(result) == "Carlton" + $0.it("can resolve the last item") { + let variable = Variable("contacts.last") + let result = try variable.resolve(context) as? String + try expect(result) == "Carlton" + } + + $0.it("can get the count") { + let variable = Variable("contacts.count") + let result = try variable.resolve(context) as? Int + try expect(result) == 2 + } } $0.it("can resolve a property with reflection") { @@ -130,12 +186,6 @@ func testVariable() { try expect(result) == "Kyle" } - $0.it("can get the count of a dictionary") { - let variable = Variable("profiles.count") - let result = try variable.resolve(context) as? Int - try expect(result) == 1 - } - #if os(OSX) $0.it("can resolve a value via KVO") { let variable = Variable("object.title") diff --git a/docs/templates.rst b/docs/templates.rst index 147be45..7094ae6 100644 --- a/docs/templates.rst +++ b/docs/templates.rst @@ -20,9 +20,9 @@ following lookup: - Context lookup - Dictionary lookup -- Array lookup (first, last, count, index) +- Array and string lookup (first, last, count, by index) - Key value coding lookup -- Type introspection +- Type introspection (via ``Mirror``) For example, if `people` was an array: From f7bda226e81de06b330ec5c1e341601769d42d3f Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sun, 23 Sep 2018 03:46:27 +0300 Subject: [PATCH 63/81] Update to Spectre 0.9.0 (#247) * update to Spectre 0.9.0 * fix variable spec tests * fix flatMap warning * updated CHANGELOG --- .gitignore | 1 - CHANGELOG.md | 4 +- Package.resolved | 25 + Package.swift | 2 +- Sources/IfTag.swift | 2 +- Tests/LinuxMain.swift | 7 +- Tests/StencilTests/ContextSpec.swift | 116 ++-- Tests/StencilTests/EnvironmentSpec.swift | 570 ++++++++++---------- Tests/StencilTests/ExpressionSpec.swift | 634 +++++++++++----------- Tests/StencilTests/FilterSpec.swift | 626 +++++++++++----------- Tests/StencilTests/FilterTagSpec.swift | 60 ++- Tests/StencilTests/ForNodeSpec.swift | 476 ++++++++-------- Tests/StencilTests/IfNodeSpec.swift | 518 +++++++++--------- Tests/StencilTests/IncludeSpec.swift | 100 ++-- Tests/StencilTests/InheritenceSpec.swift | 36 +- Tests/StencilTests/LexerSpec.swift | 128 ++--- Tests/StencilTests/LoaderSpec.swift | 80 +-- Tests/StencilTests/NodeSpec.swift | 81 +-- Tests/StencilTests/NowNodeSpec.swift | 65 +-- Tests/StencilTests/ParserSpec.swift | 98 ++-- Tests/StencilTests/StencilSpec.swift | 80 +-- Tests/StencilTests/TemplateSpec.swift | 24 +- Tests/StencilTests/TokenSpec.swift | 48 +- Tests/StencilTests/VariableSpec.swift | 655 ++++++++++++----------- Tests/StencilTests/XCTest.swift | 30 -- Tests/StencilTests/XCTestManifests.swift | 134 +++++ 26 files changed, 2386 insertions(+), 2214 deletions(-) create mode 100644 Package.resolved delete mode 100644 Tests/StencilTests/XCTest.swift create mode 100644 Tests/StencilTests/XCTestManifests.swift diff --git a/.gitignore b/.gitignore index 2dcf7f5..088c653 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .conche/ .build/ Packages/ -Package.resolved Package.pins *.xcodeproj diff --git a/CHANGELOG.md b/CHANGELOG.md index 11c1762..02fb49b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,6 @@ - Now you can conditionally render variables with `{{ variable if condition }}`, which is a shorthand for `{% if condition %}{{ variable }}{% endif %}`. You can also use `else` like `{{ variable1 if condition else variable2 }}`, which is a shorthand for `{% if condition %}{{ variable1 }}{% else %}{{ variable2 }}{% endif %}` [Ilya Puchka](https://github.com/ilyapuchka) [#243](https://github.com/stencilproject/Stencil/pull/243) - - Now you can access string characters by index or get string length the same was as if it was an array, i.e. `{{ 'string'.first }}`, `{{ 'string'.last }}`, `{{ 'string'.1 }}`, `{{ 'string'.count }}`. [Ilya Puchka](https://github.com/ilyapuchka) [#245](https://github.com/stencilproject/Stencil/pull/245) @@ -35,6 +34,9 @@ - Updated the codebase to use Swift 4 features. [David Jennes](https://github.com/djbe) [#239](https://github.com/stencilproject/Stencil/pull/239) +- Update to Spectre 0.9.0. + [Ilya Puchka](https://github.com/ilyapuchka) + [#247](https://github.com/stencilproject/Stencil/pull/247) ## 0.12.1 diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..ff6830b --- /dev/null +++ b/Package.resolved @@ -0,0 +1,25 @@ +{ + "object": { + "pins": [ + { + "package": "PathKit", + "repositoryURL": "https://github.com/kylef/PathKit.git", + "state": { + "branch": null, + "revision": "e2f5be30e4c8f531c9c1e8765aa7b71c0a45d7a0", + "version": "0.9.2" + } + }, + { + "package": "Spectre", + "repositoryURL": "https://github.com/kylef/Spectre.git", + "state": { + "branch": null, + "revision": "f14ff47f45642aa5703900980b014c2e9394b6e5", + "version": "0.9.0" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift index 01cf4c6..a0b49da 100644 --- a/Package.swift +++ b/Package.swift @@ -8,7 +8,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/kylef/PathKit.git", from: "0.9.0"), - .package(url: "https://github.com/kylef/Spectre.git", from: "0.8.0"), + .package(url: "https://github.com/kylef/Spectre.git", from: "0.9.0"), ], targets: [ .target(name: "Stencil", dependencies: [ diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index 997f4fb..7376054 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -118,7 +118,7 @@ final class IfExpressionParser { private init(components: ArraySlice, tokenParser: TokenParser, token: Token) throws { var parsedComponents = Set() var bracketsBalance = 0 - self.tokens = try zip(components.indices, components).flatMap { (index, component) in + self.tokens = try zip(components.indices, components).compactMap { (index, component) in guard !parsedComponents.contains(index) else { return nil } if component == "(" { diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 294b797..170a024 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,3 +1,8 @@ +import XCTest + import StencilTests -stencilTests() +var tests = [XCTestCaseEntry]() +tests += StencilTests.__allTests() + +XCTMain(tests) diff --git a/Tests/StencilTests/ContextSpec.swift b/Tests/StencilTests/ContextSpec.swift index d33e1d8..7e20cbd 100644 --- a/Tests/StencilTests/ContextSpec.swift +++ b/Tests/StencilTests/ContextSpec.swift @@ -1,80 +1,84 @@ +import XCTest import Spectre @testable import Stencil -func testContext() { - describe("Context") { - var context: Context! +class ContextTests: XCTestCase { + + func testContext() { + describe("Context") { + var context: Context! - $0.before { - context = Context(dictionary: ["name": "Kyle"]) - } + $0.before { + context = Context(dictionary: ["name": "Kyle"]) + } - $0.it("allows you to get a value via subscripting") { - try expect(context["name"] as? String) == "Kyle" - } - - $0.it("allows you to set a value via subscripting") { - context["name"] = "Katie" - - try expect(context["name"] as? String) == "Katie" - } - - $0.it("allows you to remove a value via subscripting") { - context["name"] = nil - - try expect(context["name"]).to.beNil() - } - - $0.it("allows you to retrieve a value from a parent") { - try context.push { + $0.it("allows you to get a value via subscripting") { try expect(context["name"] as? String) == "Kyle" } - } - $0.it("allows you to override a parent's value") { - try context.push { + $0.it("allows you to set a value via subscripting") { context["name"] = "Katie" + try expect(context["name"] as? String) == "Katie" } - } - $0.it("allows you to pop to restore previous state") { - context.push { - context["name"] = "Katie" - } - - try expect(context["name"] as? String) == "Kyle" - } - - $0.it("allows you to remove a parent's value in a level") { - try context.push { + $0.it("allows you to remove a value via subscripting") { context["name"] = nil + try expect(context["name"]).to.beNil() } - try expect(context["name"] as? String) == "Kyle" - } - - $0.it("allows you to push a dictionary and run a closure then restoring previous state") { - var didRun = false - - try context.push(dictionary: ["name": "Katie"]) { - didRun = true - try expect(context["name"] as? String) == "Katie" + $0.it("allows you to retrieve a value from a parent") { + try context.push { + try expect(context["name"] as? String) == "Kyle" + } } - try expect(didRun).to.beTrue() - try expect(context["name"] as? String) == "Kyle" - } + $0.it("allows you to override a parent's value") { + try context.push { + context["name"] = "Katie" + try expect(context["name"] as? String) == "Katie" + } + } - $0.it("allows you to flatten the context contents") { - try context.push(dictionary: ["test": "abc"]) { - let flattened = context.flatten() + $0.it("allows you to pop to restore previous state") { + context.push { + context["name"] = "Katie" + } - try expect(flattened.count) == 2 - try expect(flattened["name"] as? String) == "Kyle" - try expect(flattened["test"] as? String) == "abc" + try expect(context["name"] as? String) == "Kyle" + } + + $0.it("allows you to remove a parent's value in a level") { + try context.push { + context["name"] = nil + try expect(context["name"]).to.beNil() + } + + try expect(context["name"] as? String) == "Kyle" + } + + $0.it("allows you to push a dictionary and run a closure then restoring previous state") { + var didRun = false + + try context.push(dictionary: ["name": "Katie"]) { + didRun = true + try expect(context["name"] as? String) == "Katie" + } + + try expect(didRun).to.beTrue() + try expect(context["name"] as? String) == "Kyle" + } + + $0.it("allows you to flatten the context contents") { + try context.push(dictionary: ["test": "abc"]) { + let flattened = context.flatten() + + try expect(flattened.count) == 2 + try expect(flattened["name"] as? String) == "Kyle" + try expect(flattened["test"] as? String) == "abc" + } } } } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 04ce6c1..f2a7d06 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -1,333 +1,335 @@ +import XCTest import Spectre import PathKit @testable import Stencil - -func testEnvironment() { - describe("Environment") { - var environment: Environment! - var template: Template! - - $0.before { - environment = Environment(loader: ExampleLoader()) - template = nil - } - - $0.it("can load a template from a name") { - let template = try environment.loadTemplate(name: "example.html") - try expect(template.name) == "example.html" - } - - $0.it("can load a template from a names") { - let template = try environment.loadTemplate(names: ["first.html", "example.html"]) - try expect(template.name) == "example.html" - } - - $0.it("can render a template from a string") { - let result = try environment.renderTemplate(string: "Hello World") - try expect(result) == "Hello World" - } - - $0.it("can render a template from a file") { - let result = try environment.renderTemplate(name: "example.html") - try expect(result) == "Hello World!" - } - - $0.it("allows you to provide a custom template class") { - let environment = Environment(loader: ExampleLoader(), templateClass: CustomTemplate.self) - let result = try environment.renderTemplate(string: "Hello World") - - try expect(result) == "here" - } - - func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { - guard let range = template.templateString.range(of: token) else { - fatalError("Can't find '\(token)' in '\(template)'") - } - let lexer = Lexer(templateString: template.templateString) - let location = lexer.rangeLocation(range) - let sourceMap = SourceMap(filename: template.name, location: location) - let token = Token.block(value: token, at: sourceMap) - return TemplateSyntaxError(reason: description, token: token, stackTrace: []) - } - - func expectError(reason: String, token: String, - file: String = #file, line: Int = #line, function: String = #function) throws { - let expectedError = expectedSyntaxError(token: token, template: template, description: reason) - - let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"]), - file: file, line: line, function: function).toThrow() as TemplateSyntaxError - let reporter = SimpleErrorReporter() - try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError) - } - - $0.context("given syntax error") { - - $0.it("reports syntax error on invalid for tag syntax") { - template = "Hello {% for name in %}{{ name }}, {% endfor %}!" - try expectError(reason: "'for' statements should use the syntax: `for in [where ]`.", token: "for name in") - } - - $0.it("reports syntax error on missing endfor") { - template = "{% for name in names %}{{ name }}" - try expectError(reason: "`endfor` was not found.", token: "for name in names") - } - - $0.it("reports syntax error on unknown tag") { - template = "{% for name in names %}{{ name }}{% end %}" - try expectError(reason: "Unknown template tag 'end'", token: "end") - } - - } - - $0.context("given unknown filter") { - - $0.it("reports syntax error in for tag") { - template = "{% for name in names|unknown %}{{ name }}{% endfor %}" - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "names|unknown") - } - - $0.it("reports syntax error in for-where tag") { - template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}" - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") - } - - $0.it("reports syntax error in if tag") { - template = "{% if name|unknown %}{{ name }}{% endif %}" - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") - } - - $0.it("reports syntax error in elif tag") { - template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}" - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") - } - - $0.it("reports syntax error in ifnot tag") { - template = "{% ifnot name|unknown %}{{ name }}{% endif %}" - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") - } - - $0.it("reports syntax error in filter tag") { - template = "{% filter unknown %}Text{% endfilter %}" - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "filter unknown") - } - - $0.it("reports syntax error in variable tag") { - template = "{{ name|unknown }}" - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") - } - - } - - $0.context("given rendering error") { - - $0.it("reports rendering error in variable filter") { - let filterExtension = Extension() - filterExtension.registerFilter("throw") { (value: Any?) in - throw TemplateSyntaxError("filter error") - } - environment.extensions += [filterExtension] - - template = Template(templateString: "{{ name|throw }}", environment: environment) - try expectError(reason: "filter error", token: "name|throw") - } - - $0.it("reports rendering error in filter tag") { - let filterExtension = Extension() - filterExtension.registerFilter("throw") { (value: Any?) in - throw TemplateSyntaxError("filter error") - } - environment.extensions += [filterExtension] - - template = Template(templateString: "{% filter throw %}Test{% endfilter %}", environment: environment) - try expectError(reason: "filter error", token: "filter throw") - } - - $0.it("reports rendering error in simple tag") { - let tagExtension = Extension() - tagExtension.registerSimpleTag("simpletag") { context in - throw TemplateSyntaxError("simpletag error") - } - environment.extensions += [tagExtension] - - template = Template(templateString: "{% simpletag %}", environment: environment) - try expectError(reason: "simpletag error", token: "simpletag") - } - - $0.it("reporsts passing argument to simple filter") { - template = "{{ name|uppercase:5 }}" - try expectError(reason: "cannot invoke filter with an argument", token: "name|uppercase:5") - } - - $0.it("reports rendering error in custom tag") { - let tagExtension = Extension() - tagExtension.registerTag("customtag") { parser, token in - return ErrorNode(token: token) - } - environment.extensions += [tagExtension] - - template = Template(templateString: "{% customtag %}", environment: environment) - try expectError(reason: "Custom Error", token: "customtag") - } - - $0.it("reports rendering error in for body") { - let tagExtension = Extension() - tagExtension.registerTag("customtag") { parser, token in - return ErrorNode(token: token) - } - environment.extensions += [tagExtension] - - template = Template(templateString: "{% for name in names %}{% customtag %}{% endfor %}", environment: environment) - try expectError(reason: "Custom Error", token: "customtag") - } - - $0.it("reports rendering error in block") { - let tagExtension = Extension() - tagExtension.registerTag("customtag") { parser, token in - return ErrorNode(token: token) - } - environment.extensions += [tagExtension] - - template = Template(templateString: "{% block some %}{% customtag %}{% endblock %}", environment: environment) - try expectError(reason: "Custom Error", token: "customtag") - } - } - - $0.context("given included template") { - let path = Path(#file) + ".." + "fixtures" - let loader = FileSystemLoader(paths: [path]) - var environment = Environment(loader: loader) +class EnvironmentTests: XCTestCase { + func testEnvironment() { + describe("Environment") { + var environment: Environment! var template: Template! - var includedTemplate: Template! $0.before { - environment = Environment(loader: loader) + environment = Environment(loader: ExampleLoader()) template = nil - includedTemplate = nil } - func expectError(reason: String, token: String, includedToken: String, - file: String = #file, line: Int = #line, function: String = #function) throws { - var expectedError = expectedSyntaxError(token: token, template: template, description: reason) - expectedError.stackTrace = [expectedSyntaxError(token: includedToken, template: includedTemplate, description: reason).token!] + $0.it("can load a template from a name") { + let template = try environment.loadTemplate(name: "example.html") + try expect(template.name) == "example.html" + } - let error = try expect(environment.render(template: template, context: ["target": "World"]), + $0.it("can load a template from a names") { + let template = try environment.loadTemplate(names: ["first.html", "example.html"]) + try expect(template.name) == "example.html" + } + + $0.it("can render a template from a string") { + let result = try environment.renderTemplate(string: "Hello World") + try expect(result) == "Hello World" + } + + $0.it("can render a template from a file") { + let result = try environment.renderTemplate(name: "example.html") + try expect(result) == "Hello World!" + } + + $0.it("allows you to provide a custom template class") { + let environment = Environment(loader: ExampleLoader(), templateClass: CustomTemplate.self) + let result = try environment.renderTemplate(string: "Hello World") + + try expect(result) == "here" + } + + func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { + guard let range = template.templateString.range(of: token) else { + fatalError("Can't find '\(token)' in '\(template)'") + } + let lexer = Lexer(templateString: template.templateString) + let location = lexer.rangeLocation(range) + let sourceMap = SourceMap(filename: template.name, location: location) + let token = Token.block(value: token, at: sourceMap) + return TemplateSyntaxError(reason: description, token: token, stackTrace: []) + } + + func expectError(reason: String, token: String, + file: String = #file, line: Int = #line, function: String = #function) throws { + let expectedError = expectedSyntaxError(token: token, template: template, description: reason) + + let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"]), file: file, line: line, function: function).toThrow() as TemplateSyntaxError let reporter = SimpleErrorReporter() try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError) } - $0.it("reports syntax error in included template") { - template = Template(templateString: """ + $0.context("given syntax error") { + + $0.it("reports syntax error on invalid for tag syntax") { + template = "Hello {% for name in %}{{ name }}, {% endfor %}!" + try expectError(reason: "'for' statements should use the syntax: `for in [where ]`.", token: "for name in") + } + + $0.it("reports syntax error on missing endfor") { + template = "{% for name in names %}{{ name }}" + try expectError(reason: "`endfor` was not found.", token: "for name in names") + } + + $0.it("reports syntax error on unknown tag") { + template = "{% for name in names %}{{ name }}{% end %}" + try expectError(reason: "Unknown template tag 'end'", token: "end") + } + + } + + $0.context("given unknown filter") { + + $0.it("reports syntax error in for tag") { + template = "{% for name in names|unknown %}{{ name }}{% endfor %}" + try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "names|unknown") + } + + $0.it("reports syntax error in for-where tag") { + template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}" + try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") + } + + $0.it("reports syntax error in if tag") { + template = "{% if name|unknown %}{{ name }}{% endif %}" + try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") + } + + $0.it("reports syntax error in elif tag") { + template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}" + try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") + } + + $0.it("reports syntax error in ifnot tag") { + template = "{% ifnot name|unknown %}{{ name }}{% endif %}" + try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") + } + + $0.it("reports syntax error in filter tag") { + template = "{% filter unknown %}Text{% endfilter %}" + try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "filter unknown") + } + + $0.it("reports syntax error in variable tag") { + template = "{{ name|unknown }}" + try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown") + } + + } + + $0.context("given rendering error") { + + $0.it("reports rendering error in variable filter") { + let filterExtension = Extension() + filterExtension.registerFilter("throw") { (value: Any?) in + throw TemplateSyntaxError("filter error") + } + environment.extensions += [filterExtension] + + template = Template(templateString: "{{ name|throw }}", environment: environment) + try expectError(reason: "filter error", token: "name|throw") + } + + $0.it("reports rendering error in filter tag") { + let filterExtension = Extension() + filterExtension.registerFilter("throw") { (value: Any?) in + throw TemplateSyntaxError("filter error") + } + environment.extensions += [filterExtension] + + template = Template(templateString: "{% filter throw %}Test{% endfilter %}", environment: environment) + try expectError(reason: "filter error", token: "filter throw") + } + + $0.it("reports rendering error in simple tag") { + let tagExtension = Extension() + tagExtension.registerSimpleTag("simpletag") { context in + throw TemplateSyntaxError("simpletag error") + } + environment.extensions += [tagExtension] + + template = Template(templateString: "{% simpletag %}", environment: environment) + try expectError(reason: "simpletag error", token: "simpletag") + } + + $0.it("reporsts passing argument to simple filter") { + template = "{{ name|uppercase:5 }}" + try expectError(reason: "cannot invoke filter with an argument", token: "name|uppercase:5") + } + + $0.it("reports rendering error in custom tag") { + let tagExtension = Extension() + tagExtension.registerTag("customtag") { parser, token in + return ErrorNode(token: token) + } + environment.extensions += [tagExtension] + + template = Template(templateString: "{% customtag %}", environment: environment) + try expectError(reason: "Custom Error", token: "customtag") + } + + $0.it("reports rendering error in for body") { + let tagExtension = Extension() + tagExtension.registerTag("customtag") { parser, token in + return ErrorNode(token: token) + } + environment.extensions += [tagExtension] + + template = Template(templateString: "{% for name in names %}{% customtag %}{% endfor %}", environment: environment) + try expectError(reason: "Custom Error", token: "customtag") + } + + $0.it("reports rendering error in block") { + let tagExtension = Extension() + tagExtension.registerTag("customtag") { parser, token in + return ErrorNode(token: token) + } + environment.extensions += [tagExtension] + + template = Template(templateString: "{% block some %}{% customtag %}{% endblock %}", environment: environment) + try expectError(reason: "Custom Error", token: "customtag") + } + } + + $0.context("given included template") { + let path = Path(#file) + ".." + "fixtures" + let loader = FileSystemLoader(paths: [path]) + var environment = Environment(loader: loader) + var template: Template! + var includedTemplate: Template! + + $0.before { + environment = Environment(loader: loader) + template = nil + includedTemplate = nil + } + + func expectError(reason: String, token: String, includedToken: String, + file: String = #file, line: Int = #line, function: String = #function) throws { + var expectedError = expectedSyntaxError(token: token, template: template, description: reason) + expectedError.stackTrace = [expectedSyntaxError(token: includedToken, template: includedTemplate, description: reason).token!] + + let error = try expect(environment.render(template: template, context: ["target": "World"]), + file: file, line: line, function: function).toThrow() as TemplateSyntaxError + let reporter = SimpleErrorReporter() + try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError) + } + + $0.it("reports syntax error in included template") { + template = Template(templateString: """ {% include "invalid-include.html" %} """, environment: environment) - includedTemplate = try environment.loadTemplate(name: "invalid-include.html") + includedTemplate = try environment.loadTemplate(name: "invalid-include.html") - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", - token: """ + try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + token: """ include "invalid-include.html" """, - includedToken: "target|unknown") - } + includedToken: "target|unknown") + } - $0.it("reports runtime error in included template") { - let filterExtension = Extension() - filterExtension.registerFilter("unknown", filter: { (_: Any?) in - throw TemplateSyntaxError("filter error") - }) - environment.extensions += [filterExtension] + $0.it("reports runtime error in included template") { + let filterExtension = Extension() + filterExtension.registerFilter("unknown", filter: { (_: Any?) in + throw TemplateSyntaxError("filter error") + }) + environment.extensions += [filterExtension] - template = Template(templateString: """ + template = Template(templateString: """ {% include "invalid-include.html" %} """, environment: environment) - includedTemplate = try environment.loadTemplate(name: "invalid-include.html") + includedTemplate = try environment.loadTemplate(name: "invalid-include.html") - try expectError(reason: "filter error", - token: "include \"invalid-include.html\"", - includedToken: "target|unknown") - } - - } - - $0.context("given base and child templates") { - let path = Path(#file) + ".." + "fixtures" - let loader = FileSystemLoader(paths: [path]) - var environment: Environment! - var childTemplate: Template! - var baseTemplate: Template! - - $0.before { - environment = Environment(loader: loader) - childTemplate = nil - baseTemplate = nil - } - - func expectError(reason: String, childToken: String, baseToken: String?, - file: String = #file, line: Int = #line, function: String = #function) throws { - var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason) - if let baseToken = baseToken { - expectedError.stackTrace = [expectedSyntaxError(token: baseToken, template: baseTemplate, description: reason).token!] + try expectError(reason: "filter error", + token: "include \"invalid-include.html\"", + includedToken: "target|unknown") } - let error = try expect(environment.render(template: childTemplate, context: ["target": "World"]), - file: file, line: line, function: function).toThrow() as TemplateSyntaxError - let reporter = SimpleErrorReporter() - try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError) + } - $0.it("reports syntax error in base template") { - childTemplate = try environment.loadTemplate(name: "invalid-child-super.html") - baseTemplate = try environment.loadTemplate(name: "invalid-base.html") + $0.context("given base and child templates") { + let path = Path(#file) + ".." + "fixtures" + let loader = FileSystemLoader(paths: [path]) + var environment: Environment! + var childTemplate: Template! + var baseTemplate: Template! - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", - childToken: "extends \"invalid-base.html\"", - baseToken: "target|unknown") - } + $0.before { + environment = Environment(loader: loader) + childTemplate = nil + baseTemplate = nil + } - $0.it("reports runtime error in base template") { - let filterExtension = Extension() - filterExtension.registerFilter("unknown", filter: { (_: Any?) in - throw TemplateSyntaxError("filter error") - }) - environment.extensions += [filterExtension] + func expectError(reason: String, childToken: String, baseToken: String?, + file: String = #file, line: Int = #line, function: String = #function) throws { + var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason) + if let baseToken = baseToken { + expectedError.stackTrace = [expectedSyntaxError(token: baseToken, template: baseTemplate, description: reason).token!] + } + let error = try expect(environment.render(template: childTemplate, context: ["target": "World"]), + file: file, line: line, function: function).toThrow() as TemplateSyntaxError + let reporter = SimpleErrorReporter() + try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError) + } - childTemplate = try environment.loadTemplate(name: "invalid-child-super.html") - baseTemplate = try environment.loadTemplate(name: "invalid-base.html") + $0.it("reports syntax error in base template") { + childTemplate = try environment.loadTemplate(name: "invalid-child-super.html") + baseTemplate = try environment.loadTemplate(name: "invalid-base.html") - try expectError(reason: "filter error", - childToken: "block.super", - baseToken: "target|unknown") - } + try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + childToken: "extends \"invalid-base.html\"", + baseToken: "target|unknown") + } - $0.it("reports syntax error in child template") { - childTemplate = Template(templateString: """ + $0.it("reports runtime error in base template") { + let filterExtension = Extension() + filterExtension.registerFilter("unknown", filter: { (_: Any?) in + throw TemplateSyntaxError("filter error") + }) + environment.extensions += [filterExtension] + + childTemplate = try environment.loadTemplate(name: "invalid-child-super.html") + baseTemplate = try environment.loadTemplate(name: "invalid-base.html") + + try expectError(reason: "filter error", + childToken: "block.super", + baseToken: "target|unknown") + } + + $0.it("reports syntax error in child template") { + childTemplate = Template(templateString: """ {% extends "base.html" %} {% block body %}Child {{ target|unknown }}{% endblock %} """, environment: environment, name: nil) - try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", - childToken: "target|unknown", - baseToken: nil) - } + try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", + childToken: "target|unknown", + baseToken: nil) + } - $0.it("reports runtime error in child template") { - let filterExtension = Extension() - filterExtension.registerFilter("unknown", filter: { (_: Any?) in - throw TemplateSyntaxError("filter error") - }) - environment.extensions += [filterExtension] + $0.it("reports runtime error in child template") { + let filterExtension = Extension() + filterExtension.registerFilter("unknown", filter: { (_: Any?) in + throw TemplateSyntaxError("filter error") + }) + environment.extensions += [filterExtension] - childTemplate = Template(templateString: """ + childTemplate = Template(templateString: """ {% extends "base.html" %} {% block body %}Child {{ target|unknown }}{% endblock %} """, environment: environment, name: nil) - try expectError(reason: "filter error", - childToken: "target|unknown", - baseToken: nil) + try expectError(reason: "filter error", + childToken: "target|unknown", + baseToken: nil) + } + } - + } - } } diff --git a/Tests/StencilTests/ExpressionSpec.swift b/Tests/StencilTests/ExpressionSpec.swift index e04408b..0109665 100644 --- a/Tests/StencilTests/ExpressionSpec.swift +++ b/Tests/StencilTests/ExpressionSpec.swift @@ -1,342 +1,344 @@ +import XCTest import Spectre @testable import Stencil +class ExpressionsTests: XCTestCase { + func testExpressions() { + describe("Expression") { + let parser = TokenParser(tokens: [], environment: Environment()) -func testExpressions() { - describe("Expression") { - let parser = TokenParser(tokens: [], environment: Environment()) - - func parseExpression(components: [String]) throws -> Expression { - let parser = try IfExpressionParser.parser(components: components, tokenParser: parser, token: .text(value: "", at: .unknown)) - return try parser.parse() - } - - $0.describe("VariableExpression") { - let expression = VariableExpression(variable: Variable("value")) - - $0.it("evaluates to true when value is not nil") { - let context = Context(dictionary: ["value": "known"]) - try expect(try expression.evaluate(context: context)).to.beTrue() + func parseExpression(components: [String]) throws -> Expression { + let parser = try IfExpressionParser.parser(components: components, tokenParser: parser, token: .text(value: "", at: .unknown)) + return try parser.parse() } - $0.it("evaluates to false when value is unset") { - let context = Context() - try expect(try expression.evaluate(context: context)).to.beFalse() - } + $0.describe("VariableExpression") { + let expression = VariableExpression(variable: Variable("value")) - $0.it("evaluates to true when array variable is not empty") { - let items: [[String: Any]] = [["key":"key1","value":42],["key":"key2","value":1337]] - let context = Context(dictionary: ["value": [items]]) - try expect(try expression.evaluate(context: context)).to.beTrue() - } - - $0.it("evaluates to false when array value is empty") { - let emptyItems = [[String: Any]]() - let context = Context(dictionary: ["value": emptyItems]) - try expect(try expression.evaluate(context: context)).to.beFalse() - } - - $0.it("evaluates to false when dictionary value is empty") { - let emptyItems = [String:Any]() - let context = Context(dictionary: ["value": emptyItems]) - try expect(try expression.evaluate(context: context)).to.beFalse() - } - - $0.it("evaluates to false when Array value is empty") { - let context = Context(dictionary: ["value": ([] as [Any])]) - try expect(try expression.evaluate(context: context)).to.beFalse() - } - - $0.it("evaluates to true when integer value is above 0") { - let context = Context(dictionary: ["value": 1]) - try expect(try expression.evaluate(context: context)).to.beTrue() - } - - $0.it("evaluates to true with string") { - let context = Context(dictionary: ["value": "test"]) - try expect(try expression.evaluate(context: context)).to.beTrue() - } - - $0.it("evaluates to false when empty string") { - let context = Context(dictionary: ["value": ""]) - try expect(try expression.evaluate(context: context)).to.beFalse() - } - - $0.it("evaluates to false when integer value is below 0 or below") { - let context = Context(dictionary: ["value": 0]) - try expect(try expression.evaluate(context: context)).to.beFalse() - - let negativeContext = Context(dictionary: ["value": 0]) - try expect(try expression.evaluate(context: negativeContext)).to.beFalse() - } - - $0.it("evaluates to true when float value is above 0") { - let context = Context(dictionary: ["value": Float(0.5)]) - try expect(try expression.evaluate(context: context)).to.beTrue() - } - - $0.it("evaluates to false when float is 0 or below") { - let context = Context(dictionary: ["value": Float(0)]) - try expect(try expression.evaluate(context: context)).to.beFalse() - } - - $0.it("evaluates to true when double value is above 0") { - let context = Context(dictionary: ["value": Double(0.5)]) - try expect(try expression.evaluate(context: context)).to.beTrue() - } - - $0.it("evaluates to false when double is 0 or below") { - let context = Context(dictionary: ["value": Double(0)]) - try expect(try expression.evaluate(context: context)).to.beFalse() - } - - $0.it("evaluates to false when uint is 0") { - let context = Context(dictionary: ["value": UInt(0)]) - try expect(try expression.evaluate(context: context)).to.beFalse() - } - } - - $0.describe("NotExpression") { - $0.it("returns truthy for positive expressions") { - let expression = NotExpression(expression: StaticExpression(value: true)) - try expect(expression.evaluate(context: Context())).to.beFalse() - } - - $0.it("returns falsy for negative expressions") { - let expression = NotExpression(expression: StaticExpression(value: false)) - try expect(expression.evaluate(context: Context())).to.beTrue() - } - } - - $0.describe("expression parsing") { - $0.it("can parse a variable expression") { - let expression = try parseExpression(components: ["value"]) - try expect(expression.evaluate(context: Context())).to.beFalse() - try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue() - } - - $0.it("can parse a not expression") { - let expression = try parseExpression(components: ["not", "value"]) - try expect(expression.evaluate(context: Context())).to.beTrue() - try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse() - } - - $0.describe("and expression") { - let expression = try! parseExpression(components: ["lhs", "and", "rhs"]) - - $0.it("evaluates to false with lhs false") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse() + $0.it("evaluates to true when value is not nil") { + let context = Context(dictionary: ["value": "known"]) + try expect(try expression.evaluate(context: context)).to.beTrue() } - $0.it("evaluates to false with rhs false") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse() + $0.it("evaluates to false when value is unset") { + let context = Context() + try expect(try expression.evaluate(context: context)).to.beFalse() } - $0.it("evaluates to false with lhs and rhs false") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse() + $0.it("evaluates to true when array variable is not empty") { + let items: [[String: Any]] = [["key":"key1","value":42],["key":"key2","value":1337]] + let context = Context(dictionary: ["value": [items]]) + try expect(try expression.evaluate(context: context)).to.beTrue() } - $0.it("evaluates to true with lhs and rhs true") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() + $0.it("evaluates to false when array value is empty") { + let emptyItems = [[String: Any]]() + let context = Context(dictionary: ["value": emptyItems]) + try expect(try expression.evaluate(context: context)).to.beFalse() + } + + $0.it("evaluates to false when dictionary value is empty") { + let emptyItems = [String:Any]() + let context = Context(dictionary: ["value": emptyItems]) + try expect(try expression.evaluate(context: context)).to.beFalse() + } + + $0.it("evaluates to false when Array value is empty") { + let context = Context(dictionary: ["value": ([] as [Any])]) + try expect(try expression.evaluate(context: context)).to.beFalse() + } + + $0.it("evaluates to true when integer value is above 0") { + let context = Context(dictionary: ["value": 1]) + try expect(try expression.evaluate(context: context)).to.beTrue() + } + + $0.it("evaluates to true with string") { + let context = Context(dictionary: ["value": "test"]) + try expect(try expression.evaluate(context: context)).to.beTrue() + } + + $0.it("evaluates to false when empty string") { + let context = Context(dictionary: ["value": ""]) + try expect(try expression.evaluate(context: context)).to.beFalse() + } + + $0.it("evaluates to false when integer value is below 0 or below") { + let context = Context(dictionary: ["value": 0]) + try expect(try expression.evaluate(context: context)).to.beFalse() + + let negativeContext = Context(dictionary: ["value": 0]) + try expect(try expression.evaluate(context: negativeContext)).to.beFalse() + } + + $0.it("evaluates to true when float value is above 0") { + let context = Context(dictionary: ["value": Float(0.5)]) + try expect(try expression.evaluate(context: context)).to.beTrue() + } + + $0.it("evaluates to false when float is 0 or below") { + let context = Context(dictionary: ["value": Float(0)]) + try expect(try expression.evaluate(context: context)).to.beFalse() + } + + $0.it("evaluates to true when double value is above 0") { + let context = Context(dictionary: ["value": Double(0.5)]) + try expect(try expression.evaluate(context: context)).to.beTrue() + } + + $0.it("evaluates to false when double is 0 or below") { + let context = Context(dictionary: ["value": Double(0)]) + try expect(try expression.evaluate(context: context)).to.beFalse() + } + + $0.it("evaluates to false when uint is 0") { + let context = Context(dictionary: ["value": UInt(0)]) + try expect(try expression.evaluate(context: context)).to.beFalse() } } - $0.describe("or expression") { - let expression = try! parseExpression(components: ["lhs", "or", "rhs"]) - - $0.it("evaluates to true with lhs true") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue() - } - - $0.it("evaluates to true with rhs true") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beTrue() - } - - $0.it("evaluates to true with lhs and rhs true") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() - } - - $0.it("evaluates to false with lhs and rhs false") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse() - } - } - - $0.describe("equality expression") { - let expression = try! parseExpression(components: ["lhs", "==", "rhs"]) - - $0.it("evaluates to true with equal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue() - } - - $0.it("evaluates to false with non equal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beFalse() - } - - $0.it("evaluates to true with nils") { - try expect(expression.evaluate(context: Context(dictionary: [:]))).to.beTrue() - } - - $0.it("evaluates to true with numbers") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.0]))).to.beTrue() - } - - $0.it("evaluates to false with non equal numbers") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.1]))).to.beFalse() - } - - $0.it("evaluates to true with booleans") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() - } - - $0.it("evaluates to false with falsy booleans") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse() - } - - $0.it("evaluates to false with different types") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": 1]))).to.beFalse() - } - } - - $0.describe("inequality expression") { - let expression = try! parseExpression(components: ["lhs", "!=", "rhs"]) - - $0.it("evaluates to true with inequal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue() - } - - $0.it("evaluates to false with equal lhs/rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "b", "rhs": "b"]))).to.beFalse() - } - } - - $0.describe("more than expression") { - let expression = try! parseExpression(components: ["lhs", ">", "rhs"]) - - $0.it("evaluates to true with lhs > rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue() - } - - $0.it("evaluates to false with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse() - } - } - - $0.describe("more than equal expression") { - let expression = try! parseExpression(components: ["lhs", ">=", "rhs"]) - - $0.it("evaluates to true with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() - } - - $0.it("evaluates to false with lhs < rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.1]))).to.beFalse() - } - } - - $0.describe("less than expression") { - let expression = try! parseExpression(components: ["lhs", "<", "rhs"]) - - $0.it("evaluates to true with lhs < rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue() - } - - $0.it("evaluates to false with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse() - } - } - - $0.describe("less than equal expression") { - let expression = try! parseExpression(components: ["lhs", "<=", "rhs"]) - - $0.it("evaluates to true with lhs == rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() - } - - $0.it("evaluates to false with lhs > rhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.1, "rhs": 5.0]))).to.beFalse() - } - } - - $0.describe("multiple expression") { - let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"]) - - $0.it("evaluates to true with one") { - try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue() - } - - $0.it("evaluates to true with one and three") { - try expect(expression.evaluate(context: Context(dictionary: ["one": true, "three": true]))).to.beTrue() - } - - $0.it("evaluates to true with two") { - try expect(expression.evaluate(context: Context(dictionary: ["two": true]))).to.beTrue() - } - - $0.it("evaluates to false with two and three") { - try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse() - } - - $0.it("evaluates to false with two and three") { - try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse() - } - - $0.it("evaluates to false with nothing") { + $0.describe("NotExpression") { + $0.it("returns truthy for positive expressions") { + let expression = NotExpression(expression: StaticExpression(value: true)) try expect(expression.evaluate(context: Context())).to.beFalse() } - } - - $0.describe("in expression") { - let expression = try! parseExpression(components: ["lhs", "in", "rhs"]) - - $0.it("evaluates to true when rhs contains lhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["a", "b", "c"]]))).to.beTrue() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "abc"]))).to.beTrue() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1...3]))).to.beTrue() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1..<3]))).to.beTrue() - } - - $0.it("evaluates to false when rhs does not contain lhs") { - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [2, 3, 4]]))).to.beFalse() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["b", "c", "d"]]))).to.beFalse() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "bcd"]))).to.beFalse() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 1...3]))).to.beFalse() - try expect(expression.evaluate(context: Context(dictionary: ["lhs": 3, "rhs": 1..<3]))).to.beFalse() + + $0.it("returns falsy for negative expressions") { + let expression = NotExpression(expression: StaticExpression(value: false)) + try expect(expression.evaluate(context: Context())).to.beTrue() } } - $0.describe("sub expression") { - $0.it("evaluates correctly") { - let context = Context(dictionary: ["one": false, "two": false, "three": true, "four": true]) - - let expression = try! parseExpression(components: ["one", "and", "two", "or", "three", "and", "four"]) - let expressionWithBrackets = try! parseExpression(components: ["one", "and", "(", "(", "two", ")", "or", "(", "three", "and", "four", ")", ")"]) - - try expect(expression.evaluate(context: context)).to.beTrue() - try expect(expressionWithBrackets.evaluate(context: context)).to.beFalse() - - let notExpression = try! parseExpression(components: ["not", "one", "or", "three"]) - let notExpressionWithBrackets = try! parseExpression(components: ["not", "(", "one", "or", "three", ")"]) - - try expect(notExpression.evaluate(context: context)).to.beTrue() - try expect(notExpressionWithBrackets.evaluate(context: context)).to.beFalse() + $0.describe("expression parsing") { + $0.it("can parse a variable expression") { + let expression = try parseExpression(components: ["value"]) + try expect(expression.evaluate(context: Context())).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue() } - $0.it("fails when brackets are not balanced") { - try expect(parseExpression(components: ["(", "lhs", "and", "rhs"])) - .toThrow(TemplateSyntaxError("'if' expression error: missing closing bracket")) - try expect(parseExpression(components: [")", "lhs", "and", "rhs"])) - .toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket")) - try expect(parseExpression(components: ["lhs", "and", "rhs", ")"])) - .toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket")) - try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", "("])) - .toThrow(TemplateSyntaxError("'if' expression error: missing closing bracket")) - try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", ")"])) - .toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket")) - try expect(parseExpression(components: ["(", "lhs", "and", ")"])) - .toThrow(TemplateSyntaxError("'if' expression error: end")) - try expect(parseExpression(components: ["(", "and", "rhs", ")"])) - .toThrow(TemplateSyntaxError("'if' expression error: infix operator 'and' doesn't have a left hand side")) + $0.it("can parse a not expression") { + let expression = try parseExpression(components: ["not", "value"]) + try expect(expression.evaluate(context: Context())).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse() + } + + $0.describe("and expression") { + let expression = try! parseExpression(components: ["lhs", "and", "rhs"]) + + $0.it("evaluates to false with lhs false") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse() + } + + $0.it("evaluates to false with rhs false") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse() + } + + $0.it("evaluates to false with lhs and rhs false") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse() + } + + $0.it("evaluates to true with lhs and rhs true") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() + } + } + + $0.describe("or expression") { + let expression = try! parseExpression(components: ["lhs", "or", "rhs"]) + + $0.it("evaluates to true with lhs true") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue() + } + + $0.it("evaluates to true with rhs true") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beTrue() + } + + $0.it("evaluates to true with lhs and rhs true") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() + } + + $0.it("evaluates to false with lhs and rhs false") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse() + } + } + + $0.describe("equality expression") { + let expression = try! parseExpression(components: ["lhs", "==", "rhs"]) + + $0.it("evaluates to true with equal lhs/rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue() + } + + $0.it("evaluates to false with non equal lhs/rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beFalse() + } + + $0.it("evaluates to true with nils") { + try expect(expression.evaluate(context: Context(dictionary: [:]))).to.beTrue() + } + + $0.it("evaluates to true with numbers") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.0]))).to.beTrue() + } + + $0.it("evaluates to false with non equal numbers") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.1]))).to.beFalse() + } + + $0.it("evaluates to true with booleans") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() + } + + $0.it("evaluates to false with falsy booleans") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse() + } + + $0.it("evaluates to false with different types") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": 1]))).to.beFalse() + } + } + + $0.describe("inequality expression") { + let expression = try! parseExpression(components: ["lhs", "!=", "rhs"]) + + $0.it("evaluates to true with inequal lhs/rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue() + } + + $0.it("evaluates to false with equal lhs/rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "b", "rhs": "b"]))).to.beFalse() + } + } + + $0.describe("more than expression") { + let expression = try! parseExpression(components: ["lhs", ">", "rhs"]) + + $0.it("evaluates to true with lhs > rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue() + } + + $0.it("evaluates to false with lhs == rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse() + } + } + + $0.describe("more than equal expression") { + let expression = try! parseExpression(components: ["lhs", ">=", "rhs"]) + + $0.it("evaluates to true with lhs == rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() + } + + $0.it("evaluates to false with lhs < rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.1]))).to.beFalse() + } + } + + $0.describe("less than expression") { + let expression = try! parseExpression(components: ["lhs", "<", "rhs"]) + + $0.it("evaluates to true with lhs < rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue() + } + + $0.it("evaluates to false with lhs == rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse() + } + } + + $0.describe("less than equal expression") { + let expression = try! parseExpression(components: ["lhs", "<=", "rhs"]) + + $0.it("evaluates to true with lhs == rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() + } + + $0.it("evaluates to false with lhs > rhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.1, "rhs": 5.0]))).to.beFalse() + } + } + + $0.describe("multiple expression") { + let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"]) + + $0.it("evaluates to true with one") { + try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue() + } + + $0.it("evaluates to true with one and three") { + try expect(expression.evaluate(context: Context(dictionary: ["one": true, "three": true]))).to.beTrue() + } + + $0.it("evaluates to true with two") { + try expect(expression.evaluate(context: Context(dictionary: ["two": true]))).to.beTrue() + } + + $0.it("evaluates to false with two and three") { + try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse() + } + + $0.it("evaluates to false with two and three") { + try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse() + } + + $0.it("evaluates to false with nothing") { + try expect(expression.evaluate(context: Context())).to.beFalse() + } + } + + $0.describe("in expression") { + let expression = try! parseExpression(components: ["lhs", "in", "rhs"]) + + $0.it("evaluates to true when rhs contains lhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["a", "b", "c"]]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "abc"]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1...3]))).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1..<3]))).to.beTrue() + } + + $0.it("evaluates to false when rhs does not contain lhs") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [2, 3, 4]]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["b", "c", "d"]]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "bcd"]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 1...3]))).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["lhs": 3, "rhs": 1..<3]))).to.beFalse() + } + } + + $0.describe("sub expression") { + $0.it("evaluates correctly") { + let context = Context(dictionary: ["one": false, "two": false, "three": true, "four": true]) + + let expression = try! parseExpression(components: ["one", "and", "two", "or", "three", "and", "four"]) + let expressionWithBrackets = try! parseExpression(components: ["one", "and", "(", "(", "two", ")", "or", "(", "three", "and", "four", ")", ")"]) + + try expect(expression.evaluate(context: context)).to.beTrue() + try expect(expressionWithBrackets.evaluate(context: context)).to.beFalse() + + let notExpression = try! parseExpression(components: ["not", "one", "or", "three"]) + let notExpressionWithBrackets = try! parseExpression(components: ["not", "(", "one", "or", "three", ")"]) + + try expect(notExpression.evaluate(context: context)).to.beTrue() + try expect(notExpressionWithBrackets.evaluate(context: context)).to.beFalse() + } + + $0.it("fails when brackets are not balanced") { + try expect(parseExpression(components: ["(", "lhs", "and", "rhs"])) + .toThrow(TemplateSyntaxError("'if' expression error: missing closing bracket")) + try expect(parseExpression(components: [")", "lhs", "and", "rhs"])) + .toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket")) + try expect(parseExpression(components: ["lhs", "and", "rhs", ")"])) + .toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket")) + try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", "("])) + .toThrow(TemplateSyntaxError("'if' expression error: missing closing bracket")) + try expect(parseExpression(components: ["(", "lhs", "and", "rhs", ")", ")"])) + .toThrow(TemplateSyntaxError("'if' expression error: missing opening bracket")) + try expect(parseExpression(components: ["(", "lhs", "and", ")"])) + .toThrow(TemplateSyntaxError("'if' expression error: end")) + try expect(parseExpression(components: ["(", "and", "rhs", ")"])) + .toThrow(TemplateSyntaxError("'if' expression error: infix operator 'and' doesn't have a left hand side")) + } } } } diff --git a/Tests/StencilTests/FilterSpec.swift b/Tests/StencilTests/FilterSpec.swift index d798632..9e4a58a 100644 --- a/Tests/StencilTests/FilterSpec.swift +++ b/Tests/StencilTests/FilterSpec.swift @@ -1,370 +1,372 @@ +import XCTest import Spectre @testable import Stencil +class FilterTests: XCTestCase { + func testFilter() { + describe("template filters") { + let context: [String: Any] = ["name": "Kyle"] -func testFilter() { - describe("template filters") { - let context: [String: Any] = ["name": "Kyle"] + $0.it("allows you to register a custom filter") { + let template = Template(templateString: "{{ name|repeat }}") - $0.it("allows you to register a custom filter") { - let template = Template(templateString: "{{ name|repeat }}") + let repeatExtension = Extension() + repeatExtension.registerFilter("repeat") { (value: Any?) in + if let value = value as? String { + return "\(value) \(value)" + } - let repeatExtension = Extension() - repeatExtension.registerFilter("repeat") { (value: Any?) in - if let value = value as? String { - return "\(value) \(value)" + return nil } - return nil + let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))) + try expect(result) == "Kyle Kyle" } - let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))) - try expect(result) == "Kyle Kyle" - } - - $0.it("allows you to register a custom filter which accepts single argument") { - let template = Template(templateString: """ - {{ name|repeat:'value1, "value2"' }} - """) - - let repeatExtension = Extension() - repeatExtension.registerFilter("repeat") { value, arguments in - if !arguments.isEmpty { - return "\(value!) \(value!) with args \(arguments.first!!)" - } - - return nil - } - - let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))) - try expect(result) == """ - Kyle Kyle with args value1, "value2" - """ - } - - $0.it("allows you to register a custom filter which accepts several arguments") { + $0.it("allows you to register a custom filter which accepts single argument") { let template = Template(templateString: """ - {{ name|repeat:'value"1"',"value'2'",'(key, value)' }} + {{ name|repeat:'value1, "value2"' }} """) let repeatExtension = Extension() repeatExtension.registerFilter("repeat") { value, arguments in - if !arguments.isEmpty { - return "\(value!) \(value!) with args 0: \(arguments[0]!), 1: \(arguments[1]!), 2: \(arguments[2]!)" - } + if !arguments.isEmpty { + return "\(value!) \(value!) with args \(arguments.first!!)" + } - return nil + return nil } let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))) try expect(result) == """ - Kyle Kyle with args 0: value"1", 1: value'2', 2: (key, value) + Kyle Kyle with args value1, "value2" """ - } - - $0.it("allows you to register a custom which throws") { - let template = Template(templateString: "{{ name|repeat }}") - let repeatExtension = Extension() - repeatExtension.registerFilter("repeat") { (value: Any?) in - throw TemplateSyntaxError("No Repeat") } - let context = Context(dictionary: context, environment: Environment(extensions: [repeatExtension])) - try expect(try template.render(context)).toThrow(TemplateSyntaxError(reason: "No Repeat", token: template.tokens.first)) - } + $0.it("allows you to register a custom filter which accepts several arguments") { + let template = Template(templateString: """ + {{ name|repeat:'value"1"',"value'2'",'(key, value)' }} + """) - $0.it("allows you to override a default filter") { - let template = Template(templateString: "{{ name|join }}") + let repeatExtension = Extension() + repeatExtension.registerFilter("repeat") { value, arguments in + if !arguments.isEmpty { + return "\(value!) \(value!) with args 0: \(arguments[0]!), 1: \(arguments[1]!), 2: \(arguments[2]!)" + } - let repeatExtension = Extension() - repeatExtension.registerFilter("join") { (value: Any?) in - return "joined" + return nil + } + + let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))) + try expect(result) == """ + Kyle Kyle with args 0: value"1", 1: value'2', 2: (key, value) + """ } - let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))) - try expect(result) == "joined" + $0.it("allows you to register a custom which throws") { + let template = Template(templateString: "{{ name|repeat }}") + let repeatExtension = Extension() + repeatExtension.registerFilter("repeat") { (value: Any?) in + throw TemplateSyntaxError("No Repeat") + } + + let context = Context(dictionary: context, environment: Environment(extensions: [repeatExtension])) + try expect(try template.render(context)).toThrow(TemplateSyntaxError(reason: "No Repeat", token: template.tokens.first)) + } + + $0.it("allows you to override a default filter") { + let template = Template(templateString: "{{ name|join }}") + + let repeatExtension = Extension() + repeatExtension.registerFilter("join") { (value: Any?) in + return "joined" + } + + let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))) + try expect(result) == "joined" + } + + $0.it("allows whitespace in expression") { + let template = Template(templateString: """ + {{ value | join : ", " }} + """) + let result = try template.render(Context(dictionary: ["value": ["One", "Two"]])) + try expect(result) == "One, Two" + } + + $0.it("throws when you pass arguments to simple filter") { + let template = Template(templateString: "{{ name|uppercase:5 }}") + try expect(try template.render(Context(dictionary: ["name": "kyle"]))).toThrow() + } } - $0.it("allows whitespace in expression") { + describe("string filters") { + $0.context("given string") { + $0.it("transforms a string to be capitalized") { + let template = Template(templateString: "{{ name|capitalize }}") + let result = try template.render(Context(dictionary: ["name": "kyle"])) + try expect(result) == "Kyle" + } + + $0.it("transforms a string to be uppercase") { + let template = Template(templateString: "{{ name|uppercase }}") + let result = try template.render(Context(dictionary: ["name": "kyle"])) + try expect(result) == "KYLE" + } + + $0.it("transforms a string to be lowercase") { + let template = Template(templateString: "{{ name|lowercase }}") + let result = try template.render(Context(dictionary: ["name": "Kyle"])) + try expect(result) == "kyle" + } + } + + $0.context("given array of strings") { + $0.it("transforms a string to be capitalized") { + let template = Template(templateString: "{{ names|capitalize }}") + let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]])) + try expect(result) == """ + ["Kyle", "Kyle"] + """ + } + + $0.it("transforms a string to be uppercase") { + let template = Template(templateString: "{{ names|uppercase }}") + let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]])) + try expect(result) == """ + ["KYLE", "KYLE"] + """ + } + + $0.it("transforms a string to be lowercase") { + let template = Template(templateString: "{{ names|lowercase }}") + let result = try template.render(Context(dictionary: ["names": ["Kyle", "Kyle"]])) + try expect(result) == """ + ["kyle", "kyle"] + """ + } + } + } + + describe("default filter") { let template = Template(templateString: """ - {{ value | join : ", " }} - """) - let result = try template.render(Context(dictionary: ["value": ["One", "Two"]])) - try expect(result) == "One, Two" - } + Hello {{ name|default:"World" }} + """) - $0.it("throws when you pass arguments to simple filter") { - let template = Template(templateString: "{{ name|uppercase:5 }}") - try expect(try template.render(Context(dictionary: ["name": "kyle"]))).toThrow() - } - } - - describe("string filters") { - $0.context("given string") { - $0.it("transforms a string to be capitalized") { - let template = Template(templateString: "{{ name|capitalize }}") - let result = try template.render(Context(dictionary: ["name": "kyle"])) - try expect(result) == "Kyle" - } - - $0.it("transforms a string to be uppercase") { - let template = Template(templateString: "{{ name|uppercase }}") - let result = try template.render(Context(dictionary: ["name": "kyle"])) - try expect(result) == "KYLE" - } - - $0.it("transforms a string to be lowercase") { - let template = Template(templateString: "{{ name|lowercase }}") + $0.it("shows the variable value") { let result = try template.render(Context(dictionary: ["name": "Kyle"])) - try expect(result) == "kyle" + try expect(result) == "Hello Kyle" + } + + $0.it("shows the default value") { + let result = try template.render(Context(dictionary: [:])) + try expect(result) == "Hello World" + } + + $0.it("supports multiple defaults") { + let template = Template(templateString: """ + Hello {{ name|default:a,b,c,"World" }} + """) + let result = try template.render(Context(dictionary: [:])) + try expect(result) == "Hello World" + } + + $0.it("can use int as default") { + let template = Template(templateString: "{{ value|default:1 }}") + let result = try template.render(Context(dictionary: [:])) + try expect(result) == "1" + } + + $0.it("can use float as default") { + let template = Template(templateString: "{{ value|default:1.5 }}") + let result = try template.render(Context(dictionary: [:])) + try expect(result) == "1.5" + } + + $0.it("checks for underlying nil value correctly") { + let template = Template(templateString: """ + Hello {{ user.name|default:"anonymous" }} + """) + let nilName: String? = nil + let user: [String: Any?] = ["name": nilName] + let result = try template.render(Context(dictionary: ["user": user])) + try expect(result) == "Hello anonymous" } } - $0.context("given array of strings") { - $0.it("transforms a string to be capitalized") { - let template = Template(templateString: "{{ names|capitalize }}") - let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]])) + describe("join filter") { + let template = Template(templateString: """ + {{ value|join:", " }} + """) + + $0.it("joins a collection of strings") { + let result = try template.render(Context(dictionary: ["value": ["One", "Two"]])) + try expect(result) == "One, Two" + } + + $0.it("joins a mixed-type collection") { + let result = try template.render(Context(dictionary: ["value": ["One", 2, true, 10.5, "Five"]])) + try expect(result) == "One, 2, true, 10.5, Five" + } + + $0.it("can join by non string") { + let template = Template(templateString: """ + {{ value|join:separator }} + """) + let result = try template.render(Context(dictionary: ["value": ["One", "Two"], "separator": true])) + try expect(result) == "OnetrueTwo" + } + + $0.it("can join without arguments") { + let template = Template(templateString: """ + {{ value|join }} + """) + let result = try template.render(Context(dictionary: ["value": ["One", "Two"]])) + try expect(result) == "OneTwo" + } + } + + describe("split filter") { + let template = Template(templateString: """ + {{ value|split:", " }} + """) + + $0.it("split a string into array") { + let result = try template.render(Context(dictionary: ["value": "One, Two"])) try expect(result) == """ - ["Kyle", "Kyle"] + ["One", "Two"] """ } - $0.it("transforms a string to be uppercase") { - let template = Template(templateString: "{{ names|uppercase }}") - let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]])) + $0.it("can split without arguments") { + let template = Template(templateString: """ + {{ value|split }} + """) + let result = try template.render(Context(dictionary: ["value": "One, Two"])) try expect(result) == """ - ["KYLE", "KYLE"] - """ - } - - $0.it("transforms a string to be lowercase") { - let template = Template(templateString: "{{ names|lowercase }}") - let result = try template.render(Context(dictionary: ["names": ["Kyle", "Kyle"]])) - try expect(result) == """ - ["kyle", "kyle"] + ["One,", "Two"] """ } } - } - - describe("default filter") { - let template = Template(templateString: """ - Hello {{ name|default:"World" }} - """) - - $0.it("shows the variable value") { - let result = try template.render(Context(dictionary: ["name": "Kyle"])) - try expect(result) == "Hello Kyle" - } - - $0.it("shows the default value") { - let result = try template.render(Context(dictionary: [:])) - try expect(result) == "Hello World" - } - - $0.it("supports multiple defaults") { - let template = Template(templateString: """ - Hello {{ name|default:a,b,c,"World" }} - """) - let result = try template.render(Context(dictionary: [:])) - try expect(result) == "Hello World" - } - - $0.it("can use int as default") { - let template = Template(templateString: "{{ value|default:1 }}") - let result = try template.render(Context(dictionary: [:])) - try expect(result) == "1" - } - - $0.it("can use float as default") { - let template = Template(templateString: "{{ value|default:1.5 }}") - let result = try template.render(Context(dictionary: [:])) - try expect(result) == "1.5" - } - - $0.it("checks for underlying nil value correctly") { - let template = Template(templateString: """ - Hello {{ user.name|default:"anonymous" }} - """) - let nilName: String? = nil - let user: [String: Any?] = ["name": nilName] - let result = try template.render(Context(dictionary: ["user": user])) - try expect(result) == "Hello anonymous" - } - } - - describe("join filter") { - let template = Template(templateString: """ - {{ value|join:", " }} - """) - - $0.it("joins a collection of strings") { - let result = try template.render(Context(dictionary: ["value": ["One", "Two"]])) - try expect(result) == "One, Two" - } - - $0.it("joins a mixed-type collection") { - let result = try template.render(Context(dictionary: ["value": ["One", 2, true, 10.5, "Five"]])) - try expect(result) == "One, 2, true, 10.5, Five" - } - - $0.it("can join by non string") { - let template = Template(templateString: """ - {{ value|join:separator }} - """) - let result = try template.render(Context(dictionary: ["value": ["One", "Two"], "separator": true])) - try expect(result) == "OnetrueTwo" - } - - $0.it("can join without arguments") { - let template = Template(templateString: """ - {{ value|join }} - """) - let result = try template.render(Context(dictionary: ["value": ["One", "Two"]])) - try expect(result) == "OneTwo" - } - } - - describe("split filter") { - let template = Template(templateString: """ - {{ value|split:", " }} - """) - - $0.it("split a string into array") { - let result = try template.render(Context(dictionary: ["value": "One, Two"])) - try expect(result) == """ - ["One", "Two"] - """ - } - - $0.it("can split without arguments") { - let template = Template(templateString: """ - {{ value|split }} - """) - let result = try template.render(Context(dictionary: ["value": "One, Two"])) - try expect(result) == """ - ["One,", "Two"] - """ - } - } - describe("filter suggestion") { - var template: Template! - var filterExtension: Extension! + describe("filter suggestion") { + var template: Template! + var filterExtension: Extension! - func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { - guard let range = template.templateString.range(of: token) else { - fatalError("Can't find '\(token)' in '\(template)'") + func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError { + guard let range = template.templateString.range(of: token) else { + fatalError("Can't find '\(token)' in '\(template)'") + } + let lexer = Lexer(templateString: template.templateString) + let location = lexer.rangeLocation(range) + let sourceMap = SourceMap(filename: template.name, location: location) + let token = Token.block(value: token, at: sourceMap) + return TemplateSyntaxError(reason: description, token: token, stackTrace: []) } - let lexer = Lexer(templateString: template.templateString) - let location = lexer.rangeLocation(range) - let sourceMap = SourceMap(filename: template.name, location: location) - let token = Token.block(value: token, at: sourceMap) - return TemplateSyntaxError(reason: description, token: token, stackTrace: []) + + func expectError(reason: String, token: String, + file: String = #file, line: Int = #line, function: String = #function) throws { + let expectedError = expectedSyntaxError(token: token, template: template, description: reason) + let environment = Environment(extensions: [filterExtension]) + + let error = try expect(environment.render(template: template, context: [:]), + file: file, line: line, function: function).toThrow() as TemplateSyntaxError + let reporter = SimpleErrorReporter() + try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError) + } + + $0.it("made for unknown filter") { + template = Template(templateString: "{{ value|unknownFilter }}") + + filterExtension = Extension() + filterExtension.registerFilter("knownFilter") { value, _ in value } + + try expectError(reason: "Unknown filter 'unknownFilter'. Found similar filters: 'knownFilter'.", token: "value|unknownFilter") + } + + $0.it("made for multiple similar filters") { + template = Template(templateString: "{{ value|lowerFirst }}") + + filterExtension = Extension() + filterExtension.registerFilter("lowerFirstWord") { value, _ in value } + filterExtension.registerFilter("lowerFirstLetter") { value, _ in value } + + try expectError(reason: "Unknown filter 'lowerFirst'. Found similar filters: 'lowerFirstWord', 'lowercase'.", token: "value|lowerFirst") + } + + $0.it("not made when can't find similar filter") { + template = Template(templateString: "{{ value|unknownFilter }}") + try expectError(reason: "Unknown filter 'unknownFilter'. Found similar filters: 'lowerFirstWord'.", token: "value|unknownFilter") + } + } - func expectError(reason: String, token: String, - file: String = #file, line: Int = #line, function: String = #function) throws { - let expectedError = expectedSyntaxError(token: token, template: template, description: reason) - let environment = Environment(extensions: [filterExtension]) - let error = try expect(environment.render(template: template, context: [:]), - file: file, line: line, function: function).toThrow() as TemplateSyntaxError - let reporter = SimpleErrorReporter() - try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError) - } - - $0.it("made for unknown filter") { - template = Template(templateString: "{{ value|unknownFilter }}") - - filterExtension = Extension() - filterExtension.registerFilter("knownFilter") { value, _ in value } - - try expectError(reason: "Unknown filter 'unknownFilter'. Found similar filters: 'knownFilter'.", token: "value|unknownFilter") - } - - $0.it("made for multiple similar filters") { - template = Template(templateString: "{{ value|lowerFirst }}") - - filterExtension = Extension() - filterExtension.registerFilter("lowerFirstWord") { value, _ in value } - filterExtension.registerFilter("lowerFirstLetter") { value, _ in value } - - try expectError(reason: "Unknown filter 'lowerFirst'. Found similar filters: 'lowerFirstWord', 'lowercase'.", token: "value|lowerFirst") - } - - $0.it("not made when can't find similar filter") { - template = Template(templateString: "{{ value|unknownFilter }}") - try expectError(reason: "Unknown filter 'unknownFilter'. Found similar filters: 'lowerFirstWord'.", token: "value|unknownFilter") - } - - } - - - describe("indent filter") { - $0.it("indents content") { - let template = Template(templateString: """ - {{ value|indent:2 }} - """) - let result = try template.render(Context(dictionary: ["value": """ - One - Two - """])) - try expect(result) == """ - One - Two - """ - } - - $0.it("can indent with arbitrary character") { - let template = Template(templateString: """ - {{ value|indent:2,"\t" }} - """) - let result = try template.render(Context(dictionary: ["value": """ - One - Two - """])) - try expect(result) == """ - One - \t\tTwo - """ - } - - $0.it("can indent first line") { - let template = Template(templateString: """ - {{ value|indent:2," ",true }} - """) - let result = try template.render(Context(dictionary: ["value": """ - One - Two - """])) - try expect(result) == """ + describe("indent filter") { + $0.it("indents content") { + let template = Template(templateString: """ + {{ value|indent:2 }} + """) + let result = try template.render(Context(dictionary: ["value": """ One Two - """ - } - - $0.it("does not indent empty lines") { - let template = Template(templateString: """ - {{ value|indent }} - """) - let result = try template.render(Context(dictionary: ["value": """ - One - - - Two - - - """])) - try expect(result) == """ - One - - + """])) + try expect(result) == """ + One Two + """ + } + + $0.it("can indent with arbitrary character") { + let template = Template(templateString: """ + {{ value|indent:2,"\t" }} + """) + let result = try template.render(Context(dictionary: ["value": """ + One + Two + """])) + try expect(result) == """ + One + \t\tTwo + """ + } + + $0.it("can indent first line") { + let template = Template(templateString: """ + {{ value|indent:2," ",true }} + """) + let result = try template.render(Context(dictionary: ["value": """ + One + Two + """])) + try expect(result) == """ + One + Two + """ + } + + $0.it("does not indent empty lines") { + let template = Template(templateString: """ + {{ value|indent }} + """) + let result = try template.render(Context(dictionary: ["value": """ + One - """ + Two + + + """])) + try expect(result) == """ + One + + + Two + + + """ + } } } } diff --git a/Tests/StencilTests/FilterTagSpec.swift b/Tests/StencilTests/FilterTagSpec.swift index 4899531..5423747 100644 --- a/Tests/StencilTests/FilterTagSpec.swift +++ b/Tests/StencilTests/FilterTagSpec.swift @@ -1,49 +1,51 @@ +import XCTest import Spectre import Stencil +class FilterTagTests: XCTestCase { + func testFilterTag() { + describe("Filter Tag") { + $0.it("allows you to use a filter") { + let template = Template(templateString: "{% filter uppercase %}Test{% endfilter %}") + let result = try template.render() + try expect(result) == "TEST" + } -func testFilterTag() { - describe("Filter Tag") { - $0.it("allows you to use a filter") { - let template = Template(templateString: "{% filter uppercase %}Test{% endfilter %}") - let result = try template.render() - try expect(result) == "TEST" - } + $0.it("allows you to chain filters") { + let template = Template(templateString: "{% filter lowercase|capitalize %}TEST{% endfilter %}") + let result = try template.render() + try expect(result) == "Test" + } - $0.it("allows you to chain filters") { - let template = Template(templateString: "{% filter lowercase|capitalize %}TEST{% endfilter %}") - let result = try template.render() - try expect(result) == "Test" - } + $0.it("errors without a filter") { + let template = Template(templateString: "Some {% filter %}Test{% endfilter %}") + try expect(try template.render()).toThrow() + } - $0.it("errors without a filter") { - let template = Template(templateString: "Some {% filter %}Test{% endfilter %}") - try expect(try template.render()).toThrow() - } - - $0.it("can render filters with arguments") { - let ext = Extension() - ext.registerFilter("split", filter: { - return ($0 as! String).components(separatedBy: $1[0] as! String) - }) - let env = Environment(extensions: [ext]) - let result = try env.renderTemplate(string: """ + $0.it("can render filters with arguments") { + let ext = Extension() + ext.registerFilter("split", filter: { + return ($0 as! String).components(separatedBy: $1[0] as! String) + }) + let env = Environment(extensions: [ext]) + let result = try env.renderTemplate(string: """ {% filter split:","|join:";" %}{{ items|join:"," }}{% endfilter %} """, context: ["items": [1, 2]]) - try expect(result) == "1;2" - } + try expect(result) == "1;2" + } - $0.it("can render filters with quote as an argument") { + $0.it("can render filters with quote as an argument") { let ext = Extension() ext.registerFilter("replace", filter: { - print($1[0] as! String) - return ($0 as! String).replacingOccurrences(of: $1[0] as! String, with: $1[1] as! String) + print($1[0] as! String) + return ($0 as! String).replacingOccurrences(of: $1[0] as! String, with: $1[1] as! String) }) let env = Environment(extensions: [ext]) let result = try env.renderTemplate(string: """ {% filter replace:'"',"" %}{{ items|join:"," }}{% endfilter %} """, context: ["items": ["\"1\"", "\"2\""]]) try expect(result) == "1,2" + } } } } diff --git a/Tests/StencilTests/ForNodeSpec.swift b/Tests/StencilTests/ForNodeSpec.swift index e362e91..8cd7846 100644 --- a/Tests/StencilTests/ForNodeSpec.swift +++ b/Tests/StencilTests/ForNodeSpec.swift @@ -1,351 +1,353 @@ +import XCTest import Spectre @testable import Stencil import Foundation - -func testForNode() { - describe("ForNode") { - let context = Context(dictionary: [ - "items": [1, 2, 3], - "emptyItems": [Int](), - "dict": [ - "one": "I", - "two": "II", - ], - "tuples": [(1, 2, 3), (4, 5, 6)] - ]) - - $0.it("renders the given nodes for each item") { - let nodes: [NodeType] = [VariableNode(variable: "item")] - let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(context)) == "123" - } - - $0.it("renders the given empty nodes when no items found item") { - let nodes: [NodeType] = [VariableNode(variable: "item")] - let emptyNodes: [NodeType] = [TextNode(text: "empty")] - let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes) - try expect(try node.render(context)) == "empty" - } - - $0.it("renders a context variable of type Array") { - let any_context = Context(dictionary: [ - "items": ([1, 2, 3] as [Any]) +class ForNodeTests: XCTestCase { + func testForNode() { + describe("ForNode") { + let context = Context(dictionary: [ + "items": [1, 2, 3], + "emptyItems": [Int](), + "dict": [ + "one": "I", + "two": "II", + ], + "tuples": [(1, 2, 3), (4, 5, 6)] ]) - let nodes: [NodeType] = [VariableNode(variable: "item")] - let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(any_context)) == "123" - } + $0.it("renders the given nodes for each item") { + let nodes: [NodeType] = [VariableNode(variable: "item")] + let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + try expect(try node.render(context)) == "123" + } - $0.it("renders a context variable of type CountableClosedRange") { - let context = Context(dictionary: ["range": 1...3]) - let nodes: [NodeType] = [VariableNode(variable: "item")] - let node = ForNode(resolvable: Variable("range"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + $0.it("renders the given empty nodes when no items found item") { + let nodes: [NodeType] = [VariableNode(variable: "item")] + let emptyNodes: [NodeType] = [TextNode(text: "empty")] + let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes) + try expect(try node.render(context)) == "empty" + } - try expect(try node.render(context)) == "123" - } + $0.it("renders a context variable of type Array") { + let any_context = Context(dictionary: [ + "items": ([1, 2, 3] as [Any]) + ]) - $0.it("renders a context variable of type CountableRange") { - let context = Context(dictionary: ["range": 1..<4]) - let nodes: [NodeType] = [VariableNode(variable: "item")] - let node = ForNode(resolvable: Variable("range"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + let nodes: [NodeType] = [VariableNode(variable: "item")] + let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + try expect(try node.render(any_context)) == "123" + } - try expect(try node.render(context)) == "123" - } + $0.it("renders a context variable of type CountableClosedRange") { + let context = Context(dictionary: ["range": 1...3]) + let nodes: [NodeType] = [VariableNode(variable: "item")] + let node = ForNode(resolvable: Variable("range"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - #if os(OSX) - $0.it("renders a context variable of type NSArray") { - let nsarray_context = Context(dictionary: [ - "items": NSArray(array: [1, 2, 3]) - ]) + try expect(try node.render(context)) == "123" + } - let nodes: [NodeType] = [VariableNode(variable: "item")] - let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(nsarray_context)) == "123" - } - #endif + $0.it("renders a context variable of type CountableRange") { + let context = Context(dictionary: ["range": 1..<4]) + let nodes: [NodeType] = [VariableNode(variable: "item")] + let node = ForNode(resolvable: Variable("range"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - $0.it("renders the given nodes while providing if the item is first in the context") { - let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.first")] - let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(context)) == "1true2false3false" - } + try expect(try node.render(context)) == "123" + } - $0.it("renders the given nodes while providing if the item is last in the context") { - let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.last")] - let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(context)) == "1false2false3true" - } + #if os(OSX) + $0.it("renders a context variable of type NSArray") { + let nsarray_context = Context(dictionary: [ + "items": NSArray(array: [1, 2, 3]) + ]) - $0.it("renders the given nodes while providing item counter") { - let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")] - let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(context)) == "112233" - } + let nodes: [NodeType] = [VariableNode(variable: "item")] + let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + try expect(try node.render(nsarray_context)) == "123" + } + #endif - $0.it("renders the given nodes while providing item counter") { - let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter0")] - let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(context)) == "102132" - } + $0.it("renders the given nodes while providing if the item is first in the context") { + let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.first")] + let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + try expect(try node.render(context)) == "1true2false3false" + } - $0.it("renders the given nodes while providing loop length") { - let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.length")] - let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) - try expect(try node.render(context)) == "132333" - } + $0.it("renders the given nodes while providing if the item is last in the context") { + let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.last")] + let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + try expect(try node.render(context)) == "1false2false3true" + } - $0.it("renders the given nodes while filtering items using where expression") { - let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")] - let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown)) - let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`) - try expect(try node.render(context)) == "2132" - } + $0.it("renders the given nodes while providing item counter") { + let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")] + let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + try expect(try node.render(context)) == "112233" + } - $0.it("renders the given empty nodes when all items filtered out with where expression") { - let nodes: [NodeType] = [VariableNode(variable: "item")] - let emptyNodes: [NodeType] = [TextNode(text: "empty")] - let `where` = try parseExpression(components: ["item", "==", "0"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown)) - let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`) - try expect(try node.render(context)) == "empty" - } + $0.it("renders the given nodes while providing item counter") { + let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter0")] + let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + try expect(try node.render(context)) == "102132" + } - $0.it("can render a filter with spaces") { - let templateString = """ + $0.it("renders the given nodes while providing loop length") { + let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.length")] + let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: []) + try expect(try node.render(context)) == "132333" + } + + $0.it("renders the given nodes while filtering items using where expression") { + let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")] + let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown)) + let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`) + try expect(try node.render(context)) == "2132" + } + + $0.it("renders the given empty nodes when all items filtered out with where expression") { + let nodes: [NodeType] = [VariableNode(variable: "item")] + let emptyNodes: [NodeType] = [TextNode(text: "empty")] + let `where` = try parseExpression(components: ["item", "==", "0"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown)) + let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`) + try expect(try node.render(context)) == "empty" + } + + $0.it("can render a filter with spaces") { + let templateString = """ {% for article in ars | default: a, b , articles %}\ - {{ article.title }} by {{ article.author }}. {% endfor %} """ - let context = Context(dictionary: [ - "articles": [ - Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"), - Article(title: "Memory Management with ARC", author: "Kyle Fuller"), - ] - ]) + let context = Context(dictionary: [ + "articles": [ + Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"), + Article(title: "Memory Management with ARC", author: "Kyle Fuller"), + ] + ]) - let template = Template(templateString: templateString) - let result = try template.render(context) + let template = Template(templateString: templateString) + let result = try template.render(context) - try expect(result) == """ + try expect(result) == """ - Migrating from OCUnit to XCTest by Kyle Fuller. - Memory Management with ARC by Kyle Fuller. """ - } + } - $0.context("given array of tuples") { - $0.it("can iterate over all tuple values") { - let templateString = """ + $0.context("given array of tuples") { + $0.it("can iterate over all tuple values") { + let templateString = """ {% for first,second,third in tuples %}\ {{ first }}, {{ second }}, {{ third }} {% endfor %} """ - let template = Template(templateString: templateString) - let result = try template.render(context) + let template = Template(templateString: templateString) + let result = try template.render(context) - try expect(result) == """ + try expect(result) == """ 1, 2, 3 4, 5, 6 """ - } + } - $0.it("can iterate with less number of variables") { - let templateString = """ + $0.it("can iterate with less number of variables") { + let templateString = """ {% for first,second in tuples %}\ {{ first }}, {{ second }} {% endfor %} """ - let template = Template(templateString: templateString) - let result = try template.render(context) + let template = Template(templateString: templateString) + let result = try template.render(context) - try expect(result) == """ + try expect(result) == """ 1, 2 4, 5 """ - } + } - $0.it("can use _ to skip variables") { - let templateString = """ + $0.it("can use _ to skip variables") { + let templateString = """ {% for first,_,third in tuples %}\ {{ first }}, {{ third }} {% endfor %} """ - let template = Template(templateString: templateString) - let result = try template.render(context) + let template = Template(templateString: templateString) + let result = try template.render(context) - try expect(result) == """ + try expect(result) == """ 1, 3 4, 6 """ - } + } - $0.it("throws when number of variables is more than number of tuple values") { - let templateString = """ + $0.it("throws when number of variables is more than number of tuple values") { + let templateString = """ {% for key,value,smth in dict %} {% endfor %} """ - let template = Template(templateString: templateString) - try expect(template.render(context)).toThrow() + let template = Template(templateString: templateString) + try expect(template.render(context)).toThrow() + } + } - } - - $0.it("can iterate over dictionary") { - let templateString = """ + $0.it("can iterate over dictionary") { + let templateString = """ {% for key, value in dict %}\ {{ key }}: {{ value }},\ {% endfor %} """ - let template = Template(templateString: templateString) - let result = try template.render(context) + let template = Template(templateString: templateString) + let result = try template.render(context) - try expect(result) == """ + try expect(result) == """ one: I,two: II, """ - } - - $0.it("renders supports iterating over dictionary") { - let nodes: [NodeType] = [ - VariableNode(variable: "key"), - TextNode(text: ","), - ] - let emptyNodes: [NodeType] = [TextNode(text: "empty")] - let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key"], nodes: nodes, emptyNodes: emptyNodes, where: nil) - let result = try node.render(context) - - try expect(result) == """ - one,two, - """ - } - - $0.it("renders supports iterating over dictionary") { - let nodes: [NodeType] = [ - VariableNode(variable: "key"), - TextNode(text: "="), - VariableNode(variable: "value"), - TextNode(text: ","), - ] - let emptyNodes: [NodeType] = [TextNode(text: "empty")] - let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key", "value"], nodes: nodes, emptyNodes: emptyNodes, where: nil) - let result = try node.render(context) - - try expect(result) == """ - one=I,two=II, - """ - } - - $0.it("handles invalid input") { - let token = Token.block(value: "for i", at: .unknown) - let parser = TokenParser(tokens: [token], environment: Environment()) - let error = TemplateSyntaxError(reason: "'for' statements should use the syntax: `for in [where ]`.", token: token) - 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) - ]) + $0.it("renders supports iterating over dictionary") { + let nodes: [NodeType] = [ + VariableNode(variable: "key"), + TextNode(text: ","), + ] + let emptyNodes: [NodeType] = [TextNode(text: "empty")] + let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key"], nodes: nodes, emptyNodes: emptyNodes, where: nil) + let result = try node.render(context) - 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) == """ + one,two, + """ + } - try expect(result) == """ + $0.it("renders supports iterating over dictionary") { + let nodes: [NodeType] = [ + VariableNode(variable: "key"), + TextNode(text: "="), + VariableNode(variable: "value"), + TextNode(text: ","), + ] + let emptyNodes: [NodeType] = [TextNode(text: "empty")] + let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key", "value"], nodes: nodes, emptyNodes: emptyNodes, where: nil) + let result = try node.render(context) + + try expect(result) == """ + one=I,two=II, + """ + } + + $0.it("handles invalid input") { + let token = Token.block(value: "for i", at: .unknown) + let parser = TokenParser(tokens: [token], environment: Environment()) + let error = TemplateSyntaxError(reason: "'for' statements should use the syntax: `for in [where ]`.", token: token) + 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 number=123 """ - } + } - $0.it("can iterate tuple items") { - let context = Context(dictionary: [ - "tuple": (one: 1, two: "dva"), - ]) + $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 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) + let node = ForNode(resolvable: Variable("tuple"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: []) + let result = try node.render(context) - try expect(result) == """ + try expect(result) == """ one=1 two=dva """ - } - - $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) + $0.it("can iterate over class properties") { + class MyClass { + var baseString: String + var baseInt: Int + init(_ string: String, _ int: Int) { + baseString = string + baseInt = int + } } - } - let context = Context(dictionary: [ - "class": MySubclass("child", "base", 1) - ]) + class MySubclass: MyClass { + var childString: String + init(_ childString: String, _ string: String, _ int: Int) { + self.childString = childString + super.init(string, int) + } + } - let nodes: [NodeType] = [ - VariableNode(variable: "label"), - TextNode(text: "="), - VariableNode(variable: "value"), - TextNode(text: "\n"), - ] + let context = Context(dictionary: [ + "class": MySubclass("child", "base", 1) + ]) - let node = ForNode(resolvable: Variable("class"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: []) - let result = try node.render(context) + let nodes: [NodeType] = [ + VariableNode(variable: "label"), + TextNode(text: "="), + VariableNode(variable: "value"), + TextNode(text: "\n"), + ] - try expect(result) == """ + let node = ForNode(resolvable: Variable("class"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: []) + let result = try node.render(context) + + try expect(result) == """ childString=child baseString=base baseInt=1 """ - } + } + + $0.it("can iterate in range of variables") { + let template: Template = "{% for i in 1...j %}{{ i }}{% endfor %}" + try expect(try template.render(Context(dictionary: ["j": 3]))) == "123" + } - $0.it("can iterate in range of variables") { - let template: Template = "{% for i in 1...j %}{{ i }}{% endfor %}" - try expect(try template.render(Context(dictionary: ["j": 3]))) == "123" } } - } fileprivate struct Article { diff --git a/Tests/StencilTests/IfNodeSpec.swift b/Tests/StencilTests/IfNodeSpec.swift index cd662d7..c9d6ae7 100644 --- a/Tests/StencilTests/IfNodeSpec.swift +++ b/Tests/StencilTests/IfNodeSpec.swift @@ -1,242 +1,243 @@ +import XCTest import Spectre @testable import Stencil +class IfNodeTests: XCTestCase { + func testIfNode() { + describe("IfNode") { + $0.describe("parsing") { + $0.it("can parse an if block") { + let tokens: [Token] = [ + .block(value: "if value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "endif", at: .unknown) + ] -func testIfNode() { - describe("IfNode") { - $0.describe("parsing") { - $0.it("can parse an if block") { - let tokens: [Token] = [ - .block(value: "if value", at: .unknown), - .text(value: "true", at: .unknown), - .block(value: "endif", at: .unknown) - ] + let parser = TokenParser(tokens: tokens, environment: Environment()) + let nodes = try parser.parse() + let node = nodes.first as? IfNode - let parser = TokenParser(tokens: tokens, environment: Environment()) - let nodes = try parser.parse() - let node = nodes.first as? IfNode + let conditions = node?.conditions + try expect(conditions?.count) == 1 + try expect(conditions?[0].nodes.count) == 1 + let trueNode = conditions?[0].nodes.first as? TextNode + try expect(trueNode?.text) == "true" + } - let conditions = node?.conditions - try expect(conditions?.count) == 1 - try expect(conditions?[0].nodes.count) == 1 - let trueNode = conditions?[0].nodes.first as? TextNode - try expect(trueNode?.text) == "true" + $0.it("can parse an if with else block") { + let tokens: [Token] = [ + .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()) + let nodes = try parser.parse() + let node = nodes.first as? IfNode + + let conditions = node?.conditions + try expect(conditions?.count) == 2 + + try expect(conditions?[0].nodes.count) == 1 + let trueNode = conditions?[0].nodes.first as? TextNode + try expect(trueNode?.text) == "true" + + try expect(conditions?[1].nodes.count) == 1 + let falseNode = conditions?[1].nodes.first as? TextNode + try expect(falseNode?.text) == "false" + } + + $0.it("can parse an if with elif block") { + let tokens: [Token] = [ + .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()) + let nodes = try parser.parse() + let node = nodes.first as? IfNode + + let conditions = node?.conditions + try expect(conditions?.count) == 3 + + try expect(conditions?[0].nodes.count) == 1 + let trueNode = conditions?[0].nodes.first as? TextNode + try expect(trueNode?.text) == "true" + + try expect(conditions?[1].nodes.count) == 1 + let elifNode = conditions?[1].nodes.first as? TextNode + try expect(elifNode?.text) == "some" + + try expect(conditions?[2].nodes.count) == 1 + let falseNode = conditions?[2].nodes.first as? TextNode + try expect(falseNode?.text) == "false" + } + + $0.it("can parse an if with elif block without else") { + let tokens: [Token] = [ + .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()) + let nodes = try parser.parse() + let node = nodes.first as? IfNode + + let conditions = node?.conditions + try expect(conditions?.count) == 2 + + try expect(conditions?[0].nodes.count) == 1 + let trueNode = conditions?[0].nodes.first as? TextNode + try expect(trueNode?.text) == "true" + + try expect(conditions?[1].nodes.count) == 1 + let elifNode = conditions?[1].nodes.first as? TextNode + try expect(elifNode?.text) == "some" + } + + $0.it("can parse an if with multiple elif block") { + let tokens: [Token] = [ + .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()) + let nodes = try parser.parse() + let node = nodes.first as? IfNode + + let conditions = node?.conditions + try expect(conditions?.count) == 4 + + try expect(conditions?[0].nodes.count) == 1 + let trueNode = conditions?[0].nodes.first as? TextNode + try expect(trueNode?.text) == "true" + + try expect(conditions?[1].nodes.count) == 1 + let elifNode = conditions?[1].nodes.first as? TextNode + try expect(elifNode?.text) == "some1" + + try expect(conditions?[2].nodes.count) == 1 + let elif2Node = conditions?[2].nodes.first as? TextNode + try expect(elif2Node?.text) == "some2" + + try expect(conditions?[3].nodes.count) == 1 + let falseNode = conditions?[3].nodes.first as? TextNode + try expect(falseNode?.text) == "false" + } + + + $0.it("can parse an if with complex expression") { + let tokens: [Token] = [ + .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()) + let nodes = try parser.parse() + try expect(nodes.first is IfNode).beTrue() + } + + $0.it("can parse an ifnot block") { + let tokens: [Token] = [ + .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()) + let nodes = try parser.parse() + let node = nodes.first as? IfNode + let conditions = node?.conditions + try expect(conditions?.count) == 2 + + try expect(conditions?[0].nodes.count) == 1 + let trueNode = conditions?[0].nodes.first as? TextNode + try expect(trueNode?.text) == "true" + + try expect(conditions?[1].nodes.count) == 1 + let falseNode = conditions?[1].nodes.first as? TextNode + try expect(falseNode?.text) == "false" + } + + $0.it("throws an error when parsing an if block without an endif") { + let tokens: [Token] = [.block(value: "if value", at: .unknown)] + + let parser = TokenParser(tokens: tokens, environment: Environment()) + let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first) + try expect(try parser.parse()).toThrow(error) + } + + $0.it("throws an error when parsing an ifnot without an endif") { + let tokens: [Token] = [.block(value: "ifnot value", at: .unknown)] + + let parser = TokenParser(tokens: tokens, environment: Environment()) + let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first) + try expect(try parser.parse()).toThrow(error) + } } - $0.it("can parse an if with else block") { - let tokens: [Token] = [ - .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) - ] + $0.describe("rendering") { + $0.it("renders a true expression") { + let node = IfNode(conditions: [ + IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "1")]), + IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]), + IfCondition(expression: nil, nodes: [TextNode(text: "3")]), + ]) - let parser = TokenParser(tokens: tokens, environment: Environment()) - let nodes = try parser.parse() - let node = nodes.first as? IfNode + try expect(try node.render(Context())) == "1" + } - let conditions = node?.conditions - try expect(conditions?.count) == 2 + $0.it("renders the first true expression") { + let node = IfNode(conditions: [ + IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]), + IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]), + IfCondition(expression: nil, nodes: [TextNode(text: "3")]), + ]) - try expect(conditions?[0].nodes.count) == 1 - let trueNode = conditions?[0].nodes.first as? TextNode - try expect(trueNode?.text) == "true" + try expect(try node.render(Context())) == "2" + } - try expect(conditions?[1].nodes.count) == 1 - let falseNode = conditions?[1].nodes.first as? TextNode - try expect(falseNode?.text) == "false" + $0.it("renders the empty expression when other conditions are falsy") { + let node = IfNode(conditions: [ + IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]), + IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]), + IfCondition(expression: nil, nodes: [TextNode(text: "3")]), + ]) + + try expect(try node.render(Context())) == "3" + } + + $0.it("renders empty when no truthy conditions") { + let node = IfNode(conditions: [ + IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]), + IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]), + ]) + + try expect(try node.render(Context())) == "" + } } - $0.it("can parse an if with elif block") { - let tokens: [Token] = [ - .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()) - let nodes = try parser.parse() - let node = nodes.first as? IfNode - - let conditions = node?.conditions - try expect(conditions?.count) == 3 - - try expect(conditions?[0].nodes.count) == 1 - let trueNode = conditions?[0].nodes.first as? TextNode - try expect(trueNode?.text) == "true" - - try expect(conditions?[1].nodes.count) == 1 - let elifNode = conditions?[1].nodes.first as? TextNode - try expect(elifNode?.text) == "some" - - try expect(conditions?[2].nodes.count) == 1 - let falseNode = conditions?[2].nodes.first as? TextNode - try expect(falseNode?.text) == "false" - } - - $0.it("can parse an if with elif block without else") { - let tokens: [Token] = [ - .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()) - let nodes = try parser.parse() - let node = nodes.first as? IfNode - - let conditions = node?.conditions - try expect(conditions?.count) == 2 - - try expect(conditions?[0].nodes.count) == 1 - let trueNode = conditions?[0].nodes.first as? TextNode - try expect(trueNode?.text) == "true" - - try expect(conditions?[1].nodes.count) == 1 - let elifNode = conditions?[1].nodes.first as? TextNode - try expect(elifNode?.text) == "some" - } - - $0.it("can parse an if with multiple elif block") { - let tokens: [Token] = [ - .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()) - let nodes = try parser.parse() - let node = nodes.first as? IfNode - - let conditions = node?.conditions - try expect(conditions?.count) == 4 - - try expect(conditions?[0].nodes.count) == 1 - let trueNode = conditions?[0].nodes.first as? TextNode - try expect(trueNode?.text) == "true" - - try expect(conditions?[1].nodes.count) == 1 - let elifNode = conditions?[1].nodes.first as? TextNode - try expect(elifNode?.text) == "some1" - - try expect(conditions?[2].nodes.count) == 1 - let elif2Node = conditions?[2].nodes.first as? TextNode - try expect(elif2Node?.text) == "some2" - - try expect(conditions?[3].nodes.count) == 1 - let falseNode = conditions?[3].nodes.first as? TextNode - try expect(falseNode?.text) == "false" - } - - - $0.it("can parse an if with complex expression") { - let tokens: [Token] = [ - .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()) - let nodes = try parser.parse() - try expect(nodes.first is IfNode).beTrue() - } - - $0.it("can parse an ifnot block") { - let tokens: [Token] = [ - .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()) - let nodes = try parser.parse() - let node = nodes.first as? IfNode - let conditions = node?.conditions - try expect(conditions?.count) == 2 - - try expect(conditions?[0].nodes.count) == 1 - let trueNode = conditions?[0].nodes.first as? TextNode - try expect(trueNode?.text) == "true" - - try expect(conditions?[1].nodes.count) == 1 - let falseNode = conditions?[1].nodes.first as? TextNode - try expect(falseNode?.text) == "false" - } - - $0.it("throws an error when parsing an if block without an endif") { - let tokens: [Token] = [.block(value: "if value", at: .unknown)] - - let parser = TokenParser(tokens: tokens, environment: Environment()) - let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first) - try expect(try parser.parse()).toThrow(error) - } - - $0.it("throws an error when parsing an ifnot without an endif") { - let tokens: [Token] = [.block(value: "ifnot value", at: .unknown)] - - let parser = TokenParser(tokens: tokens, environment: Environment()) - let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first) - try expect(try parser.parse()).toThrow(error) - } - } - - $0.describe("rendering") { - $0.it("renders a true expression") { - let node = IfNode(conditions: [ - IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "1")]), - IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]), - IfCondition(expression: nil, nodes: [TextNode(text: "3")]), - ]) - - try expect(try node.render(Context())) == "1" - } - - $0.it("renders the first true expression") { - let node = IfNode(conditions: [ - IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]), - IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]), - IfCondition(expression: nil, nodes: [TextNode(text: "3")]), - ]) - - try expect(try node.render(Context())) == "2" - } - - $0.it("renders the empty expression when other conditions are falsy") { - let node = IfNode(conditions: [ - IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]), - IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]), - IfCondition(expression: nil, nodes: [TextNode(text: "3")]), - ]) - - try expect(try node.render(Context())) == "3" - } - - $0.it("renders empty when no truthy conditions") { - let node = IfNode(conditions: [ - IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]), - IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]), - ]) - - try expect(try node.render(Context())) == "" - } - } - - $0.it("supports variable filters in the if expression") { + $0.it("supports variable filters in the if expression") { let tokens: [Token] = [ .block(value: "if value|uppercase == \"TEST\"", at: .unknown), .text(value: "true", at: .unknown), @@ -248,40 +249,41 @@ func testIfNode() { let result = try renderNodes(nodes, Context(dictionary: ["value": "test"])) try expect(result) == "true" - } - - $0.it("evaluates nil properties as false") { - let tokens: [Token] = [ - .block(value: "if instance.value", at: .unknown), - .text(value: "true", at: .unknown), - .block(value: "endif", at: .unknown) - ] - - let parser = TokenParser(tokens: tokens, environment: Environment()) - let nodes = try parser.parse() - - struct SomeType { - let value: String? = nil } - let result = try renderNodes(nodes, Context(dictionary: ["instance": SomeType()])) - try expect(result) == "" + + $0.it("evaluates nil properties as false") { + let tokens: [Token] = [ + .block(value: "if instance.value", at: .unknown), + .text(value: "true", at: .unknown), + .block(value: "endif", at: .unknown) + ] + + let parser = TokenParser(tokens: tokens, environment: Environment()) + let nodes = try parser.parse() + + struct SomeType { + let value: String? = nil + } + let result = try renderNodes(nodes, Context(dictionary: ["instance": SomeType()])) + try expect(result) == "" + } + + $0.it("supports closed range variables") { + let tokens: [Token] = [ + .block(value: "if value in 1...3", 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()) + let nodes = try parser.parse() + + try expect(renderNodes(nodes, Context(dictionary: ["value": 3]))) == "true" + try expect(renderNodes(nodes, Context(dictionary: ["value": 4]))) == "false" + } + } - - $0.it("supports closed range variables") { - let tokens: [Token] = [ - .block(value: "if value in 1...3", 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()) - let nodes = try parser.parse() - - try expect(renderNodes(nodes, Context(dictionary: ["value": 3]))) == "true" - try expect(renderNodes(nodes, Context(dictionary: ["value": 4]))) == "false" - } - } } diff --git a/Tests/StencilTests/IncludeSpec.swift b/Tests/StencilTests/IncludeSpec.swift index cf51546..1b587ae 100644 --- a/Tests/StencilTests/IncludeSpec.swift +++ b/Tests/StencilTests/IncludeSpec.swift @@ -1,69 +1,71 @@ +import XCTest import Spectre @testable import Stencil import PathKit +class IncludeTests: XCTestCase { + func testInclude() { + describe("Include") { + let path = Path(#file) + ".." + "fixtures" + let loader = FileSystemLoader(paths: [path]) + let environment = Environment(loader: loader) -func testInclude() { - describe("Include") { - let path = Path(#file) + ".." + "fixtures" - let loader = FileSystemLoader(paths: [path]) - let environment = Environment(loader: loader) + $0.describe("parsing") { + $0.it("throws an error when no template is given") { + let tokens: [Token] = [ .block(value: "include", at: .unknown) ] + let parser = TokenParser(tokens: tokens, environment: Environment()) - $0.describe("parsing") { - $0.it("throws an error when no template is given") { - let tokens: [Token] = [ .block(value: "include", at: .unknown) ] - let parser = TokenParser(tokens: tokens, environment: Environment()) + let error = TemplateSyntaxError(reason: "'include' tag requires one argument, the template file to be included. A second optional argument can be used to specify the context that will be passed to the included file", token: tokens.first) + try expect(try parser.parse()).toThrow(error) + } - let error = TemplateSyntaxError(reason: "'include' tag requires one argument, the template file to be included. A second optional argument can be used to specify the context that will be passed to the included file", token: tokens.first) - try expect(try parser.parse()).toThrow(error) - } + $0.it("can parse a valid include block") { + let tokens: [Token] = [ .block(value: "include \"test.html\"", at: .unknown) ] + let parser = TokenParser(tokens: tokens, environment: Environment()) - $0.it("can parse a valid include block") { - let tokens: [Token] = [ .block(value: "include \"test.html\"", at: .unknown) ] - let parser = TokenParser(tokens: tokens, environment: Environment()) - - let nodes = try parser.parse() - let node = nodes.first as? IncludeNode - try expect(nodes.count) == 1 - try expect(node?.templateName) == Variable("\"test.html\"") - } - } - - $0.describe("rendering") { - $0.it("throws an error when rendering without a loader") { - let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown)) - - do { - _ = try node.render(Context()) - } catch { - try expect("\(error)") == "Template named `test.html` does not exist. No loaders found" + let nodes = try parser.parse() + let node = nodes.first as? IncludeNode + try expect(nodes.count) == 1 + try expect(node?.templateName) == Variable("\"test.html\"") } } - $0.it("throws an error when it cannot find the included template") { - let node = IncludeNode(templateName: Variable("\"unknown.html\""), token: .block(value: "", at: .unknown)) + $0.describe("rendering") { + $0.it("throws an error when rendering without a loader") { + let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown)) - do { - _ = try node.render(Context(environment: environment)) - } catch { - try expect("\(error)".hasPrefix("Template named `unknown.html` does not exist in loader")).to.beTrue() + do { + _ = try node.render(Context()) + } catch { + try expect("\(error)") == "Template named `test.html` does not exist. No loaders found" + } } - } - $0.it("successfully renders a found included template") { - let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown)) - let context = Context(dictionary: ["target": "World"], environment: environment) - let value = try node.render(context) - try expect(value) == "Hello World!" - } + $0.it("throws an error when it cannot find the included template") { + let node = IncludeNode(templateName: Variable("\"unknown.html\""), token: .block(value: "", at: .unknown)) - $0.it("successfully passes context") { - let template = Template(templateString: """ + do { + _ = try node.render(Context(environment: environment)) + } catch { + try expect("\(error)".hasPrefix("Template named `unknown.html` does not exist in loader")).to.beTrue() + } + } + + $0.it("successfully renders a found included template") { + let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown)) + let context = Context(dictionary: ["target": "World"], environment: environment) + let value = try node.render(context) + try expect(value) == "Hello World!" + } + + $0.it("successfully passes context") { + let template = Template(templateString: """ {% include "test.html" child %} """) - let context = Context(dictionary: ["child": ["target": "World"]], environment: environment) - let value = try template.render(context) - try expect(value) == "Hello World!" + let context = Context(dictionary: ["child": ["target": "World"]], environment: environment) + let value = try template.render(context) + try expect(value) == "Hello World!" + } } } } diff --git a/Tests/StencilTests/InheritenceSpec.swift b/Tests/StencilTests/InheritenceSpec.swift index 68ef597..d859fb4 100644 --- a/Tests/StencilTests/InheritenceSpec.swift +++ b/Tests/StencilTests/InheritenceSpec.swift @@ -1,36 +1,38 @@ +import XCTest import Spectre import Stencil import PathKit +class InheritenceTests: XCTestCase { + func testInheritence() { + describe("Inheritence") { + let path = Path(#file) + ".." + "fixtures" + let loader = FileSystemLoader(paths: [path]) + let environment = Environment(loader: loader) -func testInheritence() { - describe("Inheritence") { - let path = Path(#file) + ".." + "fixtures" - let loader = FileSystemLoader(paths: [path]) - let environment = Environment(loader: loader) - - $0.it("can inherit from another template") { - let template = try environment.loadTemplate(name: "child.html") - try expect(try template.render()) == """ + $0.it("can inherit from another template") { + let template = try environment.loadTemplate(name: "child.html") + try expect(try template.render()) == """ Super_Header Child_Header Child_Body """ - } + } - $0.it("can inherit from another template inheriting from another template") { - let template = try environment.loadTemplate(name: "child-child.html") - try expect(try template.render()) == """ + $0.it("can inherit from another template inheriting from another template") { + let template = try environment.loadTemplate(name: "child-child.html") + try expect(try template.render()) == """ Super_Header Child_Header Child_Child_Header Child_Body """ - } + } - $0.it("can inherit from a template that calls a super block") { - let template = try environment.loadTemplate(name: "child-super.html") - try expect(try template.render()) == """ + $0.it("can inherit from a template that calls a super block") { + let template = try environment.loadTemplate(name: "child-super.html") + try expect(try template.render()) == """ Header Child_Body """ + } } } } diff --git a/Tests/StencilTests/LexerSpec.swift b/Tests/StencilTests/LexerSpec.swift index fd3493d..6f49a4c 100644 --- a/Tests/StencilTests/LexerSpec.swift +++ b/Tests/StencilTests/LexerSpec.swift @@ -1,75 +1,76 @@ +import XCTest import Spectre @testable import Stencil +class LexerTests: XCTestCase { + func testLexer() { + describe("Lexer") { + $0.it("can tokenize text") { + let lexer = Lexer(templateString: "Hello World") + let tokens = lexer.tokenize() -func testLexer() { - describe("Lexer") { - $0.it("can tokenize text") { - let lexer = Lexer(templateString: "Hello World") - let tokens = lexer.tokenize() + try expect(tokens.count) == 1 + try expect(tokens.first) == .text(value: "Hello World", at: SourceMap(location: ("Hello World", 1, 0))) + } - try expect(tokens.count) == 1 - try expect(tokens.first) == .text(value: "Hello World", at: SourceMap(location: ("Hello World", 1, 0))) - } + $0.it("can tokenize a comment") { + let lexer = Lexer(templateString: "{# Comment #}") + let tokens = lexer.tokenize() - $0.it("can tokenize a comment") { - let lexer = Lexer(templateString: "{# Comment #}") - let tokens = lexer.tokenize() + try expect(tokens.count) == 1 + try expect(tokens.first) == .comment(value: "Comment", at: SourceMap(location: ("{# Comment #}", 1, 3))) + } - try expect(tokens.count) == 1 - try expect(tokens.first) == .comment(value: "Comment", at: SourceMap(location: ("{# Comment #}", 1, 3))) - } + $0.it("can tokenize a variable") { + let lexer = Lexer(templateString: "{{ Variable }}") + let tokens = lexer.tokenize() - $0.it("can tokenize a variable") { - let lexer = Lexer(templateString: "{{ Variable }}") - let tokens = lexer.tokenize() + try expect(tokens.count) == 1 + try expect(tokens.first) == .variable(value: "Variable", at: SourceMap(location: ("{{ Variable }}", 1, 3))) + } - try expect(tokens.count) == 1 - try expect(tokens.first) == .variable(value: "Variable", at: SourceMap(location: ("{{ Variable }}", 1, 3))) - } + $0.it("can tokenize unclosed tag by ignoring it") { + let templateString = "{{ thing" + let lexer = Lexer(templateString: templateString) + let tokens = lexer.tokenize() - $0.it("can tokenize unclosed tag by ignoring it") { - let templateString = "{{ thing" - let lexer = Lexer(templateString: templateString) - let tokens = lexer.tokenize() + try expect(tokens.count) == 1 + try expect(tokens.first) == .text(value: "", at: SourceMap(location: ("{{ thing", 1, 0))) + } - try expect(tokens.count) == 1 - try expect(tokens.first) == .text(value: "", at: SourceMap(location: ("{{ thing", 1, 0))) - } + $0.it("can tokenize a mixture of content") { + let templateString = "My name is {{ myname }}." + let lexer = Lexer(templateString: templateString) + let tokens = lexer.tokenize() - $0.it("can tokenize a mixture of content") { - let templateString = "My name is {{ myname }}." - let lexer = Lexer(templateString: templateString) - let tokens = lexer.tokenize() + try expect(tokens.count) == 3 + try expect(tokens[0]) == Token.text(value: "My name is ", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "My name is ")!))) + try expect(tokens[1]) == Token.variable(value: "myname", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "myname")!))) + try expect(tokens[2]) == Token.text(value: ".", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: ".")!))) + } - try expect(tokens.count) == 3 - try expect(tokens[0]) == Token.text(value: "My name is ", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "My name is ")!))) - try expect(tokens[1]) == Token.variable(value: "myname", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "myname")!))) - try expect(tokens[2]) == Token.text(value: ".", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: ".")!))) - } + $0.it("can tokenize two variables without being greedy") { + let templateString = "{{ thing }}{{ name }}" + let lexer = Lexer(templateString: templateString) + let tokens = lexer.tokenize() - $0.it("can tokenize two variables without being greedy") { - 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", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "thing")!))) + try expect(tokens[1]) == Token.variable(value: "name", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "name")!))) + } - try expect(tokens.count) == 2 - try expect(tokens[0]) == Token.variable(value: "thing", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "thing")!))) - try expect(tokens[1]) == Token.variable(value: "name", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "name")!))) - } + $0.it("can tokenize an unclosed block") { + let lexer = Lexer(templateString: "{%}") + let _ = lexer.tokenize() + } - $0.it("can tokenize an unclosed block") { - let lexer = Lexer(templateString: "{%}") - let _ = lexer.tokenize() - } + $0.it("can tokenize an empty variable") { + let lexer = Lexer(templateString: "{{}}") + let _ = lexer.tokenize() + } - $0.it("can tokenize an empty variable") { - let lexer = Lexer(templateString: "{{}}") - let _ = lexer.tokenize() - } - - $0.it("can tokenize with new lines") { - let templateString = """ + $0.it("can tokenize with new lines") { + let templateString = """ My name is {% if name and @@ -80,16 +81,17 @@ func testLexer() { endif %}. """ - let lexer = Lexer(templateString: templateString) + let lexer = Lexer(templateString: templateString) - let tokens = lexer.tokenize() + let tokens = lexer.tokenize() - try expect(tokens.count) == 5 - try expect(tokens[0]) == Token.text(value: "My name is ", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "My name is")!))) - try expect(tokens[1]) == Token.block(value: "if name and name", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "{%")!))) - try expect(tokens[2]) == Token.variable(value: "name", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "name", options: [.backwards])!))) - try expect(tokens[3]) == Token.block(value: "endif", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "endif")!))) - try expect(tokens[4]) == Token.text(value: ".", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: ".")!))) + try expect(tokens.count) == 5 + try expect(tokens[0]) == Token.text(value: "My name is ", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "My name is")!))) + try expect(tokens[1]) == Token.block(value: "if name and name", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "{%")!))) + try expect(tokens[2]) == Token.variable(value: "name", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "name", options: [.backwards])!))) + try expect(tokens[3]) == Token.block(value: "endif", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "endif")!))) + try expect(tokens[4]) == Token.text(value: ".", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: ".")!))) + } } } } diff --git a/Tests/StencilTests/LoaderSpec.swift b/Tests/StencilTests/LoaderSpec.swift index 6e207d6..91d541f 100644 --- a/Tests/StencilTests/LoaderSpec.swift +++ b/Tests/StencilTests/LoaderSpec.swift @@ -1,55 +1,57 @@ +import XCTest import Spectre import Stencil import PathKit +class TemplateLoaderTests: XCTestCase { + func testTemplateLoader() { + describe("FileSystemLoader") { + let path = Path(#file) + ".." + "fixtures" + let loader = FileSystemLoader(paths: [path]) + let environment = Environment(loader: loader) -func testTemplateLoader() { - describe("FileSystemLoader") { - let path = Path(#file) + ".." + "fixtures" - let loader = FileSystemLoader(paths: [path]) - let environment = Environment(loader: loader) + $0.it("errors when a template cannot be found") { + try expect(try environment.loadTemplate(name: "unknown.html")).toThrow() + } - $0.it("errors when a template cannot be found") { - try expect(try environment.loadTemplate(name: "unknown.html")).toThrow() + $0.it("errors when an array of templates cannot be found") { + try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow() + } + + $0.it("can load a template from a file") { + _ = try environment.loadTemplate(name: "test.html") + } + + $0.it("errors when loading absolute file outside of the selected path") { + try expect(try environment.loadTemplate(name: "/etc/hosts")).toThrow() + } + + $0.it("errors when loading relative file outside of the selected path") { + try expect(try environment.loadTemplate(name: "../LoaderSpec.swift")).toThrow() + } } - $0.it("errors when an array of templates cannot be found") { - try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow() - } - - $0.it("can load a template from a file") { - _ = try environment.loadTemplate(name: "test.html") - } - - $0.it("errors when loading absolute file outside of the selected path") { - try expect(try environment.loadTemplate(name: "/etc/hosts")).toThrow() - } - - $0.it("errors when loading relative file outside of the selected path") { - try expect(try environment.loadTemplate(name: "../LoaderSpec.swift")).toThrow() - } - } - - describe("DictionaryLoader") { - let loader = DictionaryLoader(templates: [ + describe("DictionaryLoader") { + let loader = DictionaryLoader(templates: [ "index.html": "Hello World" - ]) - let environment = Environment(loader: loader) + ]) + let environment = Environment(loader: loader) - $0.it("errors when a template cannot be found") { - try expect(try environment.loadTemplate(name: "unknown.html")).toThrow() - } + $0.it("errors when a template cannot be found") { + try expect(try environment.loadTemplate(name: "unknown.html")).toThrow() + } - $0.it("errors when an array of templates cannot be found") { - try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow() - } + $0.it("errors when an array of templates cannot be found") { + try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow() + } - $0.it("can load a template from a known templates") { - _ = try environment.loadTemplate(name: "index.html") - } + $0.it("can load a template from a known templates") { + _ = try environment.loadTemplate(name: "index.html") + } - $0.it("can load a known template from a collection of templates") { - _ = try environment.loadTemplate(names: ["unknown.html", "index.html"]) + $0.it("can load a known template from a collection of templates") { + _ = try environment.loadTemplate(names: ["unknown.html", "index.html"]) + } } } } diff --git a/Tests/StencilTests/NodeSpec.swift b/Tests/StencilTests/NodeSpec.swift index 1adfa26..1987282 100644 --- a/Tests/StencilTests/NodeSpec.swift +++ b/Tests/StencilTests/NodeSpec.swift @@ -1,7 +1,7 @@ +import XCTest import Spectre @testable import Stencil - class ErrorNode : NodeType { let token: Token? init(token: Token? = nil) { @@ -13,52 +13,53 @@ class ErrorNode : NodeType { } } +class NodeTests: XCTestCase { + func testNode() { + describe("Node") { + let context = Context(dictionary: [ + "name": "Kyle", + "age": 27, + "items": [1, 2, 3], + ]) -func testNode() { - describe("Node") { - let context = Context(dictionary: [ - "name": "Kyle", - "age": 27, - "items": [1, 2, 3], - ]) - - $0.describe("TextNode") { - $0.it("renders the given text") { - let node = TextNode(text: "Hello World") - try expect(try node.render(context)) == "Hello World" - } - } - - $0.describe("VariableNode") { - $0.it("resolves and renders the variable") { - let node = VariableNode(variable: Variable("name")) - try expect(try node.render(context)) == "Kyle" + $0.describe("TextNode") { + $0.it("renders the given text") { + let node = TextNode(text: "Hello World") + try expect(try node.render(context)) == "Hello World" + } } - $0.it("resolves and renders a non string variable") { - let node = VariableNode(variable: Variable("age")) - try expect(try node.render(context)) == "27" - } - } + $0.describe("VariableNode") { + $0.it("resolves and renders the variable") { + let node = VariableNode(variable: Variable("name")) + try expect(try node.render(context)) == "Kyle" + } - $0.describe("rendering nodes") { - $0.it("renders the nodes") { - let nodes: [NodeType] = [ - TextNode(text:"Hello "), - VariableNode(variable: "name"), - ] - - try expect(try renderNodes(nodes, context)) == "Hello Kyle" + $0.it("resolves and renders a non string variable") { + let node = VariableNode(variable: Variable("age")) + try expect(try node.render(context)) == "27" + } } - $0.it("correctly throws a nodes failure") { - let nodes: [NodeType] = [ - TextNode(text:"Hello "), - VariableNode(variable: "name"), - ErrorNode(), - ] + $0.describe("rendering nodes") { + $0.it("renders the nodes") { + let nodes: [NodeType] = [ + TextNode(text:"Hello "), + VariableNode(variable: "name"), + ] - try expect(try renderNodes(nodes, context)).toThrow(TemplateSyntaxError("Custom Error")) + try expect(try renderNodes(nodes, context)) == "Hello Kyle" + } + + $0.it("correctly throws a nodes failure") { + let nodes: [NodeType] = [ + TextNode(text:"Hello "), + VariableNode(variable: "name"), + ErrorNode(), + ] + + try expect(try renderNodes(nodes, context)).toThrow(TemplateSyntaxError("Custom Error")) + } } } } diff --git a/Tests/StencilTests/NowNodeSpec.swift b/Tests/StencilTests/NowNodeSpec.swift index 33a0d3f..e9a2a62 100644 --- a/Tests/StencilTests/NowNodeSpec.swift +++ b/Tests/StencilTests/NowNodeSpec.swift @@ -1,43 +1,46 @@ +import XCTest import Foundation import Spectre @testable import Stencil -func testNowNode() { -#if !os(Linux) - describe("NowNode") { - $0.describe("parsing") { - $0.it("parses default format without any now arguments") { - let tokens: [Token] = [ .block(value: "now", at: .unknown) ] - let parser = TokenParser(tokens: tokens, environment: Environment()) +class NowNodeTests: XCTestCase { + func testNowNode() { + #if !os(Linux) + describe("NowNode") { + $0.describe("parsing") { + $0.it("parses default format without any now arguments") { + let tokens: [Token] = [ .block(value: "now", at: .unknown) ] + let parser = TokenParser(tokens: tokens, environment: Environment()) - let nodes = try parser.parse() - let node = nodes.first as? NowNode - try expect(nodes.count) == 1 - try expect(node?.format.variable) == "\"yyyy-MM-dd 'at' HH:mm\"" + let nodes = try parser.parse() + let node = nodes.first as? NowNode + try expect(nodes.count) == 1 + try expect(node?.format.variable) == "\"yyyy-MM-dd 'at' HH:mm\"" + } + + $0.it("parses now with a format") { + 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 + try expect(nodes.count) == 1 + try expect(node?.format.variable) == "\"HH:mm\"" + } } - $0.it("parses now with a format") { - 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 - try expect(nodes.count) == 1 - try expect(node?.format.variable) == "\"HH:mm\"" - } - } - - $0.describe("rendering") { - $0.it("renders the date") { - let node = NowNode(format: Variable("\"yyyy-MM-dd\"")) - - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd" - let date = formatter.string(from: NSDate() as Date) - - try expect(try node.render(Context())) == date + $0.describe("rendering") { + $0.it("renders the date") { + let node = NowNode(format: Variable("\"yyyy-MM-dd\"")) + + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + let date = formatter.string(from: NSDate() as Date) + + try expect(try node.render(Context())) == date + } } } + #endif } -#endif } diff --git a/Tests/StencilTests/ParserSpec.swift b/Tests/StencilTests/ParserSpec.swift index 25c485f..e8650d3 100644 --- a/Tests/StencilTests/ParserSpec.swift +++ b/Tests/StencilTests/ParserSpec.swift @@ -1,61 +1,63 @@ +import XCTest import Spectre @testable import Stencil +class TokenParserTests: XCTestCase { + func testTokenParser() { + describe("TokenParser") { + $0.it("can parse a text token") { + let parser = TokenParser(tokens: [ + .text(value: "Hello World", at: .unknown) + ], environment: Environment()) -func testTokenParser() { - describe("TokenParser") { - $0.it("can parse a text token") { - let parser = TokenParser(tokens: [ - .text(value: "Hello World", at: .unknown) - ], environment: Environment()) + let nodes = try parser.parse() + let node = nodes.first as? TextNode - let nodes = try parser.parse() - let node = nodes.first as? TextNode - - try expect(nodes.count) == 1 - try expect(node?.text) == "Hello World" - } - - $0.it("can parse a variable token") { - let parser = TokenParser(tokens: [ - .variable(value: "'name'", at: .unknown) - ], environment: Environment()) - - let nodes = try parser.parse() - let node = nodes.first as? VariableNode - try expect(nodes.count) == 1 - let result = try node?.render(Context()) - try expect(result) == "name" - } - - $0.it("can parse a comment token") { - let parser = TokenParser(tokens: [ - .comment(value: "Secret stuff!", at: .unknown) - ], environment: Environment()) - - let nodes = try parser.parse() - try expect(nodes.count) == 0 - } - - $0.it("can parse a tag token") { - let simpleExtension = Extension() - simpleExtension.registerSimpleTag("known") { _ in - return "" + try expect(nodes.count) == 1 + try expect(node?.text) == "Hello World" } - let parser = TokenParser(tokens: [ - .block(value: "known", at: .unknown), - ], environment: Environment(extensions: [simpleExtension])) + $0.it("can parse a variable token") { + let parser = TokenParser(tokens: [ + .variable(value: "'name'", at: .unknown) + ], environment: Environment()) - let nodes = try parser.parse() - try expect(nodes.count) == 1 - } + let nodes = try parser.parse() + let node = nodes.first as? VariableNode + try expect(nodes.count) == 1 + let result = try node?.render(Context()) + try expect(result) == "name" + } - $0.it("errors when parsing an unknown tag") { - let tokens: [Token] = [.block(value: "unknown", at: .unknown)] - let parser = TokenParser(tokens: tokens, environment: Environment()) + $0.it("can parse a comment token") { + let parser = TokenParser(tokens: [ + .comment(value: "Secret stuff!", at: .unknown) + ], environment: Environment()) - try expect(try parser.parse()).toThrow(TemplateSyntaxError(reason: "Unknown template tag 'unknown'", token: tokens.first)) + let nodes = try parser.parse() + try expect(nodes.count) == 0 + } + + $0.it("can parse a tag token") { + let simpleExtension = Extension() + simpleExtension.registerSimpleTag("known") { _ in + return "" + } + + let parser = TokenParser(tokens: [ + .block(value: "known", at: .unknown), + ], environment: Environment(extensions: [simpleExtension])) + + let nodes = try parser.parse() + try expect(nodes.count) == 1 + } + + $0.it("errors when parsing an unknown tag") { + let tokens: [Token] = [.block(value: "unknown", at: .unknown)] + let parser = TokenParser(tokens: tokens, environment: Environment()) + + try expect(try parser.parse()).toThrow(TemplateSyntaxError(reason: "Unknown template tag 'unknown'", token: tokens.first)) + } } } } diff --git a/Tests/StencilTests/StencilSpec.swift b/Tests/StencilTests/StencilSpec.swift index 137f5be..5ec6094 100644 --- a/Tests/StencilTests/StencilSpec.swift +++ b/Tests/StencilTests/StencilSpec.swift @@ -1,7 +1,7 @@ +import XCTest import Spectre import Stencil - fileprivate struct CustomNode : NodeType { let token: Token? func render(_ context:Context) throws -> String { @@ -9,64 +9,64 @@ fileprivate struct CustomNode : NodeType { } } - fileprivate struct Article { let title: String let author: String } +class StencilTests: XCTestCase { + func testStencil() { + describe("Stencil") { + let exampleExtension = Extension() -func testStencil() { - describe("Stencil") { - let exampleExtension = Extension() + exampleExtension.registerSimpleTag("simpletag") { context in + return "Hello World" + } - exampleExtension.registerSimpleTag("simpletag") { context in - return "Hello World" - } + exampleExtension.registerTag("customtag") { parser, token in + return CustomNode(token: token) + } - exampleExtension.registerTag("customtag") { parser, token in - return CustomNode(token: token) - } + let environment = Environment(extensions: [exampleExtension]) - let environment = Environment(extensions: [exampleExtension]) + $0.it("can render the README example") { - $0.it("can render the README example") { + let templateString = """ + There are {{ articles.count }} articles. - let templateString = """ - There are {{ articles.count }} articles. + {% for article in articles %}\ + - {{ article.title }} by {{ article.author }}. + {% endfor %} + """ - {% for article in articles %}\ - - {{ article.title }} by {{ article.author }}. - {% endfor %} - """ - - let context = [ - "articles": [ - Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"), - Article(title: "Memory Management with ARC", author: "Kyle Fuller"), + let context = [ + "articles": [ + Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"), + Article(title: "Memory Management with ARC", author: "Kyle Fuller"), + ] ] - ] - let template = Template(templateString: templateString) - let result = try template.render(context) + let template = Template(templateString: templateString) + let result = try template.render(context) - try expect(result) == """ - There are 2 articles. + try expect(result) == """ + There are 2 articles. - - Migrating from OCUnit to XCTest by Kyle Fuller. - - Memory Management with ARC by Kyle Fuller. + - Migrating from OCUnit to XCTest by Kyle Fuller. + - Memory Management with ARC by Kyle Fuller. - """ - } + """ + } - $0.it("can render a custom template tag") { - let result = try environment.renderTemplate(string: "{% customtag %}") - try expect(result) == "Hello World" - } + $0.it("can render a custom template tag") { + let result = try environment.renderTemplate(string: "{% customtag %}") + try expect(result) == "Hello World" + } - $0.it("can render a simple custom tag") { - let result = try environment.renderTemplate(string: "{% simpletag %}") - try expect(result) == "Hello World" + $0.it("can render a simple custom tag") { + let result = try environment.renderTemplate(string: "{% simpletag %}") + try expect(result) == "Hello World" + } } } } diff --git a/Tests/StencilTests/TemplateSpec.swift b/Tests/StencilTests/TemplateSpec.swift index fee0c5e..3d3001b 100644 --- a/Tests/StencilTests/TemplateSpec.swift +++ b/Tests/StencilTests/TemplateSpec.swift @@ -1,20 +1,22 @@ +import XCTest import Spectre @testable import Stencil - -func testTemplate() { - describe("Template") { - $0.it("can render a template from a string") { - let template = Template(templateString: "Hello World") - let result = try template.render([ "name": "Kyle" ]) - try expect(result) == "Hello World" - } - - $0.it("can render a template from a string literal") { +class TemplateTests: XCTestCase { + func testTemplate() { + describe("Template") { + $0.it("can render a template from a string") { + let template = Template(templateString: "Hello World") + let result = try template.render([ "name": "Kyle" ]) + try expect(result) == "Hello World" + } + + $0.it("can render a template from a string literal") { let template: Template = "Hello World" let result = try template.render([ "name": "Kyle" ]) try expect(result) == "Hello World" + } + } - } } diff --git a/Tests/StencilTests/TokenSpec.swift b/Tests/StencilTests/TokenSpec.swift index dd73298..0722b6d 100644 --- a/Tests/StencilTests/TokenSpec.swift +++ b/Tests/StencilTests/TokenSpec.swift @@ -1,34 +1,36 @@ +import XCTest import Spectre @testable import Stencil +class TokenTests: XCTestCase { + func testToken() { + describe("Token") { + $0.it("can split the contents into components") { + let token = Token.text(value: "hello world", at: .unknown) + let components = token.components() -func testToken() { - describe("Token") { - $0.it("can split the contents into components") { - let token = Token.text(value: "hello world", at: .unknown) - let components = token.components() + try expect(components.count) == 2 + try expect(components[0]) == "hello" + try expect(components[1]) == "world" + } - try expect(components.count) == 2 - try expect(components[0]) == "hello" - try expect(components[1]) == "world" - } + $0.it("can split the contents into components with single quoted strings") { + let token = Token.text(value: "hello 'kyle fuller'", at: .unknown) + let components = token.components() - $0.it("can split the contents into components with single quoted strings") { - let token = Token.text(value: "hello 'kyle fuller'", at: .unknown) - let components = token.components() + try expect(components.count) == 2 + try expect(components[0]) == "hello" + try expect(components[1]) == "'kyle fuller'" + } - try expect(components.count) == 2 - try expect(components[0]) == "hello" - try expect(components[1]) == "'kyle fuller'" - } + $0.it("can split the contents into components with double quoted strings") { + let token = Token.text(value: "hello \"kyle fuller\"", at: .unknown) + let components = token.components() - $0.it("can split the contents into components with double quoted strings") { - let token = Token.text(value: "hello \"kyle fuller\"", at: .unknown) - let components = token.components() - - try expect(components.count) == 2 - try expect(components[0]) == "hello" - try expect(components[1]) == "\"kyle fuller\"" + try expect(components.count) == 2 + try expect(components[0]) == "hello" + try expect(components[1]) == "\"kyle fuller\"" + } } } } diff --git a/Tests/StencilTests/VariableSpec.swift b/Tests/StencilTests/VariableSpec.swift index 2d052bb..ef2632d 100644 --- a/Tests/StencilTests/VariableSpec.swift +++ b/Tests/StencilTests/VariableSpec.swift @@ -1,3 +1,4 @@ +import XCTest import Foundation import Spectre @testable import Stencil @@ -29,381 +30,383 @@ fileprivate class Blog: WebSite { let featuring: Article? = Article(author: Person(name: "Jhon")) } -func testVariable() { - describe("Variable") { - let context = Context(dictionary: [ - "name": "Kyle", - "contacts": ["Katie", "Carlton"], - "profiles": [ - "github": "kylef", - ], - "counter": [ - "count": "kylef", +class VariableTests: XCTestCase { + func testVariable() { + describe("Variable") { + let context = Context(dictionary: [ + "name": "Kyle", + "contacts": ["Katie", "Carlton"], + "profiles": [ + "github": "kylef", ], - "article": Article(author: Person(name: "Kyle")), - "tuple": (one: 1, two: 2) - ]) + "counter": [ + "count": "kylef", + ], + "article": Article(author: Person(name: "Kyle")), + "tuple": (one: 1, two: 2) + ]) -#if os(OSX) - context["object"] = Object() -#endif - context["blog"] = Blog() + #if os(OSX) + context["object"] = Object() + #endif + context["blog"] = Blog() - $0.it("can resolve a string literal with double quotes") { - let variable = Variable("\"name\"") - let result = try variable.resolve(context) as? String - try expect(result) == "name" - } - - $0.it("can resolve a string literal with single quotes") { - let variable = Variable("'name'") - let result = try variable.resolve(context) as? String - try expect(result) == "name" - } - - $0.it("can resolve an integer literal") { - let variable = Variable("5") - let result = try variable.resolve(context) as? Int - try expect(result) == 5 - } - - $0.it("can resolve an float literal") { - let variable = Variable("3.14") - let result = try variable.resolve(context) as? Number - try expect(result) == 3.14 - } - - $0.it("can resolve boolean literal") { - try expect(Variable("true").resolve(context) as? Bool) == true - try expect(Variable("false").resolve(context) as? Bool) == false - try expect(Variable("0").resolve(context) as? Int) == 0 - try expect(Variable("1").resolve(context) as? Int) == 1 - } - - $0.it("can resolve a string variable") { - let variable = Variable("name") - let result = try variable.resolve(context) as? String - try expect(result) == "Kyle" - } - - $0.context("given string") { - $0.it("can resolve an item via it's index") { - let variable = Variable("name.0") - let result = try variable.resolve(context) as? Character - try expect(result) == "K" - - let variable1 = Variable("name.1") - let result1 = try variable1.resolve(context) as? Character - try expect(result1) == "y" + $0.it("can resolve a string literal with double quotes") { + let variable = Variable("\"name\"") + let result = try variable.resolve(context) as? String + try expect(result) == "name" } - $0.it("can resolve an item via unknown index") { - let variable = Variable("name.5") - let result = try variable.resolve(context) as? Character - try expect(result).to.beNil() - - let variable1 = Variable("name.-5") - let result1 = try variable1.resolve(context) as? Character - try expect(result1).to.beNil() + $0.it("can resolve a string literal with single quotes") { + let variable = Variable("'name'") + let result = try variable.resolve(context) as? String + try expect(result) == "name" } - $0.it("can resolve the first item") { - let variable = Variable("name.first") - let result = try variable.resolve(context) as? Character - try expect(result) == "K" - } - - $0.it("can resolve the last item") { - let variable = Variable("name.last") - let result = try variable.resolve(context) as? Character - try expect(result) == "e" - } - - $0.it("can get the characters count") { - let variable = Variable("name.count") + $0.it("can resolve an integer literal") { + let variable = Variable("5") let result = try variable.resolve(context) as? Int - try expect(result) == 4 + try expect(result) == 5 } - } - $0.context("given dictionary") { - $0.it("can resolve an item") { - let variable = Variable("profiles.github") + $0.it("can resolve an float literal") { + let variable = Variable("3.14") + let result = try variable.resolve(context) as? Number + try expect(result) == 3.14 + } + + $0.it("can resolve boolean literal") { + try expect(Variable("true").resolve(context) as? Bool) == true + try expect(Variable("false").resolve(context) as? Bool) == false + try expect(Variable("0").resolve(context) as? Int) == 0 + try expect(Variable("1").resolve(context) as? Int) == 1 + } + + $0.it("can resolve a string variable") { + let variable = Variable("name") let result = try variable.resolve(context) as? String - try expect(result) == "kylef" + try expect(result) == "Kyle" } - $0.it("can get the count") { - let variable = Variable("profiles.count") - let result = try variable.resolve(context) as? Int - try expect(result) == 1 - } - } + $0.context("given string") { + $0.it("can resolve an item via it's index") { + let variable = Variable("name.0") + let result = try variable.resolve(context) as? Character + try expect(result) == "K" - $0.context("given array") { - $0.it("can resolve an item via it's index") { - let variable = Variable("contacts.0") - let result = try variable.resolve(context) as? String - try expect(result) == "Katie" + let variable1 = Variable("name.1") + let result1 = try variable1.resolve(context) as? Character + try expect(result1) == "y" + } - let variable1 = Variable("contacts.1") - let result1 = try variable1.resolve(context) as? String - try expect(result1) == "Carlton" - } + $0.it("can resolve an item via unknown index") { + let variable = Variable("name.5") + let result = try variable.resolve(context) as? Character + try expect(result).to.beNil() - $0.it("can resolve an item via unknown index") { - let variable = Variable("contacts.5") - let result = try variable.resolve(context) as? String - try expect(result).to.beNil() + let variable1 = Variable("name.-5") + let result1 = try variable1.resolve(context) as? Character + try expect(result1).to.beNil() + } - let variable1 = Variable("contacts.-5") - let result1 = try variable1.resolve(context) as? String - try expect(result1).to.beNil() - } + $0.it("can resolve the first item") { + let variable = Variable("name.first") + let result = try variable.resolve(context) as? Character + try expect(result) == "K" + } - $0.it("can resolve the first item") { - let variable = Variable("contacts.first") - let result = try variable.resolve(context) as? String - try expect(result) == "Katie" - } + $0.it("can resolve the last item") { + let variable = Variable("name.last") + let result = try variable.resolve(context) as? Character + try expect(result) == "e" + } - $0.it("can resolve the last item") { - let variable = Variable("contacts.last") - let result = try variable.resolve(context) as? String - try expect(result) == "Carlton" - } - - $0.it("can get the count") { - let variable = Variable("contacts.count") - let result = try variable.resolve(context) as? Int - try expect(result) == 2 - } - } - - $0.it("can resolve a property with reflection") { - let variable = Variable("article.author.name") - let result = try variable.resolve(context) as? String - try expect(result) == "Kyle" - } - -#if os(OSX) - $0.it("can resolve a value via KVO") { - let variable = Variable("object.title") - let result = try variable.resolve(context) as? String - try expect(result) == "Hello World" - } - - $0.it("can resolve a superclass value via KVO") { - let variable = Variable("object.name") - let result = try variable.resolve(context) as? String - try expect(result) == "Foo" - } - - $0.it("does not crash on KVO") { - let variable = Variable("object.fullname") - let result = try variable.resolve(context) as? String - try expect(result).to.beNil() - } -#endif - - $0.it("can resolve a value via reflection") { - let variable = Variable("blog.articles.0.author.name") - let result = try variable.resolve(context) as? String - try expect(result) == "Kyle" - } - - $0.it("can resolve a superclass value via reflection") { - let variable = Variable("blog.url") - let result = try variable.resolve(context) as? String - try expect(result) == "blog.com" - } - - $0.it("can resolve optional variable property using reflection") { - let variable = Variable("blog.featuring.author.name") - let result = try variable.resolve(context) as? String - try expect(result) == "Jhon" - } - - $0.it("does not render Optional") { - var array: [Any?] = [1, nil] - array.append(array) - let context = Context(dictionary: ["values": array]) - - try expect(VariableNode(variable: "values").render(context)) == "[1, nil, [1, nil]]" - try expect(VariableNode(variable: "values.1").render(context)) == "" - } - - $0.it("can subscript tuple by index") { - let variable = Variable("tuple.0") - let result = try variable.resolve(context) as? Int - try expect(result) == 1 - } - - $0.it("can subscript tuple by label") { - let variable = Variable("tuple.two") - let result = try variable.resolve(context) as? Int - try expect(result) == 2 - } - - $0.describe("Subscripting") { - $0.it("can resolve a property subscript via reflection") { - try context.push(dictionary: ["property": "name"]) { - let variable = Variable("article.author[property]") - let result = try variable.resolve(context) as? String - try expect(result) == "Kyle" + $0.it("can get the characters count") { + let variable = Variable("name.count") + let result = try variable.resolve(context) as? Int + try expect(result) == 4 } } - $0.it("can subscript an array with a valid index") { - try context.push(dictionary: ["property": 0]) { - let variable = Variable("contacts[property]") + $0.context("given dictionary") { + $0.it("can resolve an item") { + let variable = Variable("profiles.github") + let result = try variable.resolve(context) as? String + try expect(result) == "kylef" + } + + $0.it("can get the count") { + let variable = Variable("profiles.count") + let result = try variable.resolve(context) as? Int + try expect(result) == 1 + } + } + + $0.context("given array") { + $0.it("can resolve an item via it's index") { + let variable = Variable("contacts.0") + let result = try variable.resolve(context) as? String + try expect(result) == "Katie" + + let variable1 = Variable("contacts.1") + let result1 = try variable1.resolve(context) as? String + try expect(result1) == "Carlton" + } + + $0.it("can resolve an item via unknown index") { + let variable = Variable("contacts.5") + let result = try variable.resolve(context) as? String + try expect(result).to.beNil() + + let variable1 = Variable("contacts.-5") + let result1 = try variable1.resolve(context) as? String + try expect(result1).to.beNil() + } + + $0.it("can resolve the first item") { + let variable = Variable("contacts.first") let result = try variable.resolve(context) as? String try expect(result) == "Katie" } - } - $0.it("can subscript an array with an unknown index") { - try context.push(dictionary: ["property": 5]) { - let variable = Variable("contacts[property]") + $0.it("can resolve the last item") { + let variable = Variable("contacts.last") let result = try variable.resolve(context) as? String - try expect(result).to.beNil() + try expect(result) == "Carlton" + } + + $0.it("can get the count") { + let variable = Variable("contacts.count") + let result = try variable.resolve(context) as? Int + try expect(result) == 2 } } -#if os(OSX) - $0.it("can resolve a subscript via KVO") { - try context.push(dictionary: ["property": "name"]) { - let variable = Variable("object[property]") - let result = try variable.resolve(context) as? String - try expect(result) == "Foo" - } - } -#endif - - $0.it("can resolve an optional subscript via reflection") { - try context.push(dictionary: ["property": "featuring"]) { - let variable = Variable("blog[property].author.name") - let result = try variable.resolve(context) as? String - try expect(result) == "Jhon" - } + $0.it("can resolve a property with reflection") { + let variable = Variable("article.author.name") + let result = try variable.resolve(context) as? String + try expect(result) == "Kyle" } - $0.it("can resolve multiple subscripts") { - try context.push(dictionary: [ - "prop1": "articles", - "prop2": 0, - "prop3": "name" - ]) { - let variable = Variable("blog[prop1][prop2].author[prop3]") - let result = try variable.resolve(context) as? String - try expect(result) == "Kyle" - } + #if os(OSX) + $0.it("can resolve a value via KVO") { + let variable = Variable("object.title") + let result = try variable.resolve(context) as? String + try expect(result) == "Hello World" } - $0.it("can resolve nested subscripts") { - try context.push(dictionary: [ - "prop1": "prop2", - "ref": ["prop2": "name"] - ]) { - let variable = Variable("article.author[ref[prop1]]") - let result = try variable.resolve(context) as? String - try expect(result) == "Kyle" - } + $0.it("can resolve a superclass value via KVO") { + let variable = Variable("object.name") + let result = try variable.resolve(context) as? String + try expect(result) == "Foo" } - $0.it("throws for invalid keypath syntax") { - try context.push(dictionary: ["prop": "name"]) { - let samples = [ - ".", - "..", - ".test", - "test..test", - "[prop]", - "article.author[prop", - "article.author[[prop]", - "article.author[prop]]", - "article.author[]", - "article.author[[]]", - "article.author[prop][]", - "article.author[prop]comments", - "article.author[.]" - ] + $0.it("does not crash on KVO") { + let variable = Variable("object.fullname") + let result = try variable.resolve(context) as? String + try expect(result).to.beNil() + } + #endif - for lookup in samples { - let variable = Variable(lookup) - try expect(variable.resolve(context)).toThrow() + $0.it("can resolve a value via reflection") { + let variable = Variable("blog.articles.0.author.name") + let result = try variable.resolve(context) as? String + try expect(result) == "Kyle" + } + + $0.it("can resolve a superclass value via reflection") { + let variable = Variable("blog.url") + let result = try variable.resolve(context) as? String + try expect(result) == "blog.com" + } + + $0.it("can resolve optional variable property using reflection") { + let variable = Variable("blog.featuring.author.name") + let result = try variable.resolve(context) as? String + try expect(result) == "Jhon" + } + + $0.it("does not render Optional") { + var array: [Any?] = [1, nil] + array.append(array) + let context = Context(dictionary: ["values": array]) + + try expect(VariableNode(variable: "values").render(context)) == "[1, nil, [1, nil]]" + try expect(VariableNode(variable: "values.1").render(context)) == "" + } + + $0.it("can subscript tuple by index") { + let variable = Variable("tuple.0") + let result = try variable.resolve(context) as? Int + try expect(result) == 1 + } + + $0.it("can subscript tuple by label") { + let variable = Variable("tuple.two") + let result = try variable.resolve(context) as? Int + try expect(result) == 2 + } + + $0.describe("Subscripting") { + $0.it("can resolve a property subscript via reflection") { + try context.push(dictionary: ["property": "name"]) { + let variable = Variable("article.author[property]") + let result = try variable.resolve(context) as? String + try expect(result) == "Kyle" + } + } + + $0.it("can subscript an array with a valid index") { + try context.push(dictionary: ["property": 0]) { + let variable = Variable("contacts[property]") + let result = try variable.resolve(context) as? String + try expect(result) == "Katie" + } + } + + $0.it("can subscript an array with an unknown index") { + try context.push(dictionary: ["property": 5]) { + let variable = Variable("contacts[property]") + let result = try variable.resolve(context) as? String + try expect(result).to.beNil() + } + } + + #if os(OSX) + $0.it("can resolve a subscript via KVO") { + try context.push(dictionary: ["property": "name"]) { + let variable = Variable("object[property]") + let result = try variable.resolve(context) as? String + try expect(result) == "Foo" + } + } + #endif + + $0.it("can resolve an optional subscript via reflection") { + try context.push(dictionary: ["property": "featuring"]) { + let variable = Variable("blog[property].author.name") + let result = try variable.resolve(context) as? String + try expect(result) == "Jhon" + } + } + + $0.it("can resolve multiple subscripts") { + try context.push(dictionary: [ + "prop1": "articles", + "prop2": 0, + "prop3": "name" + ]) { + let variable = Variable("blog[prop1][prop2].author[prop3]") + let result = try variable.resolve(context) as? String + try expect(result) == "Kyle" + } + } + + $0.it("can resolve nested subscripts") { + try context.push(dictionary: [ + "prop1": "prop2", + "ref": ["prop2": "name"] + ]) { + let variable = Variable("article.author[ref[prop1]]") + let result = try variable.resolve(context) as? String + try expect(result) == "Kyle" + } + } + + $0.it("throws for invalid keypath syntax") { + try context.push(dictionary: ["prop": "name"]) { + let samples = [ + ".", + "..", + ".test", + "test..test", + "[prop]", + "article.author[prop", + "article.author[[prop]", + "article.author[prop]]", + "article.author[]", + "article.author[[]]", + "article.author[prop][]", + "article.author[prop]comments", + "article.author[.]" + ] + + for lookup in samples { + let variable = Variable(lookup) + try expect(variable.resolve(context)).toThrow() + } } } } } - } - describe("RangeVariable") { + describe("RangeVariable") { - let context: Context = { - let ext = Extension() - ext.registerFilter("incr", filter: { (arg: Any?) in toNumber(value: arg!)! + 1 }) - let environment = Environment(extensions: [ext]) - return Context(dictionary: [:], environment: environment) - }() + let context: Context = { + let ext = Extension() + ext.registerFilter("incr", filter: { (arg: Any?) in toNumber(value: arg!)! + 1 }) + let environment = Environment(extensions: [ext]) + return Context(dictionary: [:], environment: environment) + }() + + func makeVariable(_ token: String) throws -> RangeVariable? { + let token = Token.variable(value: token, at: .unknown) + let parser = TokenParser(tokens: [token], environment: context.environment) + return try RangeVariable(token.contents, parser: parser, containedIn: token) + } + + $0.it("can resolve closed range as array") { + let result = try makeVariable("1...3")?.resolve(context) as? [Int] + try expect(result) == [1, 2, 3] + } + + $0.it("can resolve decreasing closed range as reversed array") { + let result = try makeVariable("3...1")?.resolve(context) as? [Int] + try expect(result) == [3, 2, 1] + } + + $0.it("can use filter on range variables") { + let result = try makeVariable("1|incr...3|incr")?.resolve(context) as? [Int] + try expect(result) == [2, 3, 4] + } + + $0.it("throws when left value is not int") { + let template: Template = "{% for i in k...j %}{{ i }}{% endfor %}" + try expect(try template.render(Context(dictionary: ["j": 3, "k": "1"]))).toThrow() + } + + $0.it("throws when right value is not int") { + let variable = try makeVariable("k...j") + try expect(try variable?.resolve(Context(dictionary: ["j": "3", "k": 1]))).toThrow() + } + + $0.it("throws is left range value is missing") { + try expect(makeVariable("...1")).toThrow() + } + + $0.it("throws is right range value is missing") { + try expect(makeVariable("1...")).toThrow() + } - func makeVariable(_ token: String) throws -> RangeVariable? { - let token = Token.variable(value: token, at: .unknown) - let parser = TokenParser(tokens: [token], environment: context.environment) - return try RangeVariable(token.contents, parser: parser, containedIn: token) } - $0.it("can resolve closed range as array") { - let result = try makeVariable("1...3")?.resolve(context) as? [Int] - try expect(result) == [1, 2, 3] - } + describe("inline if expression") { - $0.it("can resolve decreasing closed range as reversed array") { - let result = try makeVariable("3...1")?.resolve(context) as? [Int] - try expect(result) == [3, 2, 1] - } + $0.it("can conditionally render variable") { + let template: Template = "{{ variable if variable|uppercase == \"A\" }}" + try expect(template.render(Context(dictionary: ["variable": "a"]))) == "a" + try expect(template.render(Context(dictionary: ["variable": "b"]))) == "" + } - $0.it("can use filter on range variables") { - let result = try makeVariable("1|incr...3|incr")?.resolve(context) as? [Int] - try expect(result) == [2, 3, 4] - } + $0.it("can render with else expression") { + let template: Template = "{{ variable if variable|uppercase == \"A\" else fallback|uppercase }}" + try expect(template.render(Context(dictionary: ["variable": "b", "fallback": "c"]))) == "C" + } - $0.it("throws when left value is not int") { - let template: Template = "{% for i in k...j %}{{ i }}{% endfor %}" - try expect(try template.render(Context(dictionary: ["j": 3, "k": "1"]))).toThrow() - } - - $0.it("throws when right value is not int") { - let variable = try makeVariable("k...j") - try expect(try variable?.resolve(Context(dictionary: ["j": "3", "k": 1]))).toThrow() - } - - $0.it("throws is left range value is missing") { - try expect(makeVariable("...1")).toThrow() - } - - $0.it("throws is right range value is missing") { - try expect(makeVariable("1...")).toThrow() - } - - } - - describe("inline if expression") { - - $0.it("can conditionally render variable") { - let template: Template = "{{ variable if variable|uppercase == \"A\" }}" - try expect(template.render(Context(dictionary: ["variable": "a"]))) == "a" - try expect(template.render(Context(dictionary: ["variable": "b"]))) == "" - } - - $0.it("can render with else expression") { - let template: Template = "{{ variable if variable|uppercase == \"A\" else fallback|uppercase }}" - try expect(template.render(Context(dictionary: ["variable": "b", "fallback": "c"]))) == "C" - } - - $0.it("throws when used invalid condition") { - let template: Template = "{{ variable if variable \"A\" }}" - try expect(template.render(Context(dictionary: ["variable": "a"]))).toThrow() + $0.it("throws when used invalid condition") { + let template: Template = "{{ variable if variable \"A\" }}" + try expect(template.render(Context(dictionary: ["variable": "a"]))).toThrow() + } } } } diff --git a/Tests/StencilTests/XCTest.swift b/Tests/StencilTests/XCTest.swift deleted file mode 100644 index b2d4de9..0000000 --- a/Tests/StencilTests/XCTest.swift +++ /dev/null @@ -1,30 +0,0 @@ -import XCTest - - -public func stencilTests() { - testContext() - testFilter() - testLexer() - testToken() - testTokenParser() - testTemplateLoader() - testTemplate() - testVariable() - testNode() - testForNode() - testExpressions() - testIfNode() - testNowNode() - testInclude() - testInheritence() - testFilterTag() - testEnvironment() - testStencil() -} - - -class StencilTests: XCTestCase { - func testRunStencilTests() { - stencilTests() - } -} diff --git a/Tests/StencilTests/XCTestManifests.swift b/Tests/StencilTests/XCTestManifests.swift new file mode 100644 index 0000000..84f6cce --- /dev/null +++ b/Tests/StencilTests/XCTestManifests.swift @@ -0,0 +1,134 @@ +import XCTest + +extension ContextTests { + static let __allTests = [ + ("testContext", testContext), + ] +} + +extension EnvironmentTests { + static let __allTests = [ + ("testEnvironment", testEnvironment), + ] +} + +extension ExpressionsTests { + static let __allTests = [ + ("testExpressions", testExpressions), + ] +} + +extension FilterTagTests { + static let __allTests = [ + ("testFilterTag", testFilterTag), + ] +} + +extension FilterTests { + static let __allTests = [ + ("testFilter", testFilter), + ] +} + +extension ForNodeTests { + static let __allTests = [ + ("testForNode", testForNode), + ] +} + +extension IfNodeTests { + static let __allTests = [ + ("testIfNode", testIfNode), + ] +} + +extension IncludeTests { + static let __allTests = [ + ("testInclude", testInclude), + ] +} + +extension InheritenceTests { + static let __allTests = [ + ("testInheritence", testInheritence), + ] +} + +extension LexerTests { + static let __allTests = [ + ("testLexer", testLexer), + ] +} + +extension NodeTests { + static let __allTests = [ + ("testNode", testNode), + ] +} + +extension NowNodeTests { + static let __allTests = [ + ("testNowNode", testNowNode), + ] +} + +extension StencilTests { + static let __allTests = [ + ("testStencil", testStencil), + ] +} + +extension TemplateLoaderTests { + static let __allTests = [ + ("testTemplateLoader", testTemplateLoader), + ] +} + +extension TemplateTests { + static let __allTests = [ + ("testTemplate", testTemplate), + ] +} + +extension TokenParserTests { + static let __allTests = [ + ("testTokenParser", testTokenParser), + ] +} + +extension TokenTests { + static let __allTests = [ + ("testToken", testToken), + ] +} + +extension VariableTests { + static let __allTests = [ + ("testVariable", testVariable), + ] +} + +#if !os(macOS) +public func __allTests() -> [XCTestCaseEntry] { + return [ + testCase(ContextTests.__allTests), + testCase(EnvironmentTests.__allTests), + testCase(ExpressionsTests.__allTests), + testCase(FilterTagTests.__allTests), + testCase(FilterTests.__allTests), + testCase(ForNodeTests.__allTests), + testCase(IfNodeTests.__allTests), + testCase(IncludeTests.__allTests), + testCase(InheritenceTests.__allTests), + testCase(LexerTests.__allTests), + testCase(NodeTests.__allTests), + testCase(NowNodeTests.__allTests), + testCase(StencilTests.__allTests), + testCase(TemplateLoaderTests.__allTests), + testCase(TemplateTests.__allTests), + testCase(TokenParserTests.__allTests), + testCase(TokenTests.__allTests), + testCase(VariableTests.__allTests), + ] +} +#endif From fce3dc5e489f27e5995bde73c5112b785550748b Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Tue, 25 Sep 2018 23:29:21 +0100 Subject: [PATCH 64/81] Added method to register boolean filters (#160) * added method to register boolean filters * parametrised negative filter name * Update Extension.swift * Update CHANGELOG.md * renamed registerBooleanFilter to registerFilter * updated docs --- CHANGELOG.md | 3 +++ Sources/Extension.swift | 9 +++++++++ Tests/StencilTests/FilterSpec.swift | 18 ++++++++++++++++++ docs/custom-template-tags-and-filters.rst | 11 +++++++++++ 4 files changed, 41 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02fb49b..9ce06c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ - You can now use parentheses in boolean expressions to change operator precedence. [Ilya Puchka](https://github.com/ilyapuchka) [#165](https://github.com/stencilproject/Stencil/pull/165) +- Added method to add boolean filters with their negative counterparts. + [Ilya Puchka](https://github.com/ilyapuchka) + [#160](https://github.com/stencilproject/Stencil/pull/160) ### New Features diff --git a/Sources/Extension.swift b/Sources/Extension.swift index 1203378..d2ee907 100644 --- a/Sources/Extension.swift +++ b/Sources/Extension.swift @@ -18,6 +18,15 @@ open class Extension { return SimpleNode(token: token, handler: handler) }) } + + /// Registers boolean filter with it's negative counterpart + public func registerFilter(name: String, negativeFilterName: String, filter: @escaping (Any?) throws -> Bool?) { + filters[name] = .simple(filter) + filters[negativeFilterName] = .simple { + guard let result = try filter($0) else { return nil } + return !result + } + } /// Registers a template filter with the given name public func registerFilter(_ name: String, filter: @escaping (Any?) throws -> Any?) { diff --git a/Tests/StencilTests/FilterSpec.swift b/Tests/StencilTests/FilterSpec.swift index 9e4a58a..35f20e5 100644 --- a/Tests/StencilTests/FilterSpec.swift +++ b/Tests/StencilTests/FilterSpec.swift @@ -22,6 +22,24 @@ class FilterTests: XCTestCase { let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))) try expect(result) == "Kyle Kyle" } + + $0.it("allows you to register boolean filters") { + let repeatExtension = Extension() + repeatExtension.registerFilter(name: "isPositive", negativeFilterName: "isNotPositive") { (value: Any?) in + if let value = value as? Int { + return value > 0 + } + return nil + } + + let result = try Template(templateString: "{{ value|isPositive }}") + .render(Context(dictionary: ["value": 1], environment: Environment(extensions: [repeatExtension]))) + try expect(result) == "true" + + let negativeResult = try Template(templateString: "{{ value|isNotPositive }}") + .render(Context(dictionary: ["value": -1], environment: Environment(extensions: [repeatExtension]))) + try expect(negativeResult) == "true" + } $0.it("allows you to register a custom filter which accepts single argument") { let template = Template(templateString: """ diff --git a/docs/custom-template-tags-and-filters.rst b/docs/custom-template-tags-and-filters.rst index 2ec7e5a..7ce4756 100644 --- a/docs/custom-template-tags-and-filters.rst +++ b/docs/custom-template-tags-and-filters.rst @@ -48,6 +48,17 @@ Registering custom filters with arguments: return value } +Registering custom boolean filters: + +.. code-block:: swift + + ext.registerFilter("ordinary", negativeFilterName: "odd") { (value: Any?) in + if let value = value as? Int { + return myInt % 2 == 0 + } + return nil + } + Custom Tags ----------- From 07a6b2aea5c476b7de5dbd8bc9a8e8d0ab18028d Mon Sep 17 00:00:00 2001 From: ethorpe Date: Wed, 4 Apr 2018 13:57:33 +1000 Subject: [PATCH 65/81] Rewrites scanner for better performance. This is primarily an improvement under Ubuntu Cleanup readability a little bit Rewrite original scan function so it's available. Syntax improvements Fix deprecation warnings in Lexer Cleanup some syntax issues lexer t t --- Sources/Lexer.swift | 90 ++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 55 deletions(-) diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 26d6a1a..6781f0d 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -7,6 +7,13 @@ struct Lexer { let templateString: String let lines: [Line] + private static let tokenChars: [Unicode.Scalar] = ["{", "%", "#"] + private static let tokenCharMap: [Unicode.Scalar: Unicode.Scalar] = [ + "{": "}", + "%": "%", + "#": "#" + ] + init(templateName: String? = nil, templateString: String) { self.templateName = templateName self.templateString = templateString @@ -20,9 +27,7 @@ struct Lexer { func createToken(string: String, at range: Range) -> Token { func strip() -> String { guard string.count > 4 else { return "" } - let start = string.index(string.startIndex, offsetBy: 2) - let end = string.index(string.endIndex, offsetBy: -2) - let trimmed = String(string[start.. + private static let tokenStartDelimiter: Unicode.Scalar = "{" + private static let tokenEndDelimiter: Unicode.Scalar = "}" + init(_ content: String) { self.originalContent = content self.content = content @@ -105,64 +106,43 @@ class Scanner { return content.isEmpty } - func scan(until: String, returnUntil: Bool = false) -> String { - var index = content.startIndex + func scanForTokenEnd(_ tokenChar: Unicode.Scalar) -> String { + var foundChar = false - if until.isEmpty { - return "" - } - - range = range.upperBound.. (String, String)? { - if until.isEmpty { - return nil - } + func scanForTokenStart(_ tokenChars: [Unicode.Scalar]) -> (Unicode.Scalar, String)? { + var foundBrace = false - var index = content.startIndex range = range.upperBound.. String.Index? { var index = startIndex From 4f84627caa594a2982f19284fe03d42e1b39609d Mon Sep 17 00:00:00 2001 From: David Jennes Date: Sat, 22 Sep 2018 00:54:10 +0200 Subject: [PATCH 66/81] Add test for crashing --- Tests/StencilTests/LexerSpec.swift | 44 +++++++++++++++++------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/Tests/StencilTests/LexerSpec.swift b/Tests/StencilTests/LexerSpec.swift index 6f49a4c..02f6d5e 100644 --- a/Tests/StencilTests/LexerSpec.swift +++ b/Tests/StencilTests/LexerSpec.swift @@ -61,36 +61,42 @@ class LexerTests: XCTestCase { $0.it("can tokenize an unclosed block") { let lexer = Lexer(templateString: "{%}") - let _ = lexer.tokenize() + _ = lexer.tokenize() + } + + $0.it("can tokenize incorrect syntax without crashing") { + let lexer = Lexer(templateString: "func some() {{% if %}") + _ = lexer.tokenize() } $0.it("can tokenize an empty variable") { let lexer = Lexer(templateString: "{{}}") - let _ = lexer.tokenize() + _ = lexer.tokenize() } $0.it("can tokenize with new lines") { let templateString = """ - My name is {% - if name - and - name - %}{{ - name - }}{% - endif %}. - """ + My name is {% + if name + and + name + %}{{ + name + }}{% + endif %}. + """ - let lexer = Lexer(templateString: templateString) + let lexer = Lexer(templateString: templateString) - let tokens = lexer.tokenize() + let tokens = lexer.tokenize() - try expect(tokens.count) == 5 - try expect(tokens[0]) == Token.text(value: "My name is ", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "My name is")!))) - try expect(tokens[1]) == Token.block(value: "if name and name", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "{%")!))) - try expect(tokens[2]) == Token.variable(value: "name", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "name", options: [.backwards])!))) - try expect(tokens[3]) == Token.block(value: "endif", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "endif")!))) - try expect(tokens[4]) == Token.text(value: ".", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: ".")!))) + try expect(tokens.count) == 5 + try expect(tokens[0]) == Token.text(value: "My name is ", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "My name is")!))) + try expect(tokens[1]) == Token.block(value: "if name and name", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "{%")!))) + try expect(tokens[2]) == Token.variable(value: "name", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "name", options: [.backwards])!))) + try expect(tokens[3]) == Token.block(value: "endif", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "endif")!))) + try expect(tokens[4]) == Token.text(value: ".", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: ".")!))) + } } } } From e77bd22e839eea185e8d22d1da78a0f741e2f8ee Mon Sep 17 00:00:00 2001 From: Liquidsoul Date: Sat, 1 Sep 2018 12:15:15 +0200 Subject: [PATCH 67/81] Add changelog entry --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ce06c8..9645027 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,11 @@ - Update to Spectre 0.9.0. [Ilya Puchka](https://github.com/ilyapuchka) [#247](https://github.com/stencilproject/Stencil/pull/247) +- Optimise Scanner performance. + [Eric Thorpe](https://github.com/trametheka) + [Sébastien Duperron](https://github.com/Liquidsoul) + [David Jennes](https://github.com/djbe) + [#226](https://github.com/stencilproject/Stencil/pull/226) ## 0.12.1 From 652dcd246dcb8a9779617b950f5a42d9c8abee6c Mon Sep 17 00:00:00 2001 From: David Jennes Date: Sat, 22 Sep 2018 03:33:40 +0200 Subject: [PATCH 68/81] Add lexer test for escape sequence --- Tests/StencilTests/LexerSpec.swift | 43 ++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/Tests/StencilTests/LexerSpec.swift b/Tests/StencilTests/LexerSpec.swift index 02f6d5e..c772d30 100644 --- a/Tests/StencilTests/LexerSpec.swift +++ b/Tests/StencilTests/LexerSpec.swift @@ -5,6 +5,11 @@ import Spectre class LexerTests: XCTestCase { func testLexer() { describe("Lexer") { + func makeSourceMap(_ token: String, for lexer: Lexer, options: String.CompareOptions = []) -> SourceMap { + guard let range = lexer.templateString.range(of: token, options: options) else { fatalError("Token not found") } + return SourceMap(location: lexer.rangeLocation(range)) + } + $0.it("can tokenize text") { let lexer = Lexer(templateString: "Hello World") let tokens = lexer.tokenize() @@ -44,9 +49,9 @@ class LexerTests: XCTestCase { let tokens = lexer.tokenize() try expect(tokens.count) == 3 - try expect(tokens[0]) == Token.text(value: "My name is ", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "My name is ")!))) - try expect(tokens[1]) == Token.variable(value: "myname", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "myname")!))) - try expect(tokens[2]) == Token.text(value: ".", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: ".")!))) + try expect(tokens[0]) == Token.text(value: "My name is ", at: makeSourceMap("My name is ", for: lexer)) + try expect(tokens[1]) == Token.variable(value: "myname", at: makeSourceMap("myname", for: lexer)) + try expect(tokens[2]) == Token.text(value: ".", at: makeSourceMap(".", for: lexer)) } $0.it("can tokenize two variables without being greedy") { @@ -55,8 +60,8 @@ class LexerTests: XCTestCase { let tokens = lexer.tokenize() try expect(tokens.count) == 2 - try expect(tokens[0]) == Token.variable(value: "thing", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "thing")!))) - try expect(tokens[1]) == Token.variable(value: "name", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "name")!))) + try expect(tokens[0]) == Token.variable(value: "thing", at: makeSourceMap("thing", for: lexer)) + try expect(tokens[1]) == Token.variable(value: "name", at: makeSourceMap("name", for: lexer)) } $0.it("can tokenize an unclosed block") { @@ -85,18 +90,28 @@ class LexerTests: XCTestCase { }}{% endif %}. """ + let lexer = Lexer(templateString: templateString) + let tokens = lexer.tokenize() - let lexer = Lexer(templateString: templateString) + try expect(tokens.count) == 5 + try expect(tokens[0]) == Token.text(value: "My name is ", at: makeSourceMap("My name is", for: lexer)) + try expect(tokens[1]) == Token.block(value: "if name and name", at: makeSourceMap("{%", for: lexer)) + try expect(tokens[2]) == Token.variable(value: "name", at: makeSourceMap("name", for: lexer, options: .backwards)) + try expect(tokens[3]) == Token.block(value: "endif", at: makeSourceMap("endif", for: lexer)) + try expect(tokens[4]) == Token.text(value: ".", at: makeSourceMap(".", for: lexer)) + } - let tokens = lexer.tokenize() + $0.it("can tokenize escape sequences") { + let templateString = "class Some {{ '{' }}{% if true %}{{ stuff }}{% endif %}" + let lexer = Lexer(templateString: templateString) + let tokens = lexer.tokenize() - try expect(tokens.count) == 5 - try expect(tokens[0]) == Token.text(value: "My name is ", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "My name is")!))) - try expect(tokens[1]) == Token.block(value: "if name and name", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "{%")!))) - try expect(tokens[2]) == Token.variable(value: "name", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "name", options: [.backwards])!))) - try expect(tokens[3]) == Token.block(value: "endif", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: "endif")!))) - try expect(tokens[4]) == Token.text(value: ".", at: SourceMap(location: lexer.rangeLocation(templateString.range(of: ".")!))) - } + try expect(tokens.count) == 5 + try expect(tokens[0]) == Token.text(value: "class Some ", at: makeSourceMap("class Some ", for: lexer)) + try expect(tokens[1]) == Token.variable(value: "'{'", at: makeSourceMap("'{'", for: lexer)) + try expect(tokens[2]) == Token.block(value: "if true", at: makeSourceMap("if true", for: lexer)) + try expect(tokens[3]) == Token.variable(value: "stuff", at: makeSourceMap("stuff", for: lexer)) + try expect(tokens[4]) == Token.block(value: "endif", at: makeSourceMap("endif", for: lexer)) } } } From fff93f18dd7359e0774ad9fe3d1413fc247c7180 Mon Sep 17 00:00:00 2001 From: David Jennes Date: Mon, 24 Sep 2018 00:15:52 +0200 Subject: [PATCH 69/81] Add performance test (no reporting yet) --- Tests/StencilTests/LexerSpec.swift | 13 +- Tests/StencilTests/XCTestManifests.swift | 1 + Tests/StencilTests/fixtures/huge.html | 1131 ++++++++++++++++++++++ 3 files changed, 1144 insertions(+), 1 deletion(-) create mode 100644 Tests/StencilTests/fixtures/huge.html diff --git a/Tests/StencilTests/LexerSpec.swift b/Tests/StencilTests/LexerSpec.swift index c772d30..ebc114e 100644 --- a/Tests/StencilTests/LexerSpec.swift +++ b/Tests/StencilTests/LexerSpec.swift @@ -1,6 +1,7 @@ -import XCTest +import PathKit import Spectre @testable import Stencil +import XCTest class LexerTests: XCTestCase { func testLexer() { @@ -115,4 +116,14 @@ class LexerTests: XCTestCase { } } } + + func testPerformance() throws { + let path = Path(#file) + ".." + "fixtures" + "huge.html" + let content: String = try path.read() + + measure { + let lexer = Lexer(templateString: content) + _ = lexer.tokenize() + } + } } diff --git a/Tests/StencilTests/XCTestManifests.swift b/Tests/StencilTests/XCTestManifests.swift index 84f6cce..73cf026 100644 --- a/Tests/StencilTests/XCTestManifests.swift +++ b/Tests/StencilTests/XCTestManifests.swift @@ -57,6 +57,7 @@ extension InheritenceTests { extension LexerTests { static let __allTests = [ ("testLexer", testLexer), + ("testPerformance", testPerformance), ] } diff --git a/Tests/StencilTests/fixtures/huge.html b/Tests/StencilTests/fixtures/huge.html new file mode 100644 index 0000000..c338ed6 --- /dev/null +++ b/Tests/StencilTests/fixtures/huge.html @@ -0,0 +1,1131 @@ + + + + {% block title %}Rond De Tafel + {% if sort == "new" %} + {{ block.super }} - Nieuwste spellen + {% elif sort == "upcoming" %} + {{ block.super }} - Binnenkort op de agenda + {% elif sort == "near-me" %} + {{ block.super }} - In mijn buurt + {% endif %} + {% endblock %} + + + + + + + + + + {% block opengraph %} + + + {% endblock %} + + + + + + + + {% block additional-head %}{% endblock %} + + + + + + + +
+
+ +
    + {% if sort == "new" %} +
  • + + Nieuw + +
  • + {% else %} +
  • + + Nieuw + +
  • + {% endif %} + {% if sort == "upcoming" %} +
  • + + Binnenkort + +
  • + {% else %} +
  • + + Binnenkort + +
  • + {% endif %} + {% if sort == "near-me" %} +
  • + + Dichtbij + +
  • + {% else %} +
  • + + Dichtbij + +
  • + {% endif %} +
+ + +
+ +{% if sort == "new" %} +

Nieuwste spellen

+{% elif sort == "upcoming" %} +

Binnenkort op de agenda

+{% elif sort == "near-me" %} +

In mijn buurt

+{% endif %} + +{% if base.user %} +
+ Spellen die je zelf organiseert worden niet getoond op deze pagina. + Deze spellen zijn te vinden in je persoonlijk menu, onder Mijn spellen. +
+{% endif %} + +{% if sort == "near-me" and not base.user.location %} +
+ {% if base.user %} + Om deze functie te activeren moet je een adres ingeven bij Instellingen. + {% else %} + Om deze functie te activeren moet je eerst aanmelden. + Daarna kan je een adres ingeven bij Instellingen. + {% endif %} +
+ +{% elif activities %} + {% for activity in activities %} + + + + + +
+
{{ activity.name }}
+

+ + + {{ activity.shortDate }} + + {{ activity.longDate }} + + om {{ activity.time }} +
+ + + {{ activity.time }}
+
+ {{ activity.host.name }}
+ {{ activity.location.city }} + {% if base.user.location %} + ({{ activity.distance }}km) + {% endif %} +

+
+
+ {% endfor %} + +{% else %} +

Geen spellen gepland.

+{% endif %} +
+ +
+ © 2018 - Rond De Tafel
+ Like ons op Facebook
+ Broncode beschikbaar op GitHub +
+ + + + + {% block additional-body %}{% endblock %} + + + + + + {% block title %}Rond De Tafel + {% if sort == "new" %} + {{ block.super }} - Nieuwste spellen + {% elif sort == "upcoming" %} + {{ block.super }} - Binnenkort op de agenda + {% elif sort == "near-me" %} + {{ block.super }} - In mijn buurt + {% endif %} + {% endblock %} + + + + + + + + + + {% block opengraph %} + + + {% endblock %} + + + + + + + + {% block additional-head %}{% endblock %} + + + + + + + +
+
+ +
    + {% if sort == "new" %} +
  • + + Nieuw + +
  • + {% else %} +
  • + + Nieuw + +
  • + {% endif %} + {% if sort == "upcoming" %} +
  • + + Binnenkort + +
  • + {% else %} +
  • + + Binnenkort + +
  • + {% endif %} + {% if sort == "near-me" %} +
  • + + Dichtbij + +
  • + {% else %} +
  • + + Dichtbij + +
  • + {% endif %} +
+ + +
+ +{% if sort == "new" %} +

Nieuwste spellen

+{% elif sort == "upcoming" %} +

Binnenkort op de agenda

+{% elif sort == "near-me" %} +

In mijn buurt

+{% endif %} + +{% if base.user %} +
+ Spellen die je zelf organiseert worden niet getoond op deze pagina. + Deze spellen zijn te vinden in je persoonlijk menu, onder Mijn spellen. +
+{% endif %} + +{% if sort == "near-me" and not base.user.location %} +
+ {% if base.user %} + Om deze functie te activeren moet je een adres ingeven bij Instellingen. + {% else %} + Om deze functie te activeren moet je eerst aanmelden. + Daarna kan je een adres ingeven bij Instellingen. + {% endif %} +
+ +{% elif activities %} + {% for activity in activities %} + + + + + +
+
{{ activity.name }}
+

+ + + {{ activity.shortDate }} + + {{ activity.longDate }} + + om {{ activity.time }} +
+ + + {{ activity.time }}
+
+ {{ activity.host.name }}
+ {{ activity.location.city }} + {% if base.user.location %} + ({{ activity.distance }}km) + {% endif %} +

+
+
+ {% endfor %} + +{% else %} +

Geen spellen gepland.

+{% endif %} +
+ +
+ © 2018 - Rond De Tafel
+ Like ons op Facebook
+ Broncode beschikbaar op GitHub +
+ + + + + {% block additional-body %}{% endblock %} + + + + + + {% block title %}Rond De Tafel + {% if sort == "new" %} + {{ block.super }} - Nieuwste spellen + {% elif sort == "upcoming" %} + {{ block.super }} - Binnenkort op de agenda + {% elif sort == "near-me" %} + {{ block.super }} - In mijn buurt + {% endif %} + {% endblock %} + + + + + + + + + + {% block opengraph %} + + + {% endblock %} + + + + + + + + {% block additional-head %}{% endblock %} + + + + + + + +
+
+ +
    + {% if sort == "new" %} +
  • + + Nieuw + +
  • + {% else %} +
  • + + Nieuw + +
  • + {% endif %} + {% if sort == "upcoming" %} +
  • + + Binnenkort + +
  • + {% else %} +
  • + + Binnenkort + +
  • + {% endif %} + {% if sort == "near-me" %} +
  • + + Dichtbij + +
  • + {% else %} +
  • + + Dichtbij + +
  • + {% endif %} +
+ + +
+ +{% if sort == "new" %} +

Nieuwste spellen

+{% elif sort == "upcoming" %} +

Binnenkort op de agenda

+{% elif sort == "near-me" %} +

In mijn buurt

+{% endif %} + +{% if base.user %} +
+ Spellen die je zelf organiseert worden niet getoond op deze pagina. + Deze spellen zijn te vinden in je persoonlijk menu, onder Mijn spellen. +
+{% endif %} + +{% if sort == "near-me" and not base.user.location %} +
+ {% if base.user %} + Om deze functie te activeren moet je een adres ingeven bij Instellingen. + {% else %} + Om deze functie te activeren moet je eerst aanmelden. + Daarna kan je een adres ingeven bij Instellingen. + {% endif %} +
+ +{% elif activities %} + {% for activity in activities %} + + + + + +
+
{{ activity.name }}
+

+ + + {{ activity.shortDate }} + + {{ activity.longDate }} + + om {{ activity.time }} +
+ + + {{ activity.time }}
+
+ {{ activity.host.name }}
+ {{ activity.location.city }} + {% if base.user.location %} + ({{ activity.distance }}km) + {% endif %} +

+
+
+ {% endfor %} + +{% else %} +

Geen spellen gepland.

+{% endif %} +
+ +
+ © 2018 - Rond De Tafel
+ Like ons op Facebook
+ Broncode beschikbaar op GitHub +
+ + + + + {% block additional-body %}{% endblock %} + + From cb4e51484660d17c92c0baa8e4c6f1e1b0f8e1a0 Mon Sep 17 00:00:00 2001 From: David Jennes Date: Wed, 26 Sep 2018 00:27:31 +0200 Subject: [PATCH 70/81] Code documentation --- Sources/Lexer.swift | 52 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 6781f0d..015b7d7 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -7,7 +7,12 @@ struct Lexer { let templateString: String let lines: [Line] + /// The potential token start characters. In a template these appear after a + /// `{` character, for example `{{`, `{%`, `{#`, ... private static let tokenChars: [Unicode.Scalar] = ["{", "%", "#"] + + /// The token end characters, corresponding to their token start characters. + /// For example, a variable token starts with `{{` and ends with `}}` private static let tokenCharMap: [Unicode.Scalar: Unicode.Scalar] = [ "{": "}", "%": "%", @@ -24,6 +29,15 @@ struct Lexer { } } + /// Create a token that will be passed on to the parser, with the given + /// content and a range. The content will be tested to see if it's a + /// `variable`, a `block` or a `comment`, otherwise it'll default to a simple + /// `text` token. + /// + /// - Parameters: + /// - string: The content string of the token + /// - range: The range within the template content, used for smart + /// error reporting func createToken(string: String, at range: Range) -> Token { func strip() -> String { guard string.count > 4 else { return "" } @@ -55,7 +69,10 @@ struct Lexer { return .text(value: string, at: sourceMap) } - /// Returns an array of tokens from a given template string. + /// Transforms the template into a list of tokens, that will eventually be + /// passed on to the parser. + /// + /// - Returns: The list of tokens (see `createToken(string: at:)`). func tokenize() -> [Token] { var tokens: [Token] = [] @@ -78,6 +95,11 @@ struct Lexer { return tokens } + /// Finds the line matching the given range (for a token) + /// + /// - Parameter range: The range to search for. + /// - Returns: The content for that line, the line number and offset within + /// the line. func rangeLocation(_ range: Range) -> ContentLocation { guard let line = self.lines.first(where: { $0.range.contains(range.lowerBound) }) else { return ("", 0, 0) @@ -93,7 +115,9 @@ class Scanner { var content: String var range: Range + /// The start delimiter for a token. private static let tokenStartDelimiter: Unicode.Scalar = "{" + /// And the corresponding end delimiter for a token. private static let tokenEndDelimiter: Unicode.Scalar = "}" init(_ content: String) { @@ -106,6 +130,17 @@ class Scanner { return content.isEmpty } + /// Scans for the end of a token, with a specific ending character. If we're + /// searching for the end of a block token `%}`, this method receives a `%`. + /// The scanner will search for that `%` followed by a `}`. + /// + /// Note: if the end of a token is found, the `content` and `range` + /// properties are updated to reflect this. `content` will be set to what + /// remains of the template after the token. `range` will be set to the range + /// of the token within the template. + /// + /// - Parameter tokenChar: The token end character to search for. + /// - Returns: The content of a token, or "" if no token end was found. func scanForTokenEnd(_ tokenChar: Unicode.Scalar) -> String { var foundChar = false @@ -124,6 +159,21 @@ class Scanner { return "" } + /// Scans for the start of a token, with a list of potential starting + /// characters. To scan for the start of variables (`{{`), blocks (`{%`) and + /// comments (`{#`), this method receives the characters `{`, `%` and `#`. + /// The scanner will search for a `{`, followed by one of the search + /// characters. It will give the found character, and the content that came + /// before the token. + /// + /// Note: if the start of a token is found, the `content` and `range` + /// properties are updated to reflect this. `content` will be set to what + /// remains of the template starting with the token. `range` will be set to + /// the start of the token within the template. + /// + /// - Parameter tokenChars: List of token start characters to search for. + /// - Returns: The found token start character, together with the content + /// before the token, or nil of no token start was found. func scanForTokenStart(_ tokenChars: [Unicode.Scalar]) -> (Unicode.Scalar, String)? { var foundBrace = false From 88bec575a5215399da59e7dc98e6d3b9369f2f0a Mon Sep 17 00:00:00 2001 From: David Jennes Date: Tue, 25 Sep 2018 03:01:14 +0200 Subject: [PATCH 71/81] Compile with Swift 4.2 if possible t t t --- Package.swift | 10 +++++----- Package@swift-4.2.swift | 23 +++++++++++++++++++++++ Stencil.podspec.json | 2 +- 3 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 Package@swift-4.2.swift diff --git a/Package.swift b/Package.swift index a0b49da..2dd6977 100644 --- a/Package.swift +++ b/Package.swift @@ -1,22 +1,22 @@ -// swift-tools-version:4.0 +// swift-tools-version:4.1 import PackageDescription let package = Package( name: "Stencil", products: [ - .library(name: "Stencil", targets: ["Stencil"]), + .library(name: "Stencil", targets: ["Stencil"]) ], dependencies: [ .package(url: "https://github.com/kylef/PathKit.git", from: "0.9.0"), - .package(url: "https://github.com/kylef/Spectre.git", from: "0.9.0"), + .package(url: "https://github.com/kylef/Spectre.git", from: "0.9.0") ], targets: [ .target(name: "Stencil", dependencies: [ - "PathKit", + "PathKit" ], path: "Sources"), .testTarget(name: "StencilTests", dependencies: [ "Stencil", - "Spectre", + "Spectre" ]) ] ) diff --git a/Package@swift-4.2.swift b/Package@swift-4.2.swift new file mode 100644 index 0000000..b8312ca --- /dev/null +++ b/Package@swift-4.2.swift @@ -0,0 +1,23 @@ +// swift-tools-version:4.2 +import PackageDescription + +let package = Package( + name: "Stencil", + products: [ + .library(name: "Stencil", targets: ["Stencil"]) + ], + dependencies: [ + .package(url: "https://github.com/kylef/PathKit.git", from: "0.9.0"), + .package(url: "https://github.com/kylef/Spectre.git", from: "0.9.0") + ], + targets: [ + .target(name: "Stencil", dependencies: [ + "PathKit" + ], path: "Sources"), + .testTarget(name: "StencilTests", dependencies: [ + "Stencil", + "Spectre" + ]) + ], + swiftLanguageVersions: [.v4, .v4_2] +) diff --git a/Stencil.podspec.json b/Stencil.podspec.json index a0dde4c..d9d7c70 100644 --- a/Stencil.podspec.json +++ b/Stencil.podspec.json @@ -24,7 +24,7 @@ "tvos": "9.0" }, "cocoapods_version": "1.4.0", - "swift_version": "4.1", + "swift_version": "4.2", "requires_arc": true, "dependencies": { "PathKit": [ From 535a8061d91185ce99b76dae39ada60265639065 Mon Sep 17 00:00:00 2001 From: David Jennes Date: Tue, 25 Sep 2018 03:05:19 +0200 Subject: [PATCH 72/81] Match old Changelog section names t --- CHANGELOG.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9645027..cb0e1cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,7 @@ ## Master -### Bug Fixes - -- Now accessing undefined keys in NSObject does not cause runtime crash and instead renders empty string. - [Ilya Puchka](https://github.com/ilyapuchka) - [#234](https://github.com/stencilproject/Stencil/pull/234) -- `for` tag: When iterating over a dictionary the keys will now always be sorted (in an ascending order) to ensure consistent output generation. - [David Jennes](https://github.com/djbe) - [#240](https://github.com/stencilproject/Stencil/pull/240) - -### Breaking Changes +### Breaking - Now requires Swift 4.1 or newer. [Yonas Kolb](https://github.com/yonaskolb) @@ -23,7 +14,7 @@ [Ilya Puchka](https://github.com/ilyapuchka) [#160](https://github.com/stencilproject/Stencil/pull/160) -### New Features +### Enhancements - Now you can conditionally render variables with `{{ variable if condition }}`, which is a shorthand for `{% if condition %}{{ variable }}{% endif %}`. You can also use `else` like `{{ variable1 if condition else variable2 }}`, which is a shorthand for `{% if condition %}{{ variable1 }}{% else %}{{ variable2 }}{% endif %}` [Ilya Puchka](https://github.com/ilyapuchka) @@ -32,6 +23,18 @@ [Ilya Puchka](https://github.com/ilyapuchka) [#245](https://github.com/stencilproject/Stencil/pull/245) +### Bug Fixes + +- Fixed the performance issues introduced in Stencil 0.12 with the error log improvements. + [Ilya Puchka](https://github.com/ilyapuchka) + [#230](https://github.com/stencilproject/Stencil/pull/230) +- Now accessing undefined keys in NSObject does not cause runtime crash and instead renders empty string. + [Ilya Puchka](https://github.com/ilyapuchka) + [#234](https://github.com/stencilproject/Stencil/pull/234) +- `for` tag: When iterating over a dictionary the keys will now always be sorted (in an ascending order) to ensure consistent output generation. + [David Jennes](https://github.com/djbe) + [#240](https://github.com/stencilproject/Stencil/pull/240) + ### Internal Changes - Updated the codebase to use Swift 4 features. From 27608432367759b2614ecdb663579884c658409d Mon Sep 17 00:00:00 2001 From: David Jennes Date: Tue, 25 Sep 2018 03:17:44 +0200 Subject: [PATCH 73/81] Update some old refs --- LICENSE | 3 +-- docs/_templates/sidebar_intro.html | 2 +- docs/installation.rst | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/LICENSE b/LICENSE index a8c6d13..8aa8286 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014, Kyle Fuller +Copyright (c) 2018, Kyle Fuller All rights reserved. Redistribution and use in source and binary forms, with or without @@ -21,4 +21,3 @@ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - diff --git a/docs/_templates/sidebar_intro.html b/docs/_templates/sidebar_intro.html index 6e5f6d2..d5bd29f 100644 --- a/docs/_templates/sidebar_intro.html +++ b/docs/_templates/sidebar_intro.html @@ -2,7 +2,7 @@

diff --git a/docs/installation.rst b/docs/installation.rst index bc3fad6..f3a58a5 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -14,7 +14,7 @@ dependencies inside ``Package.swift``. let package = Package( name: "MyApplication", dependencies: [ - .Package(url: "https://github.com/kylef/Stencil.git", majorVersion: 0, minor: 8), + .Package(url: "https://github.com/stencilproject/Stencil.git", majorVersion: 0, minor: 12), ] ) @@ -37,7 +37,7 @@ Carthage .. code-block:: text - github "kylef/Stencil" ~> 0.8.0 + github "stencilproject/Stencil" ~> 0.12.1 2) Checkout your dependencies, generate the Stencil Xcode project, and then use Carthage to build Stencil: From 42e415a9bf26e897ddcd0350b42dbddfe0d2d3fd Mon Sep 17 00:00:00 2001 From: David Jennes Date: Tue, 25 Sep 2018 03:18:14 +0200 Subject: [PATCH 74/81] Version 0.13.0 --- CHANGELOG.md | 2 +- Stencil.podspec.json | 4 ++-- docs/conf.py | 4 ++-- docs/installation.rst | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb0e1cc..ff5407f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Stencil Changelog -## Master +## 0.13.0 ### Breaking diff --git a/Stencil.podspec.json b/Stencil.podspec.json index d9d7c70..54ecd20 100644 --- a/Stencil.podspec.json +++ b/Stencil.podspec.json @@ -1,6 +1,6 @@ { "name": "Stencil", - "version": "0.12.1", + "version": "0.13.0", "summary": "Stencil is a simple and powerful template language for Swift.", "homepage": "https://stencil.fuller.li", "license": { @@ -13,7 +13,7 @@ "social_media_url": "https://twitter.com/kylefuller", "source": { "git": "https://github.com/stencilproject/Stencil.git", - "tag": "0.12.1" + "tag": "0.13.0" }, "source_files": [ "Sources/*.swift" diff --git a/docs/conf.py b/docs/conf.py index 4d69c8d..bf3e798 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -58,9 +58,9 @@ author = 'Kyle Fuller' # built documents. # # The short X.Y version. -version = '0.12.1' +version = '0.13.0' # The full version, including alpha/beta/rc tags. -release = '0.12.1' +release = '0.13.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/installation.rst b/docs/installation.rst index f3a58a5..1457752 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -14,7 +14,7 @@ dependencies inside ``Package.swift``. let package = Package( name: "MyApplication", dependencies: [ - .Package(url: "https://github.com/stencilproject/Stencil.git", majorVersion: 0, minor: 12), + .Package(url: "https://github.com/stencilproject/Stencil.git", majorVersion: 0, minor: 13), ] ) @@ -26,7 +26,7 @@ If you're using CocoaPods, you can add Stencil to your ``Podfile`` and then run .. code-block:: ruby - pod 'Stencil', '~> 0.8.0' + pod 'Stencil', '~> 0.13.0' Carthage -------- @@ -37,7 +37,7 @@ Carthage .. code-block:: text - github "stencilproject/Stencil" ~> 0.12.1 + github "stencilproject/Stencil" ~> 0.13.0 2) Checkout your dependencies, generate the Stencil Xcode project, and then use Carthage to build Stencil: From c7dbba41a5cf7ab5a5bdfb5f7855170d05c5f031 Mon Sep 17 00:00:00 2001 From: David Jennes Date: Wed, 26 Sep 2018 00:52:18 +0200 Subject: [PATCH 75/81] Fix cocoapods min. version --- Stencil.podspec.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Stencil.podspec.json b/Stencil.podspec.json index 54ecd20..63111e0 100644 --- a/Stencil.podspec.json +++ b/Stencil.podspec.json @@ -23,7 +23,7 @@ "osx": "10.9", "tvos": "9.0" }, - "cocoapods_version": "1.4.0", + "cocoapods_version": ">= 1.4.0", "swift_version": "4.2", "requires_arc": true, "dependencies": { From 2e677551188e1e8bc5e86aec077b05ab404fc0ef Mon Sep 17 00:00:00 2001 From: David Jennes Date: Wed, 26 Sep 2018 03:06:49 +0200 Subject: [PATCH 76/81] Fix a bug where tokens without spaces were parsed incorrectly --- Sources/Lexer.swift | 2 +- Tests/StencilTests/LexerSpec.swift | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Sources/Lexer.swift b/Sources/Lexer.swift index 015b7d7..f6fc426 100644 --- a/Sources/Lexer.swift +++ b/Sources/Lexer.swift @@ -146,7 +146,7 @@ class Scanner { for (index, char) in content.unicodeScalars.enumerated() { if foundChar && char == Scanner.tokenEndDelimiter { - let result = String(content.prefix(index)) + let result = String(content.prefix(index + 1)) content = String(content.dropFirst(index + 1)) range = range.upperBound.. Date: Wed, 26 Sep 2018 03:10:53 +0200 Subject: [PATCH 77/81] Changelog entry --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff5407f..2fe081b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Stencil Changelog +## Master + +### Bug Fixes + +- Fixed a bug in Stencil 0.13 where tags without spaces were incorrectly parsed. + [David Jennes](https://github.com/djbe) + [#252](https://github.com/stencilproject/Stencil/pull/252) + + ## 0.13.0 ### Breaking From 8cceac921aa68367840722152f71ccd2bc4d5ffe Mon Sep 17 00:00:00 2001 From: David Jennes Date: Wed, 26 Sep 2018 03:20:08 +0200 Subject: [PATCH 78/81] Avoid swift installation on macOS image --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9240617..5f44de5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ matrix: include: - os: osx osx_image: xcode9.4 - env: SWIFT_VERSION=4.1 + env: SWIFT_VERSION=4.0 - os: osx osx_image: xcode10 env: SWIFT_VERSION=4.2 From 8eae79dbffa14fcbef1d2cb135afe9cbd2ed2c87 Mon Sep 17 00:00:00 2001 From: David Jennes Date: Wed, 26 Sep 2018 03:22:43 +0200 Subject: [PATCH 79/81] Version 0.13.1 --- CHANGELOG.md | 2 +- Stencil.podspec.json | 4 ++-- docs/conf.py | 4 ++-- docs/installation.rst | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fe081b..8ed41af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Stencil Changelog -## Master +## 0.13.1 ### Bug Fixes diff --git a/Stencil.podspec.json b/Stencil.podspec.json index 63111e0..e667dc4 100644 --- a/Stencil.podspec.json +++ b/Stencil.podspec.json @@ -1,6 +1,6 @@ { "name": "Stencil", - "version": "0.13.0", + "version": "0.13.1", "summary": "Stencil is a simple and powerful template language for Swift.", "homepage": "https://stencil.fuller.li", "license": { @@ -13,7 +13,7 @@ "social_media_url": "https://twitter.com/kylefuller", "source": { "git": "https://github.com/stencilproject/Stencil.git", - "tag": "0.13.0" + "tag": "0.13.1" }, "source_files": [ "Sources/*.swift" diff --git a/docs/conf.py b/docs/conf.py index bf3e798..aefffdf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -58,9 +58,9 @@ author = 'Kyle Fuller' # built documents. # # The short X.Y version. -version = '0.13.0' +version = '0.13.1' # The full version, including alpha/beta/rc tags. -release = '0.13.0' +release = '0.13.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/installation.rst b/docs/installation.rst index 1457752..75c71e5 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -26,7 +26,7 @@ If you're using CocoaPods, you can add Stencil to your ``Podfile`` and then run .. code-block:: ruby - pod 'Stencil', '~> 0.13.0' + pod 'Stencil', '~> 0.13.1' Carthage -------- @@ -37,7 +37,7 @@ Carthage .. code-block:: text - github "stencilproject/Stencil" ~> 0.13.0 + github "stencilproject/Stencil" ~> 0.13.1 2) Checkout your dependencies, generate the Stencil Xcode project, and then use Carthage to build Stencil: From 0e9a78d6584e3812cd9c09494d5c7b483e8f533c Mon Sep 17 00:00:00 2001 From: David Jennes Date: Wed, 26 Sep 2018 03:26:27 +0200 Subject: [PATCH 80/81] Revert change (sorry!) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5f44de5..9240617 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ matrix: include: - os: osx osx_image: xcode9.4 - env: SWIFT_VERSION=4.0 + env: SWIFT_VERSION=4.1 - os: osx osx_image: xcode10 env: SWIFT_VERSION=4.2 From 9a6ba94d7d5f6abe620d530e7936535fe4d0ff24 Mon Sep 17 00:00:00 2001 From: David Jennes Date: Wed, 26 Sep 2018 13:10:56 +0200 Subject: [PATCH 81/81] Reset changelog --- CHANGELOG.md | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ed41af..2a2f9d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Stencil Changelog +## Master + +### Breaking + +_None_ + +### Enhancements + +_None_ + +### Deprecations + +_None_ + +### Bug Fixes + +_None_ + +### Internal Changes + +_None_ + + ## 0.13.1 ### Bug Fixes @@ -16,15 +39,15 @@ - Now requires Swift 4.1 or newer. [Yonas Kolb](https://github.com/yonaskolb) [#228](https://github.com/stencilproject/Stencil/pull/228) + +### Enhancements + - You can now use parentheses in boolean expressions to change operator precedence. [Ilya Puchka](https://github.com/ilyapuchka) [#165](https://github.com/stencilproject/Stencil/pull/165) - Added method to add boolean filters with their negative counterparts. [Ilya Puchka](https://github.com/ilyapuchka) [#160](https://github.com/stencilproject/Stencil/pull/160) - -### Enhancements - - Now you can conditionally render variables with `{{ variable if condition }}`, which is a shorthand for `{% if condition %}{{ variable }}{% endif %}`. You can also use `else` like `{{ variable1 if condition else variable2 }}`, which is a shorthand for `{% if condition %}{{ variable1 }}{% else %}{{ variable2 }}{% endif %}` [Ilya Puchka](https://github.com/ilyapuchka) [#243](https://github.com/stencilproject/Stencil/pull/243)