Implement trim whitespace
This commit is contained in:
@@ -4,6 +4,8 @@ public struct Environment {
|
||||
public let templateClass: Template.Type
|
||||
/// List of registered extensions
|
||||
public var extensions: [Extension]
|
||||
/// How to handle whitespace
|
||||
public var trimBehaviour: TrimBehaviour
|
||||
/// Mechanism for loading new files
|
||||
public var loader: Loader?
|
||||
|
||||
@@ -13,14 +15,17 @@ public struct Environment {
|
||||
/// - loader: Mechanism for loading new files
|
||||
/// - extensions: List of extension containers
|
||||
/// - templateClass: Class for newly loaded templates
|
||||
/// - trimBehaviour: How to handle whitespace
|
||||
public init(
|
||||
loader: Loader? = nil,
|
||||
extensions: [Extension] = [],
|
||||
templateClass: Template.Type = Template.self
|
||||
templateClass: Template.Type = Template.self,
|
||||
trimBehaviour: TrimBehaviour = .nothing
|
||||
) {
|
||||
self.templateClass = templateClass
|
||||
self.loader = loader
|
||||
self.extensions = extensions + [DefaultExtension()]
|
||||
self.trimBehaviour = trimBehaviour
|
||||
}
|
||||
|
||||
/// Load a template with the given name
|
||||
|
||||
@@ -11,6 +11,9 @@ struct Lexer {
|
||||
/// `{` character, for example `{{`, `{%`, `{#`, ...
|
||||
private static let tokenChars: [Unicode.Scalar] = ["{", "%", "#"]
|
||||
|
||||
/// The minimum length of a tag
|
||||
private static let tagLength = 2
|
||||
|
||||
/// 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] = [
|
||||
@@ -19,6 +22,12 @@ struct Lexer {
|
||||
"#": "#"
|
||||
]
|
||||
|
||||
/// Characters controlling whitespace trimming behaviour
|
||||
private static let behaviourMap: [Character: WhitespaceBehaviour.Behaviour] = [
|
||||
"+": .keep,
|
||||
"-": .trim
|
||||
]
|
||||
|
||||
init(templateName: String? = nil, templateString: String) {
|
||||
self.templateName = templateName
|
||||
self.templateString = templateString
|
||||
@@ -30,6 +39,16 @@ struct Lexer {
|
||||
}
|
||||
}
|
||||
|
||||
private func behaviour(string: String, tagLength: Int) -> WhitespaceBehaviour {
|
||||
let leftIndex = string.index(string.startIndex, offsetBy: tagLength, limitedBy: string.endIndex)
|
||||
let rightIndex = string.index(string.endIndex, offsetBy: -(tagLength + 1), limitedBy: string.startIndex)
|
||||
|
||||
return WhitespaceBehaviour(
|
||||
leading: Self.behaviourMap[leftIndex.map { string[$0] } ?? " "] ?? .unspecified,
|
||||
trailing: Self.behaviourMap[rightIndex.map { string[$0] } ?? " "] ?? .unspecified
|
||||
)
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -40,9 +59,9 @@ struct Lexer {
|
||||
/// - range: The range within the template content, used for smart
|
||||
/// error reporting
|
||||
func createToken(string: String, at range: Range<String.Index>) -> Token {
|
||||
func strip() -> String {
|
||||
guard string.count > 4 else { return "" }
|
||||
let trimmed = String(string.dropFirst(2).dropLast(2))
|
||||
func strip(length: (Int, Int) = (Self.tagLength, Self.tagLength)) -> String {
|
||||
guard string.count > (length.0 + length.1) else { return "" }
|
||||
let trimmed = String(string.dropFirst(length.0).dropLast(length.1))
|
||||
.components(separatedBy: "\n")
|
||||
.filter { !$0.isEmpty }
|
||||
.map { $0.trim(character: " ") }
|
||||
@@ -51,7 +70,13 @@ struct Lexer {
|
||||
}
|
||||
|
||||
if string.hasPrefix("{{") || string.hasPrefix("{%") || string.hasPrefix("{#") {
|
||||
let value = strip()
|
||||
let behaviour = string.hasPrefix("{%") ? behaviour(string: string, tagLength: Self.tagLength) : .unspecified
|
||||
let stripLengths = (
|
||||
Self.tagLength + (behaviour.leading != .unspecified ? 1 : 0),
|
||||
Self.tagLength + (behaviour.trailing != .unspecified ? 1 : 0)
|
||||
)
|
||||
|
||||
let value = strip(length: stripLengths)
|
||||
let range = templateString.range(of: value, range: range) ?? range
|
||||
let location = rangeLocation(range)
|
||||
let sourceMap = SourceMap(filename: templateName, location: location)
|
||||
@@ -59,7 +84,7 @@ struct Lexer {
|
||||
if string.hasPrefix("{{") {
|
||||
return .variable(value: value, at: sourceMap)
|
||||
} else if string.hasPrefix("{%") {
|
||||
return .block(value: value, at: sourceMap)
|
||||
return .block(value: strip(length: stripLengths), at: sourceMap, whitespace: behaviour)
|
||||
} else if string.hasPrefix("{#") {
|
||||
return .comment(value: value, at: sourceMap)
|
||||
}
|
||||
|
||||
@@ -41,14 +41,27 @@ public class SimpleNode: NodeType {
|
||||
public class TextNode: NodeType {
|
||||
public let text: String
|
||||
public let token: Token?
|
||||
public let trimBehaviour: TrimBehaviour
|
||||
|
||||
public init(text: String) {
|
||||
public init(text: String, trimBehaviour: TrimBehaviour = .nothing) {
|
||||
self.text = text
|
||||
self.token = nil
|
||||
self.trimBehaviour = trimBehaviour
|
||||
}
|
||||
|
||||
public func render(_ context: Context) throws -> String {
|
||||
self.text
|
||||
var string = self.text
|
||||
if trimBehaviour.leading != .nothing, !string.isEmpty {
|
||||
let range = NSRange(..<string.endIndex, in: string)
|
||||
string = TrimBehaviour.leadingRegex(trim: trimBehaviour.leading)
|
||||
.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: "")
|
||||
}
|
||||
if trimBehaviour.trailing != .nothing, !string.isEmpty {
|
||||
let range = NSRange(..<string.endIndex, in: string)
|
||||
string = TrimBehaviour.trailingRegex(trim: trimBehaviour.trailing)
|
||||
.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: "")
|
||||
}
|
||||
return string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ public class TokenParser {
|
||||
|
||||
fileprivate var tokens: [Token]
|
||||
fileprivate let environment: Environment
|
||||
fileprivate var previousWhiteSpace: WhitespaceBehaviour.Behaviour?
|
||||
|
||||
/// Simple initializer
|
||||
public init(tokens: [Token], environment: Environment) {
|
||||
@@ -41,10 +42,12 @@ public class TokenParser {
|
||||
|
||||
switch token.kind {
|
||||
case .text:
|
||||
nodes.append(TextNode(text: token.contents))
|
||||
nodes.append(TextNode(text: token.contents, trimBehaviour: trimBehaviour))
|
||||
case .variable:
|
||||
previousWhiteSpace = nil
|
||||
try nodes.append(VariableNode.parse(self, token: token))
|
||||
case .block:
|
||||
previousWhiteSpace = token.whitespace?.trailing
|
||||
if let parseUntil = parseUntil, parseUntil(self, token) {
|
||||
prependToken(token)
|
||||
return nodes
|
||||
@@ -60,6 +63,7 @@ public class TokenParser {
|
||||
}
|
||||
}
|
||||
case .comment:
|
||||
previousWhiteSpace = nil
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -76,6 +80,10 @@ public class TokenParser {
|
||||
return nil
|
||||
}
|
||||
|
||||
func peekWhitespace() -> WhitespaceBehaviour.Behaviour? {
|
||||
tokens.first?.whitespace?.leading
|
||||
}
|
||||
|
||||
/// Insert a token
|
||||
public func prependToken(_ token: Token) {
|
||||
tokens.insert(token, at: 0)
|
||||
@@ -95,6 +103,27 @@ public class TokenParser {
|
||||
public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable {
|
||||
try environment.compileResolvable(token, containedIn: containingToken)
|
||||
}
|
||||
|
||||
private var trimBehaviour: TrimBehaviour {
|
||||
var behaviour: TrimBehaviour = .nothing
|
||||
|
||||
if let leading = previousWhiteSpace {
|
||||
if leading == .unspecified {
|
||||
behaviour.leading = environment.trimBehaviour.trailing
|
||||
} else {
|
||||
behaviour.leading = leading == .trim ? .whitespaceAndNewLines : .nothing
|
||||
}
|
||||
}
|
||||
if let trailing = peekWhitespace() {
|
||||
if trailing == .unspecified {
|
||||
behaviour.trailing = environment.trimBehaviour.leading
|
||||
} else {
|
||||
behaviour.trailing = trailing == .trim ? .whitespaceAndNewLines : .nothing
|
||||
}
|
||||
}
|
||||
|
||||
return behaviour
|
||||
}
|
||||
}
|
||||
|
||||
extension Environment {
|
||||
|
||||
@@ -79,6 +79,19 @@ public struct SourceMap: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct WhitespaceBehaviour: Equatable {
|
||||
public enum Behaviour {
|
||||
case unspecified
|
||||
case trim
|
||||
case keep
|
||||
}
|
||||
|
||||
let leading: Behaviour
|
||||
let trailing: Behaviour
|
||||
|
||||
public static let unspecified = WhitespaceBehaviour(leading: .unspecified, trailing: .unspecified)
|
||||
}
|
||||
|
||||
public class Token: Equatable {
|
||||
public enum Kind: Equatable {
|
||||
/// A token representing a piece of text.
|
||||
@@ -94,14 +107,16 @@ public class Token: Equatable {
|
||||
public let contents: String
|
||||
public let kind: Kind
|
||||
public let sourceMap: SourceMap
|
||||
public var whitespace: WhitespaceBehaviour?
|
||||
|
||||
/// Returns the underlying value as an array seperated by spaces
|
||||
public private(set) lazy var components: [String] = self.contents.smartSplit()
|
||||
|
||||
init(contents: String, kind: Kind, sourceMap: SourceMap) {
|
||||
init(contents: String, kind: Kind, sourceMap: SourceMap, whitespace: WhitespaceBehaviour? = nil) {
|
||||
self.contents = contents
|
||||
self.kind = kind
|
||||
self.sourceMap = sourceMap
|
||||
self.whitespace = whitespace
|
||||
}
|
||||
|
||||
/// A token representing a piece of text.
|
||||
@@ -120,8 +135,12 @@ public class Token: Equatable {
|
||||
}
|
||||
|
||||
/// A token representing a template block.
|
||||
public static func block(value: String, at sourceMap: SourceMap) -> Token {
|
||||
Token(contents: value, kind: .block, sourceMap: sourceMap)
|
||||
public static func block(
|
||||
value: String,
|
||||
at sourceMap: SourceMap,
|
||||
whitespace: WhitespaceBehaviour = .unspecified
|
||||
) -> Token {
|
||||
Token(contents: value, kind: .block, sourceMap: sourceMap, whitespace: whitespace)
|
||||
}
|
||||
|
||||
public static func == (lhs: Token, rhs: Token) -> Bool {
|
||||
|
||||
70
Sources/Stencil/TrimBehaviour.swift
Normal file
70
Sources/Stencil/TrimBehaviour.swift
Normal file
@@ -0,0 +1,70 @@
|
||||
import Foundation
|
||||
|
||||
public struct TrimBehaviour: Equatable {
|
||||
var leading: Trim
|
||||
var trailing: Trim
|
||||
|
||||
public enum Trim {
|
||||
/// nothing
|
||||
case nothing
|
||||
|
||||
/// tabs and spaces
|
||||
case whitespace
|
||||
|
||||
/// tabs and spaces and a single new line
|
||||
case whitespaceAndOneNewLine
|
||||
|
||||
/// all tabs spaces and newlines
|
||||
case whitespaceAndNewLines
|
||||
}
|
||||
|
||||
public init(leading: Trim, trailing: Trim) {
|
||||
self.leading = leading
|
||||
self.trailing = trailing
|
||||
}
|
||||
|
||||
/// doesn't touch newlines
|
||||
public static let nothing = TrimBehaviour(leading: .nothing, trailing: .nothing)
|
||||
|
||||
/// removes whitespace before a block and whitespace and a single newline after a block
|
||||
public static let smart = TrimBehaviour(leading: .whitespace, trailing: .whitespaceAndOneNewLine)
|
||||
|
||||
/// removes all whitespace and newlines before and after a block
|
||||
public static let all = TrimBehaviour(leading: .whitespaceAndNewLines, trailing: .whitespaceAndNewLines)
|
||||
|
||||
static func leadingRegex(trim: Trim) -> NSRegularExpression {
|
||||
switch trim {
|
||||
case .nothing:
|
||||
fatalError("No RegularExpression for none")
|
||||
case .whitespace:
|
||||
return Self.leadingWhitespace
|
||||
case .whitespaceAndOneNewLine:
|
||||
return Self.leadingWhitespaceAndOneNewLine
|
||||
case .whitespaceAndNewLines:
|
||||
return Self.leadingWhitespaceAndNewlines
|
||||
}
|
||||
}
|
||||
|
||||
static func trailingRegex(trim: Trim) -> NSRegularExpression {
|
||||
switch trim {
|
||||
case .nothing:
|
||||
fatalError("No RegularExpression for none")
|
||||
case .whitespace:
|
||||
return Self.trailingWhitespace
|
||||
case .whitespaceAndOneNewLine:
|
||||
return Self.trailingWhitespaceAndOneNewLine
|
||||
case .whitespaceAndNewLines:
|
||||
return Self.trailingWhitespaceAndNewLines
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable force_try
|
||||
private static let leadingWhitespaceAndNewlines = try! NSRegularExpression(pattern: "^\\s+")
|
||||
private static let trailingWhitespaceAndNewLines = try! NSRegularExpression(pattern: "\\s+$")
|
||||
|
||||
private static let leadingWhitespaceAndOneNewLine = try! NSRegularExpression(pattern: "^[ \t]*\n")
|
||||
private static let trailingWhitespaceAndOneNewLine = try! NSRegularExpression(pattern: "\n[ \t]*$")
|
||||
|
||||
private static let leadingWhitespace = try! NSRegularExpression(pattern: "^[ \t]*")
|
||||
private static let trailingWhitespace = try! NSRegularExpression(pattern: "[ \t]*$")
|
||||
}
|
||||
@@ -51,9 +51,9 @@ final class LexerTests: XCTestCase {
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 3
|
||||
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))
|
||||
try expect(tokens[0]) == .text(value: "My name is ", at: makeSourceMap("My name is ", for: lexer))
|
||||
try expect(tokens[1]) == .variable(value: "myname", at: makeSourceMap("myname", for: lexer))
|
||||
try expect(tokens[2]) == .text(value: ".", at: makeSourceMap(".", for: lexer))
|
||||
}
|
||||
|
||||
func testVariablesWithoutBeingGreedy() throws {
|
||||
@@ -62,8 +62,8 @@ final class LexerTests: XCTestCase {
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 2
|
||||
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))
|
||||
try expect(tokens[0]) == .variable(value: "thing", at: makeSourceMap("thing", for: lexer))
|
||||
try expect(tokens[1]) == .variable(value: "name", at: makeSourceMap("name", for: lexer))
|
||||
}
|
||||
|
||||
func testUnclosedBlock() throws {
|
||||
@@ -98,11 +98,26 @@ final class LexerTests: XCTestCase {
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
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))
|
||||
try expect(tokens[0]) == .text(value: "My name is ", at: makeSourceMap("My name is", for: lexer))
|
||||
try expect(tokens[1]) == .block(value: "if name and name", at: makeSourceMap("{%", for: lexer))
|
||||
try expect(tokens[2]) == .variable(value: "name", at: makeSourceMap("name", for: lexer, options: .backwards))
|
||||
try expect(tokens[3]) == .block(value: "endif", at: makeSourceMap("endif", for: lexer))
|
||||
try expect(tokens[4]) == .text(value: ".", at: makeSourceMap(".", for: lexer))
|
||||
}
|
||||
|
||||
func testTrimSymbols() throws {
|
||||
let fBlock = "if hello"
|
||||
let sBlock = "ta da"
|
||||
let lexer = Lexer(templateString: "{%+ \(fBlock) -%}{% \(sBlock) -%}")
|
||||
let tokens = lexer.tokenize()
|
||||
let behaviours = (
|
||||
WhitespaceBehaviour(leading: .keep, trailing: .trim),
|
||||
WhitespaceBehaviour(leading: .unspecified, trailing: .trim)
|
||||
)
|
||||
|
||||
try expect(tokens.count) == 2
|
||||
try expect(tokens[0]) == .block(value: fBlock, at: makeSourceMap(fBlock, for: lexer), whitespace: behaviours.0)
|
||||
try expect(tokens[1]) == .block(value: sBlock, at: makeSourceMap(sBlock, for: lexer), whitespace: behaviours.1)
|
||||
}
|
||||
|
||||
func testEscapeSequence() throws {
|
||||
@@ -111,11 +126,11 @@ final class LexerTests: XCTestCase {
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
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))
|
||||
try expect(tokens[0]) == .text(value: "class Some ", at: makeSourceMap("class Some ", for: lexer))
|
||||
try expect(tokens[1]) == .variable(value: "'{'", at: makeSourceMap("'{'", for: lexer))
|
||||
try expect(tokens[2]) == .block(value: "if true", at: makeSourceMap("if true", for: lexer))
|
||||
try expect(tokens[3]) == .variable(value: "stuff", at: makeSourceMap("stuff", for: lexer))
|
||||
try expect(tokens[4]) == .block(value: "endif", at: makeSourceMap("endif", for: lexer))
|
||||
}
|
||||
|
||||
func testPerformance() throws {
|
||||
|
||||
@@ -14,6 +14,48 @@ final class NodeTests: XCTestCase {
|
||||
let node = TextNode(text: "Hello World")
|
||||
try expect(try node.render(self.context)) == "Hello World"
|
||||
}
|
||||
it("Trims leading whitespace") {
|
||||
let text = " \n Some text "
|
||||
let trimBehaviour = TrimBehaviour(leading: .whitespace, trailing: .nothing)
|
||||
let node = TextNode(text: text, trimBehaviour: trimBehaviour)
|
||||
try expect(try node.render(self.context)) == "\n Some text "
|
||||
}
|
||||
it("Trims leading whitespace and one newline") {
|
||||
let text = "\n\n Some text "
|
||||
let trimBehaviour = TrimBehaviour(leading: .whitespaceAndOneNewLine, trailing: .nothing)
|
||||
let node = TextNode(text: text, trimBehaviour: trimBehaviour)
|
||||
try expect(try node.render(self.context)) == "\n Some text "
|
||||
}
|
||||
it("Trims leading whitespace and one newline") {
|
||||
let text = "\n\n Some text "
|
||||
let trimBehaviour = TrimBehaviour(leading: .whitespaceAndNewLines, trailing: .nothing)
|
||||
let node = TextNode(text: text, trimBehaviour: trimBehaviour)
|
||||
try expect(try node.render(self.context)) == "Some text "
|
||||
}
|
||||
it("Trims trailing whitespace") {
|
||||
let text = " Some text \n"
|
||||
let trimBehaviour = TrimBehaviour(leading: .nothing, trailing: .whitespace)
|
||||
let node = TextNode(text: text, trimBehaviour: trimBehaviour)
|
||||
try expect(try node.render(self.context)) == " Some text\n"
|
||||
}
|
||||
it("Trims trailing whitespace and one newline") {
|
||||
let text = " Some text \n \n "
|
||||
let trimBehaviour = TrimBehaviour(leading: .nothing, trailing: .whitespaceAndOneNewLine)
|
||||
let node = TextNode(text: text, trimBehaviour: trimBehaviour)
|
||||
try expect(try node.render(self.context)) == " Some text \n "
|
||||
}
|
||||
it("Trims trailing whitespace and newlines") {
|
||||
let text = " Some text \n \n "
|
||||
let trimBehaviour = TrimBehaviour(leading: .nothing, trailing: .whitespaceAndNewLines)
|
||||
let node = TextNode(text: text, trimBehaviour: trimBehaviour)
|
||||
try expect(try node.render(self.context)) == " Some text"
|
||||
}
|
||||
it("Trims all whitespace") {
|
||||
let text = " \n \nSome text \n "
|
||||
let trimBehaviour = TrimBehaviour(leading: .whitespaceAndNewLines, trailing: .whitespaceAndNewLines)
|
||||
let node = TextNode(text: text, trimBehaviour: trimBehaviour)
|
||||
try expect(try node.render(self.context)) == "Some text"
|
||||
}
|
||||
}
|
||||
|
||||
func testVariableNode() {
|
||||
|
||||
@@ -3,62 +3,77 @@ import Spectre
|
||||
import XCTest
|
||||
|
||||
final class TokenParserTests: XCTestCase {
|
||||
func testTokenParser() {
|
||||
it("can parse a text token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
.text(value: "Hello World", at: .unknown)
|
||||
], environment: Environment())
|
||||
func testTextToken() throws {
|
||||
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"
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.text) == "Hello World"
|
||||
}
|
||||
|
||||
func testVariableToken() throws {
|
||||
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"
|
||||
}
|
||||
|
||||
func testCommentToken() throws {
|
||||
let parser = TokenParser(tokens: [
|
||||
.comment(value: "Secret stuff!", at: .unknown)
|
||||
], environment: Environment())
|
||||
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.count) == 0
|
||||
}
|
||||
|
||||
func testTagToken() throws {
|
||||
let simpleExtension = Extension()
|
||||
simpleExtension.registerSimpleTag("known") { _ in
|
||||
""
|
||||
}
|
||||
|
||||
it("can parse a variable token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
.variable(value: "'name'", at: .unknown)
|
||||
], environment: Environment())
|
||||
let parser = TokenParser(tokens: [
|
||||
.block(value: "known", at: .unknown)
|
||||
], environment: Environment(extensions: [simpleExtension]))
|
||||
|
||||
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"
|
||||
}
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.count) == 1
|
||||
}
|
||||
|
||||
it("can parse a comment token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
.comment(value: "Secret stuff!", at: .unknown)
|
||||
], environment: Environment())
|
||||
func testErrorUnknownTag() throws {
|
||||
let tokens: [Token] = [.block(value: "unknown", at: .unknown)]
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.count) == 0
|
||||
}
|
||||
try expect(try parser.parse()).toThrow(TemplateSyntaxError(
|
||||
reason: "Unknown template tag 'unknown'",
|
||||
token: tokens.first
|
||||
))
|
||||
}
|
||||
|
||||
it("can parse a tag token") {
|
||||
let simpleExtension = Extension()
|
||||
simpleExtension.registerSimpleTag("known") { _ in
|
||||
""
|
||||
}
|
||||
func testTransformWhitespaceBehaviourToTrimBehaviour() throws {
|
||||
let simpleExtension = Extension()
|
||||
simpleExtension.registerSimpleTag("known") { _ in "" }
|
||||
|
||||
let parser = TokenParser(tokens: [
|
||||
.block(value: "known", at: .unknown)
|
||||
], environment: Environment(extensions: [simpleExtension]))
|
||||
let parser = TokenParser(tokens: [
|
||||
.block(value: "known", at: .unknown, whitespace: WhitespaceBehaviour(leading: .unspecified, trailing: .trim)),
|
||||
.text(value: " \nSome text ", at: .unknown),
|
||||
.block(value: "known", at: .unknown, whitespace: WhitespaceBehaviour(leading: .keep, trailing: .trim))
|
||||
], environment: Environment(extensions: [simpleExtension]))
|
||||
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.count) == 1
|
||||
}
|
||||
|
||||
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
|
||||
))
|
||||
}
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.count) == 3
|
||||
let textNode = nodes[1] as? TextNode
|
||||
try expect(textNode?.text) == " \nSome text "
|
||||
try expect(textNode?.trimBehaviour) == TrimBehaviour(leading: .whitespaceAndNewLines, trailing: .nothing)
|
||||
}
|
||||
}
|
||||
|
||||
137
Tests/StencilTests/TrimBehaviourSpec.swift
Normal file
137
Tests/StencilTests/TrimBehaviourSpec.swift
Normal file
@@ -0,0 +1,137 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import XCTest
|
||||
|
||||
final class TrimBehaviourTests: XCTestCase {
|
||||
func testSmartTrimCanRemoveNewlines() throws {
|
||||
let templateString = """
|
||||
{% for item in items %}
|
||||
- {{item}}
|
||||
{% endfor %}
|
||||
text
|
||||
"""
|
||||
|
||||
let context = ["items": ["item 1", "item 2"]]
|
||||
let template = Template(templateString: templateString, environment: .init(trimBehaviour: .smart))
|
||||
let result = try template.render(context)
|
||||
|
||||
// swiftlint:disable indentation_width
|
||||
try expect(result) == """
|
||||
- item 1
|
||||
- item 2
|
||||
text
|
||||
"""
|
||||
// swiftlint:enable indentation_width
|
||||
}
|
||||
|
||||
func testSmartTrimOnlyRemoveSingleNewlines() throws {
|
||||
let templateString = """
|
||||
{% for item in items %}
|
||||
|
||||
- {{item}}
|
||||
{% endfor %}
|
||||
text
|
||||
"""
|
||||
|
||||
let context = ["items": ["item 1", "item 2"]]
|
||||
let template = Template(templateString: templateString, environment: .init(trimBehaviour: .smart))
|
||||
let result = try template.render(context)
|
||||
|
||||
// swiftlint:disable indentation_width
|
||||
try expect(result) == """
|
||||
|
||||
- item 1
|
||||
|
||||
- item 2
|
||||
text
|
||||
"""
|
||||
// swiftlint:enable indentation_width
|
||||
}
|
||||
|
||||
func testSmartTrimCanRemoveNewlinesWhileKeepingWhitespace() throws {
|
||||
// swiftlint:disable indentation_width
|
||||
let templateString = """
|
||||
Items:
|
||||
{% for item in items %}
|
||||
- {{item}}
|
||||
{% endfor %}
|
||||
"""
|
||||
// swiftlint:enable indentation_width
|
||||
|
||||
let context = ["items": ["item 1", "item 2"]]
|
||||
let template = Template(templateString: templateString, environment: .init(trimBehaviour: .smart))
|
||||
let result = try template.render(context)
|
||||
|
||||
// swiftlint:disable indentation_width
|
||||
try expect(result) == """
|
||||
Items:
|
||||
- item 1
|
||||
- item 2
|
||||
|
||||
"""
|
||||
// swiftlint:enable indentation_width
|
||||
}
|
||||
|
||||
func testTrimSymbols() {
|
||||
it("Respects whitespace control symbols in for tags") {
|
||||
// swiftlint:disable indentation_width
|
||||
let template: Template = """
|
||||
{% for num in numbers -%}
|
||||
{{num}}
|
||||
{%- endfor %}
|
||||
"""
|
||||
// swiftlint:enable indentation_width
|
||||
let result = try template.render([ "numbers": Array(1...9) ])
|
||||
try expect(result) == "123456789"
|
||||
}
|
||||
it("Respects whitespace control symbols in if tags") {
|
||||
let template: Template = """
|
||||
{% if value -%}
|
||||
{{text}}
|
||||
{%- endif %}
|
||||
"""
|
||||
let result = try template.render([ "text": "hello", "value": true ])
|
||||
try expect(result) == "hello"
|
||||
}
|
||||
}
|
||||
|
||||
func testTrimSymbolsOverridingEnvironment() {
|
||||
let environment = Environment(trimBehaviour: .all)
|
||||
|
||||
it("respects whitespace control symbols in if tags") {
|
||||
// swiftlint:disable indentation_width
|
||||
let templateString = """
|
||||
{% if value +%}
|
||||
{{text}}
|
||||
{%+ endif %}
|
||||
|
||||
"""
|
||||
// swiftlint:enable indentation_width
|
||||
let template = Template(templateString: templateString, environment: environment)
|
||||
let result = try template.render([ "text": "hello", "value": true ])
|
||||
try expect(result) == "\n hello\n"
|
||||
}
|
||||
|
||||
it("can customize blocks on same line as text") {
|
||||
// swiftlint:disable indentation_width
|
||||
let templateString = """
|
||||
Items:{% for item in items +%}
|
||||
- {{item}}
|
||||
{%- endfor %}
|
||||
"""
|
||||
// swiftlint:enable indentation_width
|
||||
|
||||
let context = ["items": ["item 1", "item 2"]]
|
||||
let template = Template(templateString: templateString, environment: environment)
|
||||
let result = try template.render(context)
|
||||
|
||||
// swiftlint:disable indentation_width
|
||||
try expect(result) == """
|
||||
Items:
|
||||
- item 1
|
||||
- item 2
|
||||
"""
|
||||
// swiftlint:enable indentation_width
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user