Files
swiftpm-mustache/Sources/HummingbirdMustache/Template+Parser.swift
2021-03-22 12:11:22 +00:00

349 lines
15 KiB
Swift

extension HBMustacheTemplate {
/// Error return by `HBMustacheTemplate.parse`. Includes information about where error occurred
public struct ParserError: Swift.Error {
public let context: HBParser.Context
public let error: Swift.Error
}
/// Error generated by `HBMustacheTemplate.parse`
public enum Error: Swift.Error {
/// the end section does not match the name of the start section
case sectionCloseNameIncorrect
/// tag was badly formatted
case unfinishedName
/// was not expecting a section end
case expectedSectionEnd
/// set delimiter tag badly formatted
case invalidSetDelimiter
/// cannot apply transform to inherited section
case transformAppliedToInheritanceSection
/// illegal token inside inherit section of partial
case illegalTokenInsideInheritSection
/// text found inside inherit section of partial
case textInsideInheritSection
}
struct ParserState {
var sectionName: String?
var sectionTransform: String?
var newLine: Bool
var startDelimiter: String
var endDelimiter: String
init() {
self.sectionName = nil
self.newLine = true
self.startDelimiter = "{{"
self.endDelimiter = "}}"
}
func withSectionName(_ name: String, transform: String? = nil) -> ParserState {
var newValue = self
newValue.sectionName = name
newValue.sectionTransform = transform
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)
do {
return try self.parse(&parser, state: .init())
} catch {
throw ParserError(context: parser.getContext(), error: error)
}
}
/// parse section in mustache text
static func parse(_ parser: inout HBParser, state: ParserState) throws -> [Token] {
var tokens: [Token] = []
var state = state
var whiteSpaceBefore: Substring = ""
while !parser.reachedEnd() {
// if new line read whitespace
if state.newLine {
whiteSpaceBefore = parser.read(while: Set(" \t"))
}
let text = try readUntilDelimiterOrNewline(&parser, state: state)
// if we hit a newline add text
if parser.current().isNewline {
tokens.append(.text(whiteSpaceBefore + text + String(parser.current())))
state.newLine = true
parser.unsafeAdvance()
continue
}
// 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))
whiteSpaceBefore = ""
state.newLine = false
}
// have we reached the end of the text
if parser.reachedEnd() {
break
}
var setNewLine = false
switch parser.current() {
case "#":
// section
parser.unsafeAdvance()
let (name, transform) = try parseName(&parser, state: state)
if self.isStandalone(&parser, state: state) {
setNewLine = true
} else if whiteSpaceBefore.count > 0 {
tokens.append(.text(String(whiteSpaceBefore)))
whiteSpaceBefore = ""
}
let sectionTokens = try parse(&parser, state: state.withSectionName(name, transform: transform))
tokens.append(.section(name: name, transform: transform, template: HBMustacheTemplate(sectionTokens)))
case "^":
// inverted section
parser.unsafeAdvance()
let (name, transform) = try parseName(&parser, state: state)
if self.isStandalone(&parser, state: state) {
setNewLine = true
} else if whiteSpaceBefore.count > 0 {
tokens.append(.text(String(whiteSpaceBefore)))
whiteSpaceBefore = ""
}
let sectionTokens = try parse(&parser, state: state.withSectionName(name, transform: transform))
tokens.append(.invertedSection(name: name, transform: transform, template: HBMustacheTemplate(sectionTokens)))
case "$":
// inherited section
parser.unsafeAdvance()
let (name, transform) = try parseName(&parser, state: state)
// ERROR: can't have transform applied to inherited sections
guard transform == nil else { throw Error.transformAppliedToInheritanceSection }
if self.isStandalone(&parser, state: state) {
setNewLine = true
} else if whiteSpaceBefore.count > 0 {
tokens.append(.text(String(whiteSpaceBefore)))
whiteSpaceBefore = ""
}
let sectionTokens = try parse(&parser, state: state.withSectionName(name, transform: transform))
tokens.append(.inheritedSection(name: name, template: HBMustacheTemplate(sectionTokens)))
case "/":
// end of section
parser.unsafeAdvance()
let position = parser.position
let (name, transform) = try parseName(&parser, state: state)
guard name == state.sectionName, transform == state.sectionTransform else {
parser.unsafeSetPosition(position)
throw Error.sectionCloseNameIncorrect
}
if self.isStandalone(&parser, state: state) {
setNewLine = true
} else if whiteSpaceBefore.count > 0 {
tokens.append(.text(String(whiteSpaceBefore)))
whiteSpaceBefore = ""
}
return tokens
case "!":
// comment
parser.unsafeAdvance()
_ = try self.parseComment(&parser, state: state)
setNewLine = self.isStandalone(&parser, state: state)
case "{":
// unescaped variable
if whiteSpaceBefore.count > 0 {
tokens.append(.text(String(whiteSpaceBefore)))
whiteSpaceBefore = ""
}
parser.unsafeAdvance()
let (name, transform) = try parseName(&parser, state: state)
guard try parser.read("}") else { throw Error.unfinishedName }
tokens.append(.unescapedVariable(name: name, transform: transform))
case "&":
// unescaped variable
if whiteSpaceBefore.count > 0 {
tokens.append(.text(String(whiteSpaceBefore)))
whiteSpaceBefore = ""
}
parser.unsafeAdvance()
let (name, transform) = try parseName(&parser, state: state)
tokens.append(.unescapedVariable(name: name, transform: transform))
case ">":
// partial
parser.unsafeAdvance()
let (name, _) = try parseName(&parser, state: state)
if whiteSpaceBefore.count > 0 {
tokens.append(.text(String(whiteSpaceBefore)))
}
if self.isStandalone(&parser, state: state) {
setNewLine = true
tokens.append(.partial(name, indentation: String(whiteSpaceBefore), inherits: nil))
} else {
tokens.append(.partial(name, indentation: nil, inherits: nil))
}
whiteSpaceBefore = ""
case "<":
// partial with inheritance
parser.unsafeAdvance()
let (name, transform) = try parseName(&parser, state: state)
// ERROR: can't have transform applied to inherited sections
guard transform == nil else { throw Error.transformAppliedToInheritanceSection }
var indent: String?
if self.isStandalone(&parser, state: state) {
setNewLine = true
} else if whiteSpaceBefore.count > 0 {
indent = String(whiteSpaceBefore)
tokens.append(.text(indent!))
whiteSpaceBefore = ""
}
let sectionTokens = try parse(&parser, state: state.withSectionName(name, transform: transform))
var inherit: [String: HBMustacheTemplate] = [:]
// parse tokens in section to extract inherited sections
for token in sectionTokens {
switch token {
case .inheritedSection(let name, let template):
inherit[name] = template
case .text:
break
default:
throw Error.illegalTokenInsideInheritSection
}
}
tokens.append(.partial(name, indentation: indent, inherits: inherit))
case "=":
// set delimiter
parser.unsafeAdvance()
state = try self.parserSetDelimiter(&parser, state: state)
setNewLine = self.isStandalone(&parser, state: state)
default:
// variable
if whiteSpaceBefore.count > 0 {
tokens.append(.text(String(whiteSpaceBefore)))
whiteSpaceBefore = ""
}
let (name, transform) = try parseName(&parser, state: state)
tokens.append(.variable(name: name, transform: transform))
}
state.newLine = setNewLine
}
// should never get here if reading section
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<Character> = ["\n", "\r\n"]
guard let delimiterFirstChar = state.startDelimiter.first else { return "" }
var totalText = ""
untilSet.insert(delimiterFirstChar)
while !parser.reachedEnd() {
// read until we hit either a newline or "{"
let text = try parser.read(until: untilSet, throwOnOverflow: false)
totalText += text
// if new line append all text read plus newline
if parser.current().isNewline {
break
} else if parser.current() == delimiterFirstChar {
if try parser.read(string: state.startDelimiter) {
break
}
totalText += String(delimiterFirstChar)
parser.unsafeAdvance()
}
}
return totalText
}
/// parse variable name
static func parseName(_ parser: inout HBParser, state: ParserState) throws -> (String, String?) {
parser.read(while: \.isWhitespace)
let text = String(parser.read(while: self.sectionNameChars))
parser.read(while: \.isWhitespace)
guard try parser.read(string: state.endDelimiter) else { throw Error.unfinishedName }
// does the name include brackets. If so this is a transform call
var nameParser = HBParser(String(text))
let string = nameParser.read(while: self.sectionNameCharsWithoutBrackets)
if nameParser.reachedEnd() {
return (text, nil)
} else {
// parse function parameter, as we have just parsed a function name
guard nameParser.current() == "(" else { throw Error.unfinishedName }
nameParser.unsafeAdvance()
let string2 = nameParser.read(while: self.sectionNameCharsWithoutBrackets)
guard nameParser.current() == ")" else { throw Error.unfinishedName }
nameParser.unsafeAdvance()
guard nameParser.reachedEnd() else { throw Error.unfinishedName }
return (String(string2), String(string))
}
}
static func parseComment(_ parser: inout HBParser, state: ParserState) throws -> String {
let text = try parser.read(untilString: state.endDelimiter, throwOnOverflow: true, skipToEnd: true)
return String(text)
}
static func parserSetDelimiter(_ parser: inout HBParser, state: ParserState) throws -> ParserState {
let startDelimiter: Substring
let endDelimiter: Substring
do {
parser.read(while: \.isWhitespace)
startDelimiter = try parser.read(until: \.isWhitespace)
parser.read(while: \.isWhitespace)
endDelimiter = try parser.read(until: { $0 == "=" || $0.isWhitespace })
parser.read(while: \.isWhitespace)
} catch {
throw Error.invalidSetDelimiter
}
guard try parser.read("=") else { throw Error.invalidSetDelimiter }
guard try parser.read(string: state.endDelimiter) else { throw Error.invalidSetDelimiter }
guard startDelimiter.count > 0, endDelimiter.count > 0 else { throw Error.invalidSetDelimiter }
return state.withDelimiters(start: String(startDelimiter), end: String(endDelimiter))
}
static func hasLineFinished(_ parser: inout HBParser) -> Bool {
var parser2 = parser
if parser.reachedEnd() { return true }
parser2.read(while: Set(" \t"))
if parser2.current().isNewline {
parser2.unsafeAdvance()
try! parser.setPosition(parser2.position)
return true
}
return false
}
static func isStandalone(_ parser: inout HBParser, state: ParserState) -> Bool {
return state.newLine && self.hasLineFinished(&parser)
}
private static let sectionNameCharsWithoutBrackets = Set<Character>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._?")
private static let sectionNameChars = Set<Character>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._?()")
}