//
//  Mustache.swift
//  PerfectLib
//
//  Created by Kyle Jessup on 7/7/15.
//	Copyright (C) 2015 PerfectlySoft, Inc.
//
//===----------------------------------------------------------------------===//
//
// This source file is part of the Perfect.org open source project
//
// Copyright (c) 2015 - 2016 PerfectlySoft Inc. and the Perfect project authors
// Licensed under Apache License v2.0
//
// See http://perfect.org/licensing.html for license information
//
//===----------------------------------------------------------------------===//
//

import PerfectLib
import Foundation

let mustacheExtension = "mustache"

private var mustacheTemplateCache = [String: (Int, MustacheTemplate)]()
private let mustacheTemplateCacheQueue = DispatchQueue(label: "mustacheTemplateCache", attributes: .concurrent)

private func getTemplateFromCache(_ path: String) throws -> MustacheTemplate {
	let file = File(path)
	let modDate = file.modificationTime
	var template: MustacheTemplate? = mustacheTemplateCacheQueue.sync {
		if let fnd = mustacheTemplateCache[path], fnd.0 == modDate {
			return fnd.1.clone() as? MustacheTemplate
		}
		return nil
	}
	if let templateW = template?.clone() as? MustacheTemplate {
		return templateW
	}
	template = try mustacheTemplateCacheQueue.sync(flags: .barrier) {
		if let fnd = mustacheTemplateCache[path], fnd.0 == modDate {
			return fnd.1.clone() as? MustacheTemplate
		} else {
			try file.open()
			defer { file.close() }
			let bytes = try file.readSomeBytes(count: file.size)
			
			let parser = MustacheParser()
			let str = UTF8Encoding.encode(bytes: bytes)
			let templateW = try parser.parse(string: str)
			mustacheTemplateCache[path] = (modDate, templateW)
			return templateW.clone() as? MustacheTemplate
		}
	}
	guard let templateW = template else {
		throw PerfectError.systemError(404, "The file \"\(path)\" was not found")
	}
	return templateW
}

enum MustacheTagType {
	
	case plain // plain text
	case tag // some tag. not sure which yet
	case hash
	case slash
	case amp
	case caret
	case bang
	case partial
	case delims
	case unescapedName
	case name
	case unencodedName
	case pragma
	case none
	
}

/// This enum type represents the parsing and the runtime evaluation exceptions which may be generated.
public enum MustacheError : Error {
	/// The mustache template was malformed.
	case syntaxError(String)
	/// An exception occurred while evaluating the template.
	case evaluationError(String)
}

/// This class represents an individual scope for mustache template values.
/// A mustache template handler will return a `MustacheEvaluationContext.MapType` object as a result from its `PageHandler.valuesForResponse` function.
public class MustacheEvaluationContext {
	var mapValues: MapType
	
	public typealias MapType = [String:Any]
	public typealias SequenceType = [MapType]
	/// The parent of this context
	public var parent: MustacheEvaluationContext? = nil
	/// Complete path to the file being processed
	public var templatePath: String?
	/// Mustache content for dynamic generation
	public var templateContent: String?
	/// Returns the name of the current template file.
	public var templateName: String? {
		let nam = templatePath?.lastFilePathComponent
		return nam
	}
	
	public init(templatePath: String, map: MapType = MapType()) {
		self.templatePath = templatePath
		self.mapValues = map
	}
	
	public init(templateContent: String, map: MapType = MapType()) {
		self.templateContent = templateContent
		self.mapValues = map
	}
	
	public init(map: MapType = MapType()) {
		self.mapValues = map
	}
	
	/// All the template values have been completed and resulting content should be
	/// formulated and returned.
	public func formulateResponse(withCollector collector: MustacheEvaluationOutputCollector) throws -> String {
		var maybeTemplate: MustacheTemplate?
		
		if let templatePath = self.templatePath {
			maybeTemplate = try getTemplateFromCache(templatePath)
			maybeTemplate?.templateName = templatePath.lastFilePathComponent
		} else if let templateContent = self.templateContent {
			let parser = MustacheParser()
			maybeTemplate = try parser.parse(string: templateContent)
			maybeTemplate?.templateName = "dynamic"
		}
		
		guard let template = maybeTemplate else {
			throw MustacheError.evaluationError("No templatePath or templateContent")
		}
		
		template.evaluatePragmas(context: self, collector: collector)
		template.evaluate(context: self, collector: collector)
		
		let fullString = collector.asString()
		return fullString
	}
	
	func newChildContext() -> MustacheEvaluationContext {
		let cc = MustacheEvaluationContext()
		cc.parent = self
		return cc
	}
	
	func newChildContext(withMap with: MapType) -> MustacheEvaluationContext {
		let cc = MustacheEvaluationContext(map: with)
		cc.parent = self
		return cc
	}
	
	/// Search for a value starting from the current context. If not found in the current context, the parent context will be searched, etc.
	/// - parameter named: The name of the value to find
	/// - returns: The value, if found, or nil
	public func getValue(named nam: String) -> MapType.Value? {
		let values = nam.components(separatedBy: ".")
		var cntxt: MapType.Value? = mapValues
		for val in values {
			guard let prev = cntxt as? MapType else {
				break
			}
			cntxt = prev[val]
		}
		let v = cntxt
		if v == nil && parent != nil {
			return parent?.getValue(named: nam)
		}
		return v
	}
	
	/// Extends the current values with those from the parameter.
	/// - parameter with: The new values to add
	public func extendValues(with wit: MapType) {
		for (key, value) in wit {
			mapValues[key] = value
		}
	}
	
	func getCurrentFilePath() -> String? {
		if self.templateName != nil && self.templatePath != nil {
			return self.templatePath!
		}
		if self.parent != nil {
			return self.parent!.getCurrentFilePath()
		}
		return nil
	}
}

/// An instance of this class will collect all output data generated by mustache tags during evaluation.
/// Call the `asString()` function to retreive the resulting data.
public class MustacheEvaluationOutputCollector {
	var output = [String]()
	
	public var defaultEncodingFunc: (String) -> String = { $0.stringByEncodingHTML }
	
	/// Empty public initializer.
	public init() {
		
	}
	
	/// Append a new string value to the collected output.
	/// - parameter s: The string value which will be appended.
	/// - parameter encoded: If true, the string value will be HTML encoded as it is appended. Defaults to true.
	@discardableResult
	public func append(_ s: String, encoded: Bool = true) -> MustacheEvaluationOutputCollector {
		if encoded {
			output.append(self.defaultEncodingFunc(s))
		} else {
			output.append(s)
		}
		return self
	}
	
	/// Joins all the collected output into one string and returns this value.
	public func asString() -> String {
		return output.joined(separator: "")
	}
}

/// An individual mustache tag or plain-text section.
public class MustacheTag {
	var type = MustacheTagType.none
	var tag = ""
	weak var parent: MustacheGroupTag?
	
	// only used for debug purposes
	var openD: [UnicodeScalar]?
	var closeD: [UnicodeScalar]?
	
	func clone() -> MustacheTag {
		let s = MustacheTag()
		self.populateClone(s)
		return s
	}
	
	func populateClone(_ tag: MustacheTag) {
		tag.type = self.type
		tag.tag = self.tag
		// parent sets our parent
	}
	
	/// Evaluate the tag within the given context.
	public func evaluate(context contxt: MustacheEvaluationContext, collector: MustacheEvaluationOutputCollector) {
		
		switch type {
		case .plain:
			collector.append(tag, encoded: false)
		case .unescapedName:
			collector.append(tag, encoded: false)
		case .name:
			if let value = contxt.getValue(named: tag) {
				if let lambda = value as? (String, MustacheEvaluationContext) -> String {
					collector.append(lambda(tag, contxt))
				} else {
					collector.append("\(value)")
				}
			}
		case .unencodedName:
			if let value = contxt.getValue(named: tag) {
				collector.append("\(value)", encoded: false)
			}
		case .pragma, .bang:
			() // ignored
		default:
			print("Unhandled mustache tag type \(type)")
		}
	}
	
	func delimOpen() -> String {
		var s = "".unicodeScalars
		s.append(contentsOf: openD!)
		return String(s)
	}
	
	func delimClose() -> String {
		var s = "".unicodeScalars
		s.append(contentsOf: closeD!)
		return String(s)
	}
	
	/// Reconstitutes the tag into its original source string form.
	/// - returns: The resulting string, including the original delimiters and tag-type marker.
	public func description() -> String {
		guard type != .plain else {
			return tag
		}
		var s = delimOpen()
		switch type {
		case .name:
			s.append(" ")
		case .unencodedName:
			s.append("{ ")
		case .hash:
			s.append("# ")
		case .caret:
			s.append("^ ")
		case .bang:
			s.append("! ")
		case .partial:
			s.append("> ")
		case .unescapedName:
			s.append("& ")
		case .pragma:
			s.append("% ")
		default:
			()
		}
		s.append(tag)
		if type == .unencodedName {
			s.append(" }" + delimClose())
		} else {
			s.append(" " + delimClose())
		}
		return s
	}
}

/// A sub-class of MustacheTag which represents a mustache "partial" tag.
public class MustachePartialTag : MustacheTag {
	override func clone() -> MustacheTag {
		let s = MustachePartialTag()
		self.populateClone(s)
		return s
	}
	
	/// Override for evaluating the partial tag.
	public override func evaluate(context contxt: MustacheEvaluationContext, collector: MustacheEvaluationOutputCollector) {
		guard let page = contxt.getCurrentFilePath(), !page.isEmpty else {
			print("Exception while executing partial \(tag): unable to find template root directory")
			return
		}
		let withoutLast = page.deletingLastFilePathComponent
		let slash = withoutLast[withoutLast.startIndex] == "/" ? "/" : ""
		let pageDir = slash + withoutLast.split(separator: "/").map(String.init).joined(separator: "/")
		let fullPath = pageDir + "/" + self.tag + "." + mustacheExtension
		do {
			let template = try getTemplateFromCache(fullPath)
			template.evaluatePragmas(context: contxt, collector: collector)
			
			let newContext = contxt.newChildContext()
			newContext.templatePath = fullPath
			template.evaluate(context: newContext, collector: collector)
			
		} catch let e {
			print("Exception while executing partial \(tag): \(e)")
		}
	}
}

/// A sub-class of MustacheTag which represents a pragma tag.
/// Pragma tags are "meta" tags which influence template evaluation but likely do not output any data.
public class MustachePragmaTag : MustacheTag {
	
	override func clone() -> MustacheTag {
		let s = MustachePragmaTag()
		self.populateClone(s)
		return s
	}
	
	/// Parse the pragma. Pragmas should be in the format: A:B,C:D,E,F:G.
	/// - returns: A Dictionary containing the pragma names and values.
	public func parsePragma() -> Dictionary<String, String> {
		var d = Dictionary<String, String>()
		let commaSplit = tag.split() { $0 == Character(",") }.map { String($0) }
		for section in commaSplit {
			let colonSplit = section.split() { $0 == Character(":") }.map { String($0) }
			if colonSplit.count == 1 {
				d[colonSplit[0]] = ""
			} else if colonSplit.count > 1 {
				d[colonSplit[0]] = colonSplit[1]
			}
		}
		return d
	}
	
}

/// A sub-class of MustacheTag which represents a group of child tags.
public class MustacheGroupTag : MustacheTag {
	var children = [MustacheTag]()
	
	override func clone() -> MustacheTag {
		let s = MustacheGroupTag()
		self.populateClone(s)
		return s
	}
	
	override func populateClone(_ tag: MustacheTag) {
		super.populateClone(tag)
		if let groupTag = tag as? MustacheGroupTag {
			groupTag.children = self.children.map { $0.clone() }
		}
	}
	
	func evaluatePos(context contxt: MustacheEvaluationContext, collector: MustacheEvaluationOutputCollector) {
		let cValue = contxt.getValue(named: tag)
		if let value = cValue {
			// if it is a dictionary, then eval children with it as the new context
			// otherwise, it must be an array with elements which are dictionaries
			switch value {
			case let v as MustacheEvaluationContext.MapType:
				let newContext = contxt.newChildContext(withMap: v)
				for child in children {
					child.evaluate(context: newContext, collector: collector)
				}
				// case let v as [String:String]:
				// 	let newContext = context.newChildContext(v)
				// 	for child in children {
				// 		child.evaluate(newContext, collector: collector)
			// 	}
			case let sequence as MustacheEvaluationContext.SequenceType:
				for item in sequence {
					let newContext = contxt.newChildContext(withMap: item)
					for child in children {
						child.evaluate(context: newContext, collector: collector)
					}
				}
			case let lambda as (String, MustacheEvaluationContext) -> String where !lambda(bodyText(), contxt).isEmpty:
				for child in children {
					child.evaluate(context: contxt, collector: collector)
				}
			case let stringValue as String where stringValue.count > 0:
				for child in children {
					child.evaluate(context: contxt, collector: collector)
				}
			case let booleanValue as Bool where booleanValue == true:
				for child in children {
					child.evaluate(context: contxt, collector: collector)
				}
			case let intValue as Int where intValue != 0:
				for child in children {
					child.evaluate(context: contxt, collector: collector)
				}
			case let decValue as Double where decValue != 0.0:
				for child in children {
					child.evaluate(context: contxt, collector: collector)
				}
			default:
				()
			}
		}
	}
	
	func evaluateNeg(context contxt: MustacheEvaluationContext, collector: MustacheEvaluationOutputCollector) {
		let cValue = contxt.getValue(named: tag)
		if let value = cValue {
			switch value {
			case let v as MustacheEvaluationContext.MapType where v.count == 0:
				for child in children {
					child.evaluate(context: contxt, collector: collector)
				}
			case let v as [String:String] where v.count == 0:
				for child in children {
					child.evaluate(context: contxt, collector: collector)
				}
			case let sequence as MustacheEvaluationContext.SequenceType where sequence.count == 0:
				for child in children {
					child.evaluate(context: contxt, collector: collector)
				}
			case let booleanValue as Bool where booleanValue == false:
				for child in children {
					child.evaluate(context: contxt, collector: collector)
				}
			default:
				()
			}
		} else {
			for child in children {
				child.evaluate(context: contxt, collector: collector)
			}
		}
	}
	
	/// Evaluate the tag in the given context.
	public override func evaluate(context contxt: MustacheEvaluationContext, collector: MustacheEvaluationOutputCollector) {
		if type == .hash {
			self.evaluatePos(context: contxt, collector: collector)
		} else if type == .caret {
			self.evaluateNeg(context: contxt, collector: collector)
		} else {
			// optionally a warning?
			// otherwise this is a perfectly valid situation
		}
	}
	
	func bodyText() -> String {
		var s = ""
		for child in children {
			s.append(child.description())
		}
		return s
	}
	
	/// Returns a String containing the reconstituted tag, including all children.
	override public func description() -> String {
		var s = super.description()
		for child in children {
			s.append(child.description())
		}
		s.append(delimOpen() + "/ " + tag + " " + delimClose())
		return s
	}
}

/// This class represents a mustache template which has been parsed and is ready to evaluate.
/// It contains a series of "out of band" pragmas which can influence the evaluation, and a
/// series of children which constitute the body of the template itself.
public class MustacheTemplate : MustacheGroupTag {
	
	var pragmas = [MustachePragmaTag]()
	var templateName: String = ""
	
	override func clone() -> MustacheTag {
		let s = MustacheTemplate()
		self.populateClone(s)
		return s
	}
	
	override func populateClone(_ tag: MustacheTag) {
		super.populateClone(tag)
		if let template = tag as? MustacheTemplate {
			#if swift(>=4.1)
				template.pragmas = self.pragmas.compactMap { $0.clone() as? MustachePragmaTag }
			#else
				template.pragmas = self.pragmas.flatMap { $0.clone() as? MustachePragmaTag }
			#endif
			self.templateName = template.templateName
		}
	}
	
	/// Evaluate any pragmas which were found in the template. These pragmas may alter the given `MustacheEvaluationContext` parameter.
	/// - parameter context: The `MustacheEvaluationContext` object which will be used to further evaluate the template.
	/// - parameter collector: The `MustacheEvaluationOutputCollector` object which will collect all output from the template evaluation.
	public func evaluatePragmas(context contxt: MustacheEvaluationContext, collector: MustacheEvaluationOutputCollector) {
		//		for pragma in pragmas {
		//			let d = pragma.parsePragma()
		//			...
		//		}
	}
	
	/// Evaluate the template using the given context and output collector.
	/// - parameter context: The `MustacheEvaluationContext` object which holds the values used for evaluating the template.
	/// - parameter collector: The `MustacheEvaluationOutputCollector` object which will collect all output from the template evaluation. `MustacheEvaluationOutputCollector.asString()` can be called to retreive the resulting output data.
	override public func evaluate(context contxt: MustacheEvaluationContext, collector: MustacheEvaluationOutputCollector) {
		for child in children {
			child.evaluate(context: contxt, collector: collector)
		}
	}
	
	/// Returns a String containing the reconstituted template, including all children.
	override public func description() -> String {
		var s = ""
		for child in children {
			s.append(child.description())
		}
		return s
	}
}

/// This object will parse templates written in the mustache markup language.
/// Calling `parse` with the path to a file will return the resulting parsed and ready to evaluate template.
public class MustacheParser {
	
	var activeList: MustacheGroupTag?
	var pragmas = [MustachePragmaTag]()
	var openDelimiters: [UnicodeScalar] = ["{", "{"]
	var closeDelimiters: [UnicodeScalar] = ["}", "}"]
	var handlingUnencodedName = false
	var testingPutback: String?
	
	typealias UGen = String.UnicodeScalarView.Iterator
	
	var g: UGen?
	var offset = -1
	
	/// Empty public initializer.
	public init() {
		
	}
	
	/// Parses a string containing mustache markup and returns the `MustacheTemplate` object.
	/// - throws: `MustacheError.SyntaxError`
	/// - returns: A `MustacheTemplate` object which can be evaluated.
	public func parse(string strng: String) throws -> MustacheTemplate {
		let t = MustacheTemplate()
		self.activeList = t
		self.g = strng.unicodeScalars.makeIterator()
		try consumeLoop()
		t.pragmas = pragmas
		return t
	}
	
	func next() -> UnicodeScalar? {
		offset += 1
		return g?.next()
	}
	
	func consumeLoop() throws {
		var type = MustacheTagType.plain
		repeat {
			type = try consumeType(type)
			// just loop it
		} while type != .none
	}
	
	func consumeType(_ t: MustacheTagType) throws -> MustacheTagType {
		switch t {
		case .plain:
			return consumePlain()
		case .tag:
			return try consumeTag()
		default:
			throw MustacheError.syntaxError("Bad parsing logic in consumeType \(t)")
		}
	}
	
	func addChild(_ t: MustacheTag) {
		self.activeList!.children.append(t)
		t.parent = self.activeList!
		t.openD = openDelimiters
		t.closeD = closeDelimiters
	}
	
	// Read until delimiters are encountered
	func consumePlain() -> MustacheTagType {
		let currTag = MustacheTag()
		currTag.type = .plain
		addChild(currTag)
		while true {
			guard let e = next() else {
				return .none
			}
			if e == openDelimiters[0] {
				testingPutback = String(e)
				if consumePossibleOpenDelimiter(index: 1) {
					return .tag
				}
				currTag.tag.append(testingPutback!)
			} else {
				currTag.tag.append(String(Character(e)))
			}
		}
	}
	
	func consumePossibleOpenDelimiter(index idx: Int) -> Bool {
		if idx == openDelimiters.count { // we successfully encountered a full delimiter sequence
			return true
		}
		if let e = next() {
			testingPutback!.append(String(Character(e)))
			if e == openDelimiters[idx] {
				return consumePossibleOpenDelimiter(index: 1 + idx)
			}
		}
		return false
	}
	
	func consumePossibleCloseDelimiter(index idx: Int) -> Bool {
		if idx == closeDelimiters.count { // we successfully encountered a full delimiter sequence
			return true
		}
		if let e = next() {
			if e == closeDelimiters[idx] {
				testingPutback!.append(String(Character(e)))
				return consumePossibleCloseDelimiter(index: 1 + idx)
			}
		}
		return false
	}
	
	// Read until delimiters are encountered
	func consumeTag() throws -> MustacheTagType {
		if let e = skipWhiteSpace() {
			// e is first non-white character
			// # ^ ! >
			switch e {
			case "%": // pragma
				let tagName = consumeTagName(firstChar: skipWhiteSpace())
				let newTag = MustachePragmaTag()
				newTag.tag = tagName
				newTag.type = .pragma
				addChild(newTag)
				pragmas.append(newTag)
				
			case "#": // group
				let tagName = consumeTagName(firstChar: skipWhiteSpace())
				let newTag = MustacheGroupTag()
				newTag.tag = tagName
				newTag.type = .hash
				addChild(newTag)
				activeList = newTag
				
			case "^": // inverted group
				let tagName = consumeTagName(firstChar: skipWhiteSpace())
				let newTag = MustacheGroupTag()
				newTag.tag = tagName
				newTag.type = .caret
				addChild(newTag)
				activeList = newTag
				
			case "!": // comment COULD discard but I add it for debugging purposes. skipped during eval
				let tagName = consumeTagName(firstChar: skipWhiteSpace())
				let newTag = MustacheTag()
				newTag.tag = tagName
				newTag.type = .bang
				addChild(newTag)
				
			case "&": // unescaped name
				let tagName = consumeTagName(firstChar: skipWhiteSpace())
				let newTag = MustacheTag()
				newTag.tag = tagName
				newTag.type = .unescapedName
				addChild(newTag)
				
			case ">": // partial
				let tagName = consumeTagName(firstChar: skipWhiteSpace())
				let newTag = MustachePartialTag()
				newTag.tag = tagName
				newTag.type = .partial
				addChild(newTag)
				
			case "/": // pop group. ensure names match
				let tagName = consumeTagName(firstChar: skipWhiteSpace())
				guard tagName == activeList!.tag else {
					throw MustacheError.syntaxError("The closing tag /" + tagName + " did not match " + activeList!.tag)
				}
				activeList = activeList!.parent
				
			case "=": // set delimiters
				try consumeSetDelimiters()
				
			case "{": // unencoded name
				handlingUnencodedName = true
				let tagName = consumeTagName(firstChar: skipWhiteSpace())
				let newTag = MustacheTag()
				newTag.tag = tagName
				newTag.type = .unencodedName
				addChild(newTag)
				guard !handlingUnencodedName else {
					throw MustacheError.syntaxError("The unencoded tag " + tagName + " did not proper closing delimiters")
				}
			default:
				let tagName = consumeTagName(firstChar: e)
				let newTag = MustacheTag()
				newTag.tag = tagName
				newTag.type = .name
				addChild(newTag)
			}
		}
		return .plain
	}
	
	// reads until closing delimiters
	// read and discard closing delimiters leaving us things at .Plain
	func consumeTagName() -> String {
		var s = ""
		return consumeTagName(into: &s)
	}
	
	// reads until closing delimiters
	// firstChar was read as part of previous step and should be added to the result
	func consumeTagName(firstChar first: UnicodeScalar?) -> String {
		guard let f = first else {
			return ""
		}
		var s = String(f)
		return consumeTagName(into: &s)
	}
	
	func consumeTagName(into s: inout String) -> String {
		while let e = next() {
			if handlingUnencodedName && e == "}" {
				handlingUnencodedName = false
				continue
			}
			if e == closeDelimiters[0] {
				testingPutback = String(e)
				if consumePossibleCloseDelimiter(index: 1) {
					break
				}
				s.append(testingPutback!)
			} else {
				s.append(String(Character(e)))
			}
		}
		var scalars = s.unicodeScalars
		var idx = scalars.index(before: scalars.endIndex)
		while scalars[idx].isWhiteSpace() {
			scalars.remove(at: idx)
			idx = scalars.index(before: idx)
		}
		return String(scalars)
	}
	
	func skipWhiteSpace() -> UnicodeScalar? {
		var e = next()
		while e != nil {
			if !(e!).isWhiteSpace() {
				return e
			}
			e = next()
		}
		return e
	}
	
	func consumeSetDelimiters() throws {
		let errorMsg = "Syntax error while setting delimiters"
		var e = skipWhiteSpace()
		if e != nil {
			
			var openD = String(e!)
			// read until a white space
			while true {
				
				e = next()
				if e != nil && !(e!).isWhiteSpace() {
					openD.append(String(Character(e!)))
				} else {
					break
				}
			}
			
			guard e != nil && (e!).isWhiteSpace() else {
				throw MustacheError.syntaxError(errorMsg)
			}
			
			e = skipWhiteSpace()
			
			guard e != nil && !(e!).isWhiteSpace() else {
				throw MustacheError.syntaxError(errorMsg)
			}
			
			var closeD = String(e!)
			// read until a =
			while true {
				
				e = next()
				if e != nil && !(e!).isWhiteSpace() && e! != "=" {
					closeD.append(String(Character(e!)))
				} else {
					break
				}
			}
			
			if e != nil && (e!).isWhiteSpace() {
				e = skipWhiteSpace()
				guard e != nil && e! == "=" else {
					throw MustacheError.syntaxError(errorMsg)
				}
			}
			
			e = skipWhiteSpace()
			guard e != nil else {
				throw MustacheError.syntaxError(errorMsg)
			}
			guard e! == closeDelimiters[0] && consumePossibleCloseDelimiter(index: 1) else {
				throw MustacheError.syntaxError(errorMsg)
			}
			
			setDelimiters(open: Array(openD.unicodeScalars), close: Array(closeD.unicodeScalars))
		}
	}
	
	func setDelimiters(open opn: [UnicodeScalar], close: [UnicodeScalar]) {
		openDelimiters = opn
		closeDelimiters = close
	}
}
