Add support for Set Delimiters (#5)

* Added ParserState

* Add support for setting delimiters

* Add spec tests for setting delimiters

* swift format
This commit is contained in:
Adam Fowler
2021-03-18 12:43:36 +00:00
committed by GitHub
parent 66e9b23c47
commit 751126d729
3 changed files with 305 additions and 61 deletions

View File

@@ -4,52 +4,73 @@ extension HBMustacheTemplate {
case sectionCloseNameIncorrect
case unfinishedName
case expectedSectionEnd
case invalidSetDelimiter
}
struct ParserState {
var sectionName: String?
var newLine: Bool
var startDelimiter: String
var endDelimiter: String
init() {
sectionName = nil
newLine = true
startDelimiter = "{{"
endDelimiter = "}}"
}
func withSectionName(_ name: String) -> ParserState {
var newValue = self
newValue.sectionName = name
return newValue
}
func withDelimiters(start: String, end: String) -> ParserState {
var newValue = self
newValue.startDelimiter = start
newValue.endDelimiter = end
return newValue
}
func withDefaultDelimiters(start _: String, end _: String) -> ParserState {
var newValue = self
newValue.startDelimiter = "{{"
newValue.endDelimiter = "}}"
return newValue
}
}
/// parse mustache text to generate a list of tokens
static func parse(_ string: String) throws -> [Token] {
var parser = HBParser(string)
return try parse(&parser, sectionName: nil)
return try parse(&parser, state: .init())
}
/// parse section in mustache text
static func parse(_ parser: inout HBParser, sectionName: String?, newLine: Bool = true) throws -> [Token] {
static func parse(_ parser: inout HBParser, state: ParserState) throws -> [Token] {
var tokens: [Token] = []
var newLine = newLine
var state = state
var whiteSpaceBefore: String = ""
while !parser.reachedEnd() {
// if new line read whitespace
if newLine {
if state.newLine {
whiteSpaceBefore = parser.read(while: Set(" \t")).string
}
// read until we hit either a newline or "{"
let text = try parser.read(until: Set("{\n"), throwOnOverflow: false)
// if new line append all text read plus newline
let text = try readUntilDelimiterOrNewline(&parser, state: state)
// if we hit a newline add text
if parser.current() == "\n" {
tokens.append(.text(whiteSpaceBefore + text.string + "\n"))
newLine = true
tokens.append(.text(whiteSpaceBefore + text + "\n"))
state.newLine = true
parser.unsafeAdvance()
continue
} else if parser.current() == "{" {
parser.unsafeAdvance()
// if next character is not "{" then is normal text
if parser.current() != "{" {
}
// we have found a tag
// whatever text we found before the tag should be added as a token
if text.count > 0 {
tokens.append(.text(whiteSpaceBefore + text.string + "{"))
tokens.append(.text(whiteSpaceBefore + text))
whiteSpaceBefore = ""
newLine = false
}
continue
} else {
parser.unsafeAdvance()
}
}
// whatever text we found before the "{{" should be added
if text.count > 0 {
tokens.append(.text(whiteSpaceBefore + text.string))
whiteSpaceBefore = ""
newLine = false
state.newLine = false
}
// have we reached the end of the text
if parser.reachedEnd() {
@@ -60,8 +81,8 @@ extension HBMustacheTemplate {
case "#":
// section
parser.unsafeAdvance()
let (name, method) = try parseName(&parser)
if newLine, hasLineFinished(&parser) {
let (name, method) = try parseName(&parser, state: state)
if state.newLine, hasLineFinished(&parser) {
setNewLine = true
if parser.current() == "\n" {
parser.unsafeAdvance()
@@ -70,14 +91,14 @@ extension HBMustacheTemplate {
tokens.append(.text(whiteSpaceBefore))
whiteSpaceBefore = ""
}
let sectionTokens = try parse(&parser, sectionName: name, newLine: newLine)
let sectionTokens = try parse(&parser, state: state.withSectionName(name))
tokens.append(.section(name: name, method: method, template: HBMustacheTemplate(sectionTokens)))
case "^":
// inverted section
parser.unsafeAdvance()
let (name, method) = try parseName(&parser)
if newLine, hasLineFinished(&parser) {
let (name, method) = try parseName(&parser, state: state)
if state.newLine, hasLineFinished(&parser) {
setNewLine = true
if parser.current() == "\n" {
parser.unsafeAdvance()
@@ -86,17 +107,17 @@ extension HBMustacheTemplate {
tokens.append(.text(whiteSpaceBefore))
whiteSpaceBefore = ""
}
let sectionTokens = try parse(&parser, sectionName: name, newLine: newLine)
let sectionTokens = try parse(&parser, state: state.withSectionName(name))
tokens.append(.invertedSection(name: name, method: method, template: HBMustacheTemplate(sectionTokens)))
case "/":
// end of section
parser.unsafeAdvance()
let (name, _) = try parseName(&parser)
guard name == sectionName else {
let (name, _) = try parseName(&parser, state: state)
guard name == state.sectionName else {
throw Error.sectionCloseNameIncorrect
}
if newLine, hasLineFinished(&parser) {
if state.newLine, hasLineFinished(&parser) {
setNewLine = true
if parser.current() == "\n" {
parser.unsafeAdvance()
@@ -110,8 +131,8 @@ extension HBMustacheTemplate {
case "!":
// comment
parser.unsafeAdvance()
_ = try parseComment(&parser)
if newLine, hasLineFinished(&parser) {
_ = try parseComment(&parser, state: state)
if state.newLine, hasLineFinished(&parser) {
setNewLine = true
if !parser.reachedEnd() {
parser.unsafeAdvance()
@@ -125,7 +146,7 @@ extension HBMustacheTemplate {
whiteSpaceBefore = ""
}
parser.unsafeAdvance()
let (name, method) = try parseName(&parser)
let (name, method) = try parseName(&parser, state: state)
guard try parser.read("}") else { throw Error.unfinishedName }
tokens.append(.unescapedVariable(name: name, method: method))
@@ -136,17 +157,17 @@ extension HBMustacheTemplate {
whiteSpaceBefore = ""
}
parser.unsafeAdvance()
let (name, method) = try parseName(&parser)
let (name, method) = try parseName(&parser, state: state)
tokens.append(.unescapedVariable(name: name, method: method))
case ">":
// partial
parser.unsafeAdvance()
let (name, _) = try parseName(&parser)
let (name, _) = try parseName(&parser, state: state)
if whiteSpaceBefore.count > 0 {
tokens.append(.text(whiteSpaceBefore))
}
if newLine, hasLineFinished(&parser) {
if state.newLine, hasLineFinished(&parser) {
setNewLine = true
if parser.current() == "\n" {
parser.unsafeAdvance()
@@ -157,30 +178,68 @@ extension HBMustacheTemplate {
}
whiteSpaceBefore = ""
case "=":
// set delimiter
parser.unsafeAdvance()
state = try parserSetDelimiter(&parser, state: state)
if state.newLine, hasLineFinished(&parser) {
setNewLine = true
if !parser.reachedEnd() {
parser.unsafeAdvance()
}
}
default:
// variable
if whiteSpaceBefore.count > 0 {
tokens.append(.text(whiteSpaceBefore))
whiteSpaceBefore = ""
}
let (name, method) = try parseName(&parser)
let (name, method) = try parseName(&parser, state: state)
tokens.append(.variable(name: name, method: method))
}
newLine = setNewLine
state.newLine = setNewLine
}
// should never get here if reading section
guard sectionName == nil else {
guard state.sectionName == nil else {
throw Error.expectedSectionEnd
}
return tokens
}
/// read until we hit either the start delimiter of a tag or a newline
static func readUntilDelimiterOrNewline(_ parser: inout HBParser, state: ParserState) throws -> String {
var untilSet = Set("\n")
guard let delimiterFirstChar = state.startDelimiter.first,
let delimiterFirstScalar = delimiterFirstChar.unicodeScalars.first else { return "" }
var totalText = ""
untilSet.insert(delimiterFirstScalar)
while !parser.reachedEnd() {
// read until we hit either a newline or "{"
let text = try parser.read(until: untilSet, throwOnOverflow: false).string
totalText += text
// if new line append all text read plus newline
if parser.current() == "\n" {
break
} else if parser.current() == delimiterFirstScalar {
if try parser.read(state.startDelimiter) {
break
}
totalText += String(delimiterFirstScalar)
parser.unsafeAdvance()
}
}
return totalText
}
/// parse variable name
static func parseName(_ parser: inout HBParser) throws -> (String, String?) {
static func parseName(_ parser: inout HBParser, state: ParserState) throws -> (String, String?) {
parser.read(while: \.isWhitespace)
var text = parser.read(while: sectionNameChars)
parser.read(while: \.isWhitespace)
guard try parser.read("}"), try parser.read("}") else { throw Error.unfinishedName }
guard try parser.read(state.endDelimiter) else { throw Error.unfinishedName }
// does the name include brackets. If so this is a method call
let string = text.read(while: sectionNameCharsWithoutBrackets)
if text.reachedEnd() {
@@ -197,11 +256,23 @@ extension HBMustacheTemplate {
}
}
static func parseComment(_ parser: inout HBParser) throws -> String {
let text = try parser.read(untilString: "}}", throwOnOverflow: true, skipToEnd: true)
static func parseComment(_ parser: inout HBParser, state: ParserState) throws -> String {
let text = try parser.read(untilString: state.endDelimiter, throwOnOverflow: true, skipToEnd: true)
return text.string
}
static func parserSetDelimiter(_ parser: inout HBParser, state: ParserState) throws -> ParserState {
parser.read(while: \.isWhitespace)
let startDelimiter = try parser.read(until: \.isWhitespace).string
parser.read(while: \.isWhitespace)
let endDelimiter = try parser.read(until: { $0 == "=" || $0.isWhitespace }).string
parser.read(while: \.isWhitespace)
guard try parser.read("=") else { throw Error.invalidSetDelimiter }
guard try parser.read(state.endDelimiter) else { throw Error.invalidSetDelimiter }
guard startDelimiter.count > 0, endDelimiter.count > 0 else { throw Error.invalidSetDelimiter }
return state.withDelimiters(start: startDelimiter, end: endDelimiter)
}
static func hasLineFinished(_ parser: inout HBParser) -> Bool {
var parser2 = parser
if parser.reachedEnd() { return true }

View File

@@ -4,7 +4,7 @@ import XCTest
final class LibraryTests: XCTestCase {
func testDirectoryLoad() throws {
let fs = FileManager()
try fs.createDirectory(atPath: "templates", withIntermediateDirectories: false)
try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false)
let mustache = "<test>{{#value}}<value>{{.}}</value>{{/value}}</test>"
let data = Data(mustache.utf8)
defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates")) }

View File

@@ -10,6 +10,18 @@ func test(_ object: Any, _ template: String, _ expected: String) throws {
XCTAssertEqual(result, expected)
}
func testPartial(_ object: Any, _ template: String, _ partials: [String: String], _ expected: String) throws {
let library = HBMustacheLibrary()
let template = try HBMustacheTemplate(string: template)
library.register(template, named: "template")
for (key, value) in partials {
let template = try HBMustacheTemplate(string: value)
library.register(template, named: key)
}
let result = library.render(object, withTemplate: "template")
XCTAssertEqual(result, expected)
}
// MARK: Comments
final class SpecCommentsTests: XCTestCase {
@@ -112,6 +124,179 @@ final class SpecCommentsTests: XCTestCase {
}
}
// MARK: Delimiters
final class SpecDelimiterTests: XCTestCase {
func testPairBehaviour() throws {
let object = ["text": "Hey!"]
let template = "{{=<% %>=}}(<%text%>)"
let expected = "(Hey!)"
try test(object, template, expected)
}
func testSpecialCharacter() throws {
let object = ["text": "It worked!"]
let template = "({{=[ ]=}}[text])"
let expected = "(It worked!)"
try test(object, template, expected)
}
func testSections() throws {
let object: [String: Any] = ["section": true, "data": "I got interpolated."]
let template = """
[
{{#section}}
{{data}}
|data|
{{/section}}
{{= | | =}}
|#section|
{{data}}
|data|
|/section|
]
"""
let expected = """
[
I got interpolated.
|data|
{{data}}
I got interpolated.
]
"""
try test(object, template, expected)
}
func testInvertedSections() throws {
let object: [String: Any] = ["section": false, "data": "I got interpolated."]
let template = """
[
{{^section}}
{{data}}
|data|
{{/section}}
{{= | | =}}
|^section|
{{data}}
|data|
|/section|
]
"""
let expected = """
[
I got interpolated.
|data|
{{data}}
I got interpolated.
]
"""
try test(object, template, expected)
}
func testPartialInheritance() throws {
let object = ["value": "yes"]
let template = """
[ {{>include}} ]
{{= | | =}}
[ |>include| ]
"""
let partial = ".{{value}}."
let expected = """
[ .yes. ]
[ .yes. ]
"""
try testPartial(object, template, ["include": partial], expected)
}
func testPostPartialBehaviour() throws {
let object = ["value": "yes"]
let template = """
[ {{>include}} ]
[ .{{value}}. .|value|. ]
"""
let partial = ".{{value}}. {{= | | =}} .|value|."
let expected = """
[ .yes. .yes. ]
[ .yes. .|value|. ]
"""
try testPartial(object, template, ["include": partial], expected)
}
func testSurroundingWhitespace() throws {
let object = {}
let template = "| {{=@ @=}} |"
let expected = "| |"
try test(object, template, expected)
}
func testOutlyingWhitespace() throws {
let object = {}
let template = " | {{=@ @=}}\n"
let expected = " | \n"
try test(object, template, expected)
}
func testStandalone() throws {
let object = {}
let template = """
Begin.
{{=@ @=}}
End.
"""
let expected = """
Begin.
End.
"""
try test(object, template, expected)
}
func testIndentedStandalone() throws {
let object = {}
let template = """
Begin.
{{=@ @=}}
End.
"""
let expected = """
Begin.
End.
"""
try test(object, template, expected)
}
func testStandaloneLineEndings() throws {
let object = {}
let template = "|\r\n{{= @ @ =}}\r\n|"
let expected = "|\r\n|"
try test(object, template, expected)
}
func testStandaloneWithoutPreviousLine() throws {
let object = {}
let template = " {{=@ @=}}\n="
let expected = "="
try test(object, template, expected)
}
func testStandaloneWithoutNewLine() throws {
let object = {}
let template = "=\n {{=@ @=}}"
let expected = "=\n"
try test(object, template, expected)
}
func testPairWithPadding() throws {
let object = {}
let template = "|{{= @ @ =}}|"
let expected = "||"
try test(object, template, expected)
}
}
// MARK: Interpolation
final class SpecInterpolationTests: XCTestCase {
@@ -512,18 +697,6 @@ final class SpecInvertedTests: XCTestCase {
// MARK: Partials
final class SpecPartialsTests: XCTestCase {
func testPartial(_ object: Any, _ template: String, _ partials: [String: String], _ expected: String) throws {
let library = HBMustacheLibrary()
let template = try HBMustacheTemplate(string: template)
library.register(template, named: "template")
for (key, value) in partials {
let template = try HBMustacheTemplate(string: value)
library.register(template, named: key)
}
let result = library.render(object, withTemplate: "template")
XCTAssertEqual(result, expected)
}
func testBasic() throws {
let object = {}
let template = #""{{>text}}""#