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() != "{" {
if text.count > 0 {
tokens.append(.text(whiteSpaceBefore + text.string + "{"))
whiteSpaceBefore = ""
newLine = false
}
continue
} else {
parser.unsafeAdvance()
}
}
// whatever text we found before the "{{" should be added
// 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
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 }