diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Liquid.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Liquid.xcscheme
new file mode 100644
index 0000000..1a9a5bd
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/Liquid.xcscheme
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Package.swift b/Package.swift
new file mode 100644
index 0000000..877fa80
--- /dev/null
+++ b/Package.swift
@@ -0,0 +1,23 @@
+// swift-tools-version:5.0
+
+import PackageDescription
+
+let package = Package(
+ name: "Liquid",
+ products: [
+ .library(
+ name: "Liquid",
+ targets: ["Liquid"]),
+ ],
+ dependencies: [
+ // .package(url: /* package url */, from: "1.0.0"),
+ ],
+ targets: [
+ .target(
+ name: "Liquid",
+ dependencies: []),
+ .testTarget(
+ name: "LiquidTests",
+ dependencies: ["Liquid"]),
+ ]
+)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..239e8cb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# Liquid
+
+
diff --git a/Sources/Liquid/Block.swift b/Sources/Liquid/Block.swift
new file mode 100644
index 0000000..08029de
--- /dev/null
+++ b/Sources/Liquid/Block.swift
@@ -0,0 +1,37 @@
+//
+// Block.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-02.
+//
+
+import Foundation
+
+open class Block {
+ let name: String
+
+ init(name: String) {
+ self.name = name
+ }
+
+ func parse(body: BlockBody, tokenizer: Tokenizer, context: ParseContext) throws -> Bool {
+ let endTagName = "end\(name)"
+
+ var continueParsing: Bool = true
+ try body.parse(tokenizer, context: context) { (tagName, markup) in
+ guard let tagName = tagName else {
+ throw SyntaxError.unclosedTag(name)
+ }
+ if tagName == endTagName {
+ continueParsing = false
+ } else {
+ try handleUnknown(tag: tagName, markup: markup)
+ }
+ }
+ return continueParsing
+ }
+
+ func handleUnknown(tag: String, markup: String?) throws {
+ throw SyntaxError.unknownTag(tag)
+ }
+}
diff --git a/Sources/Liquid/BlockBody.swift b/Sources/Liquid/BlockBody.swift
new file mode 100644
index 0000000..0e25f82
--- /dev/null
+++ b/Sources/Liquid/BlockBody.swift
@@ -0,0 +1,42 @@
+//
+// BlockBody.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-02.
+//
+
+import Foundation
+
+class BlockBody {
+ private var nodes: [Node] = []
+
+ func parse(_ tokenizer: Tokenizer, context: ParseContext, step: (_ tagName: String?, _ markup: String?) throws -> Void) throws -> Void {
+ while let token = tokenizer.next() {
+ switch token {
+ case .text(let value):
+ nodes.append(StringNode(value))
+ case .variable(let value):
+ nodes.append(VariableNode(try Variable(string: value)))
+ case .tag(let value):
+ let tagName = value.name
+ guard let tagType = context.tags[tagName] else {
+ return try step(tagName, value.markup)
+ }
+ let tag = try tagType(tagName, value.markup, context)
+ try tag.parse(tokenizer, context: context)
+ nodes.append(tag)
+ }
+ }
+ try step(nil, nil)
+ }
+
+ func render(context: Context) throws -> [String] {
+ var result: [String] = []
+ for node in nodes {
+ result.append(contentsOf: try node.render(context: context))
+ if context.hasInterrupt { break }
+ }
+
+ return result
+ }
+}
diff --git a/Sources/Liquid/Context.swift b/Sources/Liquid/Context.swift
new file mode 100644
index 0000000..bc50e49
--- /dev/null
+++ b/Sources/Liquid/Context.swift
@@ -0,0 +1,162 @@
+//
+// Context.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-02.
+//
+
+import Foundation
+
+class Scope {
+ private var values: [String: Value]
+ let mutable: Bool
+
+ init(mutable: Bool = true, values: [String: Value] = [:]) {
+ self.mutable = mutable
+ self.values = values
+ }
+
+ subscript(key: String) -> Value? {
+ get {
+ return values[key]
+ }
+ set {
+ guard mutable else { return }
+ values[key] = newValue
+ }
+ }
+}
+
+public struct EnvironmentKey: Hashable {
+ public let rawValue: String
+
+ public init(_ rawValue: String) {
+ self.rawValue = rawValue
+ }
+}
+
+public struct RegisterKey: Hashable {
+ public let rawValue: String
+
+ public init(_ rawValue: String) {
+ self.rawValue = rawValue
+ }
+}
+
+public final class Context {
+ enum Interrupt {
+ case `break`
+ case `continue`
+ }
+
+ private var scopes: [Scope]
+ private var filters: [String: FilterFunc]
+
+ // Registers are for internal data structure storage, like forloop's and cycles to store data
+ private var registers: [String: Value] = [:]
+ //Environments are for mutliples runs of the same template with data that is persisted across
+ private(set) var environment: [String: Value]
+
+ public let encoder: Encoder
+
+ init(values: [String: Value] = [:], environment: [String: Value] = [:], filters: [String: FilterFunc] = [:], encoder: Encoder) {
+ self.scopes = [Scope(values: values)]
+ self.environment = environment
+ self.filters = filters
+ self.encoder = encoder
+ }
+
+ func withScope(_ scope: Scope? = nil, block: () throws -> T) rethrows -> T {
+ if let scope = scope {
+ pushScope(scope)
+ }
+ defer {
+ if scope != nil {
+ popScope()
+ }
+ }
+ return try block()
+ }
+
+ private func pushScope(_ scope: Scope = Scope()) {
+ scopes.append(scope)
+ }
+
+ func popScope() {
+ _ = scopes.popLast()
+ }
+
+ func value(named name: String) -> Value? {
+ var value: Value?
+ for scope in scopes.reversed() {
+ if let scopedValue = scope[name] {
+ value = scopedValue
+ break
+ }
+ }
+
+ return value
+ }
+
+ func setValue(_ value: Value, named name: String) {
+ let scope = scopes.reversed().first { $0.mutable }!
+ scope[name] = value
+ }
+
+ subscript(key: EnvironmentKey) -> Value? {
+ get {
+ return environment[key.rawValue]
+ }
+ set {
+ environment[key.rawValue] = newValue
+ }
+ }
+
+ subscript(key: EnvironmentKey, default defaultValue: @autoclosure () -> Value) -> Value {
+ get {
+ return environment[key.rawValue] ?? defaultValue()
+ }
+ set {
+ environment[key.rawValue] = newValue
+ }
+ }
+
+ subscript(key: RegisterKey) -> Value? {
+ get {
+ return registers[key.rawValue]
+ }
+ set {
+ registers[key.rawValue] = newValue
+ }
+ }
+
+ subscript(key: RegisterKey, default defaultValue: @autoclosure () -> Value) -> Value {
+ get {
+ return registers[key.rawValue] ?? defaultValue()
+ }
+ set {
+ registers[key.rawValue] = newValue
+ }
+ }
+
+ private var interrupts: [Interrupt] = []
+
+ func push(interrupt: Interrupt) {
+ interrupts.append(interrupt)
+ }
+
+ func popInterrupt() -> Interrupt {
+ guard let interrupt = interrupts.popLast() else {
+ fatalError() // TODO: exception ?
+ }
+ return interrupt
+ }
+
+ var hasInterrupt: Bool {
+ return !interrupts.isEmpty
+ }
+
+ func filter(named: String) -> FilterFunc? {
+ return filters[named]
+ }
+}
diff --git a/Sources/Liquid/Expression.swift b/Sources/Liquid/Expression.swift
new file mode 100644
index 0000000..3dcdd35
--- /dev/null
+++ b/Sources/Liquid/Expression.swift
@@ -0,0 +1,169 @@
+//
+// File.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-02.
+//
+
+import Foundation
+
+struct Expression: CustomStringConvertible {
+ var description: String {
+ switch kind {
+ case let .lookup(lookup):
+ return lookup.map { $0.description }.joined(separator: "/")
+ case .variable(let key):
+ return key
+ case .filter(let filter):
+ return filter.rawValue
+ case .value(let value):
+ return value.description
+ case .subscript(let key):
+ return "[\(key)]"
+ }
+ }
+
+ enum LookupFilter: String {
+ case size
+ case first
+ case last
+ }
+
+ indirect enum Kind {
+ case lookup([Expression])
+ case variable(key: String)
+ case filter(LookupFilter)
+ case value(Value)
+ case `subscript`(Expression)
+ }
+
+ private let kind: Kind
+
+ init(kind: Kind) {
+ self.kind = kind
+ }
+
+ init(_ value: Value) {
+ self.kind = .value(value)
+ }
+
+ init(_ filter: LookupFilter) {
+ self.kind = .filter(filter)
+ }
+
+ init(variable: String) {
+ self.kind = .variable(key: variable)
+ }
+
+ init(subscript expression: Expression) {
+ self.kind = .subscript(expression)
+ }
+
+ init(lookup: [Expression]) {
+ self.kind = .lookup(lookup)
+ }
+
+ func evaluate(context: Context) -> Value {
+ return evaluate(context: context, data: nil)
+ }
+
+ private func evaluate(context: Context, data: Value?) -> Value {
+ var result: Value?
+ switch kind {
+ case let .lookup(expressions):
+ result = expressions.first?.evaluate(context: context)
+ for expression in expressions.dropFirst() {
+ result = expression.evaluate(context: context, data: result)
+ }
+ case let .variable(key):
+ if let data = data {
+ result = data.lookup(Value(key), encoder: context.encoder)
+ } else {
+ result = context.value(named: key)
+ }
+ case let .filter(filter):
+ if let data = data {
+ if data.isDrop {
+ result = data.lookup(Value(filter.rawValue), encoder: context.encoder)
+ } else if data.isDictionary {
+ result = data.lookup(Value(filter.rawValue), encoder: context.encoder)
+ } else if data.isArray {
+ switch filter {
+ case .size:
+ result = Value(data.toArray().count)
+ case .first:
+ result = data.toArray().first
+ case .last:
+ result = data.toArray().last
+ }
+ } else {
+ switch filter {
+ case .size:
+ result = try? Filters.sizeFilter(value: data, args: [], encoder: context.encoder)
+ case .first:
+ result = try? Filters.firstFilter(value: data, args: [], encoder: context.encoder)
+ case .last:
+ result = try? Filters.lastFilter(value: data, args: [], encoder: context.encoder)
+ }
+ }
+ } else {
+ // dunno
+ }
+ case let .value(value):
+ result = value
+ case let .subscript(expression):
+ if let data = data {
+ let subscriptValue = expression.evaluate(context: context)
+ result = data.lookup(subscriptValue, encoder: context.encoder)
+ } else {
+ // TODO: throw an error?
+ }
+ }
+
+ return result ?? Value()
+ }
+
+ static func parse(_ parser: Parser) -> Expression {
+ if parser.consume(.endOfString) != nil {
+ return Expression(Value())
+ } else if let value = parser.consume(.string) {
+ return Expression(Value(value))
+ } else if let value = parser.consume(.integer) {
+ return Expression(Value(Int(value)!))
+ } else if let value = parser.consume(.decimal) {
+ return Expression(Value(Decimal(string: value)!))
+ }
+
+ var lookups: [Expression] = []
+ while let key = parser.consume(.id) {
+
+ if key == "nil" || key == "null" {
+ return Expression(Value())
+ } else if key == "true" {
+ return Expression(Value(true))
+ } else if key == "false" {
+ return Expression(Value(false))
+ }
+
+ if let lookupFilter = LookupFilter(rawValue: key) {
+ lookups.append(Expression(lookupFilter))
+ } else {
+ lookups.append(Expression(variable: key))
+ }
+
+ if parser.consume(.dot) != nil {
+ continue
+ }
+
+ // lookup keys based on []
+ while parser.consume(.openSquare) != nil {
+ lookups.append(Expression(subscript: Expression.parse(parser)))
+ _ = parser.consume(.closeSquare)
+ }
+
+ break
+ }
+
+ return Expression(lookup: lookups)
+ }
+}
diff --git a/Sources/Liquid/Extensions/CharacterSet+Extensions.swift b/Sources/Liquid/Extensions/CharacterSet+Extensions.swift
new file mode 100644
index 0000000..4abe827
--- /dev/null
+++ b/Sources/Liquid/Extensions/CharacterSet+Extensions.swift
@@ -0,0 +1,14 @@
+//
+// File.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-02.
+//
+
+import Foundation
+
+extension CharacterSet {
+ func contains(_ character: Character) -> Bool {
+ return String(character).rangeOfCharacter(from: self) != nil
+ }
+}
diff --git a/Sources/Liquid/Extensions/DateFormatter+Extensions.swift b/Sources/Liquid/Extensions/DateFormatter+Extensions.swift
new file mode 100644
index 0000000..2ce6d2e
--- /dev/null
+++ b/Sources/Liquid/Extensions/DateFormatter+Extensions.swift
@@ -0,0 +1,138 @@
+//
+// File.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-18.
+//
+
+import Foundation
+
+extension DateFormatter {
+ convenience init(strfFormatString: String) {
+ self.init()
+ self.dateFormat = dateFormatString(from: strfFormatString)
+ }
+}
+
+// %a - The abbreviated weekday name (``Sun'')
+// %A - The full weekday name (``Sunday'')
+// %b - The abbreviated month name (``Jan'')
+// %B - The full month name (``January'')
+// %c - The preferred local date and time representation
+// %d - Day of the month (01..31)
+// %H - Hour of the day, 24-hour clock (00..23)
+// %I - Hour of the day, 12-hour clock (01..12)
+// %j - Day of the year (001..366)
+// %m - Month of the year (01..12)
+// %M - Minute of the hour (00..59)
+// %p - Meridian indicator (``AM'' or ``PM'')
+// %s - Number of seconds since 1970-01-01 00:00:00 UTC.
+// %S - Second of the minute (00..60)
+// %U - Week number of the current year,
+// starting with the first Sunday as the first
+// day of the first week (00..53)
+// %W - Week number of the current year,
+// starting with the first Monday as the first
+// day of the first week (00..53)
+// %w - Day of the week (Sunday is 0, 0..6)
+// %x - Preferred representation for the date alone, no time
+// %X - Preferred representation for the time alone, no date
+// %y - Year without a century (00..99)
+// %Y - Year with century
+// %Z - Time zone name
+// %% - Literal ``%'' character
+enum STRFTokens: Character {
+ case abbreviatedWeekdayName = "a"
+ case fullWeekdayName = "A"
+ case abbreviatedMonthName = "b"
+ case fullMonthName = "B"
+ case preferredLocalDateAndTime = "c"
+ case dayOfMonth = "d"
+ case hourOfDay24 = "H"
+ case hourOfDay12 = "I"
+ case dayOfYear = "j"
+ case monthOfYear = "m"
+ case minuteOfHour = "M"
+ case meridian = "p"
+ case secondsSince1970 = "s"
+ case secondOfMinute = "S"
+ case weekNumberOfYearFirstSunday = "U"
+ case weekNumberOfYearFirstMonday = "W"
+ case dayOfWeek = "w"
+ case preferredDateRepresentation = "x"
+ case preferredTimeRepresentation = "X"
+ case yearWithoutCentury = "y"
+ case yearWithCentury = "Y"
+ case timeZoneName = "Z"
+ case literalPercent = "%"
+
+ func toFormatStringRepresentation() -> String {
+ switch self {
+ case .abbreviatedWeekdayName:
+ return "EEEE"
+ case .fullWeekdayName:
+ return "E"
+ case .abbreviatedMonthName:
+ return "MMM"
+ case .fullMonthName:
+ return "MMMM"
+ case .preferredLocalDateAndTime:
+ return ""
+ case .dayOfMonth:
+ return "dd"
+ case .hourOfDay24:
+ return "HH"
+ case .hourOfDay12:
+ return "hh"
+ case .dayOfYear:
+ return "DDD"
+ case .monthOfYear:
+ return "MM"
+ case .minuteOfHour:
+ return "mm"
+ case .meridian:
+ return "a"
+ case .secondsSince1970:
+ return ""
+ case .secondOfMinute:
+ return "ss"
+ case .weekNumberOfYearFirstSunday:
+ return "ww"
+ case .weekNumberOfYearFirstMonday:
+ return "ww" // TODO: adjust U and W for sun/mon week start days
+ case .dayOfWeek:
+ return "F"
+ case .preferredDateRepresentation:
+ return ""
+ case .preferredTimeRepresentation:
+ return ""
+ case .yearWithoutCentury:
+ return "yy"
+ case .yearWithCentury:
+ return "y"
+ case .timeZoneName:
+ return "zzz"
+ case .literalPercent:
+ return "%"
+ }
+ }
+}
+func dateFormatString(from inputString: String) -> String {
+ var format = ""
+ let scanner = Scanner(string: inputString)
+ while let c = scanner.liquid_scanCharacter() {
+ switch c {
+ case "%":
+ guard let next = scanner.liquid_scanCharacter() else { fatalError() }
+ guard let token = STRFTokens(rawValue: next) else { fatalError() }
+ format.append(token.toFormatStringRepresentation())
+ default:
+ if let _ = STRFTokens(rawValue: c) {
+ format.append("'\(c)'")
+ } else {
+ format.append(c)
+ }
+ }
+ }
+ return format
+}
diff --git a/Sources/Liquid/Extensions/Decimal+Extensions.swift b/Sources/Liquid/Extensions/Decimal+Extensions.swift
new file mode 100644
index 0000000..222ec82
--- /dev/null
+++ b/Sources/Liquid/Extensions/Decimal+Extensions.swift
@@ -0,0 +1,25 @@
+//
+// File.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-16.
+//
+
+import Foundation
+
+extension Decimal {
+ var intValue: Int {
+ return NSDecimalNumber(decimal: self).intValue
+ }
+
+ var doubleValue: Double {
+ return NSDecimalNumber(decimal: self).doubleValue
+ }
+
+ func round(scale: Int = 0) -> Decimal {
+ var input: Decimal = self
+ var output: Decimal = 0
+ NSDecimalRound(&output, &input, scale, .plain)
+ return output
+ }
+}
diff --git a/Sources/Liquid/Extensions/Scanner+Extensions.swift b/Sources/Liquid/Extensions/Scanner+Extensions.swift
new file mode 100644
index 0000000..dd63850
--- /dev/null
+++ b/Sources/Liquid/Extensions/Scanner+Extensions.swift
@@ -0,0 +1,117 @@
+//
+// File.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-05.
+//
+
+import Foundation
+
+private let validNumberCharacters = CharacterSet.decimalDigits.union(CharacterSet(charactersIn: "-."))
+
+extension Scanner {
+ var liquid_currentIndex: String.Index {
+ get {
+ let string = self.string
+ var index = string.toUTF16Index(scanLocation)
+
+ var delta = 0
+ while index != string.endIndex && index.samePosition(in: string) == nil {
+ delta += 1
+ index = string.toUTF16Index(scanLocation + delta)
+ }
+
+ return index
+ }
+ set {
+ scanLocation = string.toUTF16Offset(newValue)
+ }
+ }
+
+ var liquid_remainingString: String {
+ return String(self.string[liquid_currentIndex...])
+ }
+
+ func liquid_peekCharacter(at: Int = 0) -> Character? {
+ guard !isAtEnd else { return nil }
+ return string[string.index(liquid_currentIndex, offsetBy: at)]
+ }
+
+ func liquid_scanCharacter() -> Character? {
+ let currentIndex = liquid_currentIndex
+ let string = self.string
+ guard currentIndex != string.endIndex else { return nil }
+ defer { self.liquid_currentIndex = string.index(after: currentIndex) }
+ return string[currentIndex]
+ }
+
+ func liquid_scanInt() -> String? {
+ var int: Int = 0
+ if self.scanInt(&int) {
+ return "\(int)"
+ }
+ return nil
+ }
+
+ func liquid_scanDecimal() -> String? {
+ guard let character = liquid_peekCharacter(), validNumberCharacters.contains(character) else { return nil }
+ let currentLocation = self.liquid_currentIndex
+ var decimal: Decimal = 0
+ if self.scanDecimal(&decimal) {
+ // Check to make sure we didn't just scan a number that is part of a range statement, i.e. (1..5)
+ // If we did, then reverse out of the scan, resetting scan position, and return nil
+ guard liquid_peekCharacter() != ".", liquid_peekCharacter(at: -1) != "." else {
+ self.liquid_currentIndex = currentLocation
+ return nil
+ }
+ // Check to make sure there's a decimal place, we don't want to capture Integer values
+ guard string[currentLocation.. String? {
+ let currentIndex = liquid_currentIndex
+
+ let substringEnd = string[currentIndex...].firstIndex(where: { !set.contains($0) }) ?? string.endIndex
+ guard currentIndex != substringEnd else { return nil }
+
+ let substring = string[currentIndex ..< substringEnd]
+ self.liquid_currentIndex = substringEnd
+ return String(substring)
+ }
+
+ func liquid_scanRegex(_ regex: NSRegularExpression, captureGroup: Int = 0) -> String? {
+ let range = NSRange((liquid_currentIndex..(match.range(at: captureGroup), in: self.string) else {
+ return nil
+ }
+
+ self.scanLocation += match.range.length
+
+ return String(self.string[matchRange])
+ }
+
+ func liquid_scanRegex(_ regexString: String, captureGroup: Int = 0) throws -> String? {
+ let regex = try NSRegularExpression(pattern: regexString)
+ return liquid_scanRegex(regex, captureGroup: captureGroup)
+ }
+}
+
+private extension String {
+ func toUTF16Index(_ offset: Int) -> Index {
+ return self.utf16.index(self.utf16.startIndex, offsetBy: offset)
+ }
+
+ func toUTF16Offset(_ idx: Index) -> Int {
+ return self.utf16.distance(from: self.utf16.startIndex, to: idx)
+ }
+}
diff --git a/Sources/Liquid/Extensions/String+Extensions.swift b/Sources/Liquid/Extensions/String+Extensions.swift
new file mode 100644
index 0000000..2cda391
--- /dev/null
+++ b/Sources/Liquid/Extensions/String+Extensions.swift
@@ -0,0 +1,36 @@
+//
+// File.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-15.
+//
+
+import Foundation
+
+extension String {
+ func strip() -> String {
+ return self.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
+ }
+
+ func rstrip() -> String {
+ var idx = endIndex
+ while idx != startIndex {
+ idx = self.index(before: idx)
+ if !CharacterSet.whitespacesAndNewlines.contains(self[idx]) {
+ break
+ }
+ }
+ return String(self[startIndex...idx])
+ }
+
+ func lstrip() -> String {
+ var idx = startIndex
+ while idx != endIndex {
+ if !CharacterSet.whitespacesAndNewlines.contains(self[idx]) {
+ break
+ }
+ idx = self.index(after: idx)
+ }
+ return String(self[idx.. Value {
+ var result = value.toString()
+ args.forEach {
+ result += $0.toString()
+ }
+ return Value(result)
+ }
+
+ static func prependFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard let first = args.first, args.count == 1 else {
+ throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName())
+ }
+ return Value(first.toString() + value.toString())
+ }
+
+ static func downcaseFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ return Value(value.toString().lowercased())
+ }
+
+ static func upcaseFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ return Value(value.toString().uppercased())
+ }
+
+ static func capitalizeFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ let string = value.toString()
+ return Value(string.prefix(1).capitalized + string.dropFirst())
+ }
+
+ static func stripFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ return Value(value.toString().strip())
+ }
+
+ static func rstripFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ return Value(value.toString().rstrip())
+ }
+
+ static func lstripFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ return Value(value.toString().lstrip())
+ }
+
+ static func stripNewlinesFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ return Value(value.toString().replacingOccurrences(of: "\\s", with: "", options: [.regularExpression]))
+ }
+
+ static func newlineToBRFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ return Value(value.toString().replacingOccurrences(of: "\n", with: "
\n"))
+ }
+
+ static func escapeFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ return Value(value.toString().htmlEscape(decimal: true, useNamedReferences: true))
+ }
+
+ static func escapeOnceFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ return Value(value.toString().htmlUnescape().htmlEscape(decimal: true, useNamedReferences: true))
+ }
+
+ static func urlEncodeFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ let inputString = value.toString().replacingOccurrences(of: " ", with: "+")
+ // Based on RFC3986: https://tools.ietf.org/html/rfc3986#page-13, and including the `+` char which was already escaped above.
+ let allowedCharset = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-._~/?"))
+ return Value(inputString.addingPercentEncoding(withAllowedCharacters: allowedCharset) ?? inputString)
+ }
+
+ static func urlDecodeFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ let string = value.toString().replacingOccurrences(of: "+", with: " ")
+ return Value(string.removingPercentEncoding ?? string)
+ }
+
+ static func stripHTMLFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ return Value(value.toString().replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression))
+ }
+
+ static func truncateFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard !args.isEmpty, let firstArg = args.first else {
+ throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName())
+ }
+
+ let suffix = args.count == 2 ? args[1].toString() : "…"
+ let length = firstArg.toInteger() - suffix.count
+ let string = value.toString()
+ let endIndex = string.index(string.startIndex, offsetBy: length)
+ return Value(String((string[string.startIndex.. Value {
+ guard !args.isEmpty, let firstArg = args.first else {
+ throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName())
+ }
+
+ let suffix = args.count == 2 ? args[1].toString() : "…"
+ let wordCount = firstArg.toInteger()
+ let string = value.toString()
+ var lastEnumeratedIndex = string.startIndex
+ var words: [String] = []
+ string.enumerateSubstrings(in: string.startIndex..., options: [.byWords, .localized]) { (word, range, _, stop) in
+ guard let word = word else { return }
+
+ words.append(word)
+ lastEnumeratedIndex = range.upperBound
+ if words.count >= wordCount {
+ stop = true
+ }
+ }
+
+ if lastEnumeratedIndex == string.endIndex {
+ return value
+ }
+
+ return Value(words.joined(separator: " ").appending(suffix))
+ }
+
+ static func plusFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.count == 1, let arg = args.first else {
+ throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName())
+ }
+ if value.isInteger && arg.isInteger {
+ return Value(value.toInteger() + arg.toInteger())
+ }
+ return Value(value.toDecimal() + arg.toDecimal())
+ }
+
+ static func minusFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.count == 1, let arg = args.first else {
+ throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName())
+ }
+ if value.isInteger && arg.isInteger {
+ return Value(value.toInteger() - arg.toInteger())
+ }
+ return Value(value.toDecimal() - arg.toDecimal())
+ }
+
+ static func multipliedByFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.count == 1, let arg = args.first else {
+ throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName())
+ }
+ if value.isInteger && arg.isInteger {
+ return Value(value.toInteger() * arg.toInteger())
+ }
+ return Value(value.toDecimal() * arg.toDecimal())
+ }
+
+ static func dividedByFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.count == 1, let arg = args.first else {
+ throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName())
+ }
+ if value.isInteger && arg.isInteger {
+ return Value(value.toInteger() / arg.toInteger())
+ }
+ return Value(value.toDecimal() / arg.toDecimal())
+ }
+
+ static func absFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ if value.isInteger {
+ return Value(abs(value.toInteger()))
+ }
+ return Value(abs(value.toDecimal()))
+ }
+
+ static func ceilFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ if value.isInteger {
+ return value
+ }
+ return Value(ceil(value.toDecimal().doubleValue))
+ }
+
+ static func floorFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ if value.isInteger {
+ return value
+ }
+ return Value(floor(value.toDecimal().doubleValue))
+ }
+
+ static func roundFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.count <= 1 else {
+ throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName())
+ }
+ guard !value.isInteger else { return value }
+ let result = value.toDecimal().round(scale: args.first?.toInteger() ?? 0)
+ return Value(result)
+ }
+
+ static func moduloFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.count == 1, let arg = args.first else {
+ throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName())
+ }
+
+ if value.isInteger && arg.isInteger {
+ return Value(value.toInteger() % arg.toInteger())
+ }
+ return Value(value.toDecimal().doubleValue.truncatingRemainder(dividingBy: arg.toDecimal().doubleValue))
+ }
+
+ static func splitFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard let separator = args.first?.toString() else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ let components = value.toString().components(separatedBy: separator)
+ return Value(components.map { Value($0) })
+ }
+
+ static func joinFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard let separator = args.first?.toString() else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ return Value(value.toArray().map { $0.toString() }.joined(separator: separator))
+ }
+
+ static func uniqueFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ var seen: Set = []
+ var unique: [Value] = []
+ value.toArray().forEach {
+ if seen.contains($0) { return }
+ seen.insert($0)
+ unique.append($0)
+ }
+ return Value(unique)
+ }
+
+ static func sizeFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ return Value(value.size)
+ }
+
+ static func firstFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ return value.toArray().first ?? Value()
+ }
+ static func lastFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ return value.toArray().last ?? Value()
+ }
+ static func defaultFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.count == 1, let arg = args.first else {
+ throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName())
+ }
+ if value.isNil || (value.size == 0 && (value.isString || value.isArray || value.isDictionary)) {
+ return arg
+ } else {
+ return value
+ }
+ }
+ static func replaceFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.count == 2 else {
+ throw RuntimeError.invalidArgCount(expected: 2, received: args.count, tag: tagName())
+ }
+ let target = args[0].toString()
+ let replacement = args[1].toString()
+ return Value(value.toString().replacingOccurrences(of: target, with: replacement))
+ }
+
+ static func replaceFirstFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.count == 2 else {
+ throw RuntimeError.invalidArgCount(expected: 2, received: args.count, tag: tagName())
+ }
+ let target = args[0].toString()
+ let replacement = args[1].toString()
+ let string = value.toString()
+ guard let range = string.range(of: target) else {
+ return value
+ }
+ return Value(string.replacingCharacters(in: range, with: replacement))
+ }
+
+ static func removeFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.count == 1 else {
+ throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName())
+ }
+ let target = args[0].toString()
+ return Value(value.toString().replacingOccurrences(of: target, with: ""))
+ }
+
+ static func removeFirstFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.count == 1 else {
+ throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName())
+ }
+ let target = args[0].toString()
+ let string = value.toString()
+ guard let range = string.range(of: target) else {
+ return value
+ }
+ return Value(string.replacingCharacters(in: range, with: ""))
+ }
+
+ static func sliceFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard !args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName())
+ }
+
+ let offset = args[0].toInteger()
+ let length = args.count == 2 ? args[1].toInteger() : 1
+ let string = value.toString()
+
+ let startIndex: String.Index
+ let endIndex: String.Index
+ if offset < 0 {
+ startIndex = string.endIndex
+ endIndex = string.startIndex
+ } else {
+ startIndex = string.startIndex
+ endIndex = string.endIndex
+ }
+ guard
+ let sliceStartIndex = string.index(startIndex, offsetBy: offset, limitedBy: endIndex),
+ let sliceEndIndex = string.index(sliceStartIndex, offsetBy: length, limitedBy: string.endIndex)
+ else {
+ return Value()
+ }
+
+ return Value(String(string[sliceStartIndex.. Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ return Value(value.toArray().reversed())
+ }
+
+ static func compactFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.isEmpty else {
+ throw RuntimeError.invalidArgCount(expected: 0, received: args.count, tag: tagName())
+ }
+ return Value(value.toArray().filter { !$0.isNil })
+ }
+
+ static func mapFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.count == 1 else {
+ throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName())
+ }
+
+ let property = args[0]
+ let results = value.toArray().map { $0.lookup(property, encoder: encoder) }
+ return Value(results)
+ }
+
+ static func concatFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.count == 1 else {
+ throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName())
+ }
+ guard args[0].isArray else {
+ throw RuntimeError.wrongType("concat requires an array argument")
+ }
+
+ var array = value.toArray()
+ array.append(contentsOf: args[0].toArray())
+ return Value(array)
+ }
+
+ static func sortFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.count <= 1 else {
+ throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName())
+ }
+
+ if let property = args.first {
+ return Value(value.toArray().sorted(by: { (lhs, rhs) -> Bool in
+ return lhs.lookup(property, encoder: encoder) < rhs.lookup(property, encoder: encoder)
+ }))
+ } else {
+ return Value(value.toArray().sorted())
+ }
+ }
+
+ static func sortNaturalFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.count <= 1 else {
+ throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName())
+ }
+
+ if let property = args.first {
+ return Value(value.toArray().sorted(by: { (lhs, rhs) -> Bool in
+ return lhs.lookup(property, encoder: encoder).toString().caseInsensitiveCompare(rhs.lookup(property, encoder: encoder).toString()) == .orderedAscending
+ }))
+ } else {
+ return Value(value.toArray().sorted(by: { (lhs, rhs) -> Bool in
+ return lhs.toString().caseInsensitiveCompare(rhs.toString()) == .orderedAscending
+ }))
+ }
+ }
+
+ static func dateFilter(value: Value, args: [Value], encoder: Encoder) throws -> Value {
+ guard args.count == 1 else {
+ throw RuntimeError.invalidArgCount(expected: 1, received: args.count, tag: tagName())
+ }
+
+ guard let date = convertValueToDate(value, encoder: encoder) else {
+ throw RuntimeError.wrongType("Could not convert to date")
+ }
+
+ let format = args[0].toString()
+ let formatter: DateFormatter
+ if format.isEmpty {
+ formatter = DateFormatter()
+ formatter.dateStyle = .short
+ formatter.timeStyle = .medium
+ } else {
+ formatter = DateFormatter(strfFormatString: format)
+ }
+ return Value(formatter.string(from: date))
+ }
+
+ private static func convertValueToDate(_ value: Value, encoder: Encoder) -> Date? {
+ if value.isInteger {
+ return Date(timeIntervalSince1970: Double(value.toInteger()))
+ }
+
+ let string = value.toString()
+
+ if let integer = Int(string) {
+ return Date(timeIntervalSince1970: Double(integer))
+ }
+
+ if ["now", "today"].contains(string.lowercased()) {
+ return Date()
+ }
+ return encoder.dateEncodingStrategry.date(from: string)
+ }
+}
diff --git a/Sources/Liquid/Filter.swift b/Sources/Liquid/Filter.swift
new file mode 100644
index 0000000..22ce0b0
--- /dev/null
+++ b/Sources/Liquid/Filter.swift
@@ -0,0 +1,20 @@
+//
+// Filter.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-03.
+//
+
+import Foundation
+
+struct Filter {
+ let name: String
+ let args: [Expression]
+
+ init(name: String, args: [Expression]) {
+ self.name = name
+ self.args = args
+ }
+}
+
+public typealias FilterFunc = (_ value: Value, _ args: [Value], _ encoder: Encoder) throws -> Value
diff --git a/Sources/Liquid/HTMLEntities/Constants.swift b/Sources/Liquid/HTMLEntities/Constants.swift
new file mode 100644
index 0000000..cf72f22
--- /dev/null
+++ b/Sources/Liquid/HTMLEntities/Constants.swift
@@ -0,0 +1,704 @@
+/*
+ * Copyright IBM Corporation 2016, 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Linux toolchain requires Foundation to resolve `String` class's `hasSuffix()` function
+#if os(Linux)
+ import Foundation
+#endif
+
+// Replacement character U+FFFD
+let replacementCharacterAsUInt32: UInt32 = 0xFFFD
+
+// Generated from
+// https://www.w3.org/TR/html5/syntax.html#tokenizing-character-references
+// "Find the row with that number in the first column, and return a character token for
+// the Unicode character given in the second column of that row."
+let deprecatedNumericDecodeMap: [UInt32: UInt32] = [
+ 0x00: 0xFFFD, 0x80: 0x20AC, 0x82: 0x201A, 0x83: 0x0192, 0x84: 0x201E, 0x85: 0x2026, 0x86: 0x2020,
+ 0x87: 0x2021, 0x88: 0x02C6, 0x89: 0x2030, 0x8A: 0x0160, 0x8B: 0x2039, 0x8C: 0x0152, 0x8E: 0x017D,
+ 0x91: 0x2018, 0x92: 0x2019, 0x93: 0x201C, 0x94: 0x201D, 0x95: 0x2022, 0x96: 0x2013, 0x97: 0x2014,
+ 0x98: 0x02DC, 0x99: 0x2122, 0x9A: 0x0161, 0x9B: 0x203A, 0x9C: 0x0153, 0x9E: 0x017E, 0x9F: 0x0178
+]
+
+// Generated from
+// https://www.w3.org/TR/html5/syntax.html#tokenizing-character-references
+// "[I]f the number is in the range 0x0001 to 0x0008, 0x000D to 0x001F, 0x007F
+// to 0x009F, 0xFDD0 to 0xFDEF, or is one of 0x000B, 0xFFFE, 0xFFFF, 0x1FFFE,
+// 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE,
+// 0x5FFFF, 0x6FFFE, 0x6FFFF, 0x7FFFE, 0x7FFFF, 0x8FFFE, 0x8FFFF, 0x9FFFE,
+// 0x9FFFF, 0xAFFFE, 0xAFFFF, 0xBFFFE, 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE,
+// 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF, 0x10FFFE, or 0x10FFFF, then
+// this is a parse error."
+let disallowedNumericReferences: Set = [
+ 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0xB, 0xD, 0xE, 0xF, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
+ 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0xFDD0, 0xFDD1, 0xFDD2, 0xFDD3, 0xFDD4,
+ 0xFDD5, 0xFDD6, 0xFDD7, 0xFDD8, 0xFDD9, 0xFDDA, 0xFDDB, 0xFDDC, 0xFDDD, 0xFDDE, 0xFDDF, 0xFDE0,
+ 0xFDE1, 0xFDE2, 0xFDE3, 0xFDE4, 0xFDE5, 0xFDE6, 0xFDE7, 0xFDE8, 0xFDE9, 0xFDEA, 0xFDEB, 0xFDEC,
+ 0xFDED, 0xFDEE, 0xFDEF, 0xFFFE, 0xFFFF, 0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, 0x3FFFF,
+ 0x4FFFE, 0x4FFFF, 0x5FFFE, 0x5FFFF, 0x6FFFE, 0x6FFFF, 0x7FFFE, 0x7FFFF, 0x8FFFE, 0x8FFFF,
+ 0x9FFFE, 0x9FFFF, 0xAFFFE, 0xAFFFF, 0xBFFFE, 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE, 0xDFFFF,
+ 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF, 0x10FFFE, 0x10FFFF
+]
+
+// Only encode to named character references that end with ;
+// If multiple exists for a given character, i.e., 'AMP;' and 'amp;', pick the one
+// that is shorter and/or all lowercase
+let namedCharactersEncodeMap = namedCharactersDecodeMap.inverting { existing, new in
+ let isExistingLegacy = !existing.hasSuffix(";")
+ let isNewLegacy = !new.hasSuffix(";")
+
+ #if swift(>=3.2)
+ let existingCount = existing.count
+ let newCount = new.count
+ #else
+ let existingCount = existing.characters.count
+ let newCount = new.characters.count
+ #endif
+
+ if isExistingLegacy && !isNewLegacy {
+ // prefer non-legacy
+ return new
+ }
+
+ if !isExistingLegacy && isNewLegacy {
+ // prefer non-legacy
+ return existing
+ }
+
+ if existingCount < newCount {
+ // if both are same type, prefer shorter name
+ return existing
+ }
+
+ if newCount < existingCount {
+ // if both are same type, prefer shorter name
+ return new
+ }
+
+ if new == new.lowercased() {
+ // if both are same type and same length, prefer lowercase name
+ return new
+ }
+
+ // new isn't better than existing
+ // return existing
+ return existing
+}
+
+// Entities that map to more than one character in Swift
+// E.g., their decoded form spans more than one extended grapheme cluster
+let specialNamedCharactersDecodeMap: [String: String] = [
+ "fjlig;": "\u{66}\u{6A}",
+ "ThickSpace;": "\u{205F}\u{200A}"
+]
+
+// Range of string lengths of legacy named characters
+// Should be 2...6, but generate statically to avoid hardcoding numbers
+let legacyNamedCharactersLengthRange: CountableClosedRange = { () -> CountableClosedRange in
+ var min = Int.max, max = Int.min
+
+ for (name, _) in legacyNamedCharactersDecodeMap {
+ #if swift(>=3.2)
+ let length = name.count
+ #else
+ let length = name.characters.count
+ #endif
+ min = length < min ? length : min
+ max = length > max ? length : max
+ }
+
+ return min...max
+}()
+
+// Named character references that may be parsed without an ending ;
+let legacyNamedCharactersDecodeMap: [String: Character] = [
+ "Aacute": Character(Unicode.Scalar(0xC1)!), "aacute": Character(Unicode.Scalar(0xE1)!), "Acirc": Character(Unicode.Scalar(0xC2)!), "acirc": Character(Unicode.Scalar(0xE2)!),
+ "acute": Character(Unicode.Scalar(0xB4)!), "AElig": Character(Unicode.Scalar(0xC6)!), "aelig": Character(Unicode.Scalar(0xE6)!), "Agrave": Character(Unicode.Scalar(0xC0)!),
+ "agrave": Character(Unicode.Scalar(0xE0)!), "AMP": Character(Unicode.Scalar(0x26)!), "amp": Character(Unicode.Scalar(0x26)!), "Aring": Character(Unicode.Scalar(0xC5)!),
+ "aring": Character(Unicode.Scalar(0xE5)!), "Atilde": Character(Unicode.Scalar(0xC3)!), "atilde": Character(Unicode.Scalar(0xE3)!), "Auml": Character(Unicode.Scalar(0xC4)!),
+ "auml": Character(Unicode.Scalar(0xE4)!), "brvbar": Character(Unicode.Scalar(0xA6)!), "Ccedil": Character(Unicode.Scalar(0xC7)!), "ccedil": Character(Unicode.Scalar(0xE7)!),
+ "cedil": Character(Unicode.Scalar(0xB8)!), "cent": Character(Unicode.Scalar(0xA2)!), "COPY": Character(Unicode.Scalar(0xA9)!), "copy": Character(Unicode.Scalar(0xA9)!),
+ "curren": Character(Unicode.Scalar(0xA4)!), "deg": Character(Unicode.Scalar(0xB0)!), "divide": Character(Unicode.Scalar(0xF7)!), "Eacute": Character(Unicode.Scalar(0xC9)!),
+ "eacute": Character(Unicode.Scalar(0xE9)!), "Ecirc": Character(Unicode.Scalar(0xCA)!), "ecirc": Character(Unicode.Scalar(0xEA)!), "Egrave": Character(Unicode.Scalar(0xC8)!),
+ "egrave": Character(Unicode.Scalar(0xE8)!), "ETH": Character(Unicode.Scalar(0xD0)!), "eth": Character(Unicode.Scalar(0xF0)!), "Euml": Character(Unicode.Scalar(0xCB)!),
+ "euml": Character(Unicode.Scalar(0xEB)!), "frac12": Character(Unicode.Scalar(0xBD)!), "frac14": Character(Unicode.Scalar(0xBC)!), "frac34": Character(Unicode.Scalar(0xBE)!),
+ "GT": Character(Unicode.Scalar(0x3E)!), "gt": Character(Unicode.Scalar(0x3E)!), "Iacute": Character(Unicode.Scalar(0xCD)!), "iacute": Character(Unicode.Scalar(0xED)!),
+ "Icirc": Character(Unicode.Scalar(0xCE)!), "icirc": Character(Unicode.Scalar(0xEE)!), "iexcl": Character(Unicode.Scalar(0xA1)!), "Igrave": Character(Unicode.Scalar(0xCC)!),
+ "igrave": Character(Unicode.Scalar(0xEC)!), "iquest": Character(Unicode.Scalar(0xBF)!), "Iuml": Character(Unicode.Scalar(0xCF)!), "iuml": Character(Unicode.Scalar(0xEF)!),
+ "laquo": Character(Unicode.Scalar(0xAB)!), "LT": Character(Unicode.Scalar(0x3C)!), "lt": Character(Unicode.Scalar(0x3C)!), "macr": Character(Unicode.Scalar(0xAF)!),
+ "micro": Character(Unicode.Scalar(0xB5)!), "middot": Character(Unicode.Scalar(0xB7)!), "nbsp": Character(Unicode.Scalar(0xA0)!), "not": Character(Unicode.Scalar(0xAC)!),
+ "Ntilde": Character(Unicode.Scalar(0xD1)!), "ntilde": Character(Unicode.Scalar(0xF1)!), "Oacute": Character(Unicode.Scalar(0xD3)!), "oacute": Character(Unicode.Scalar(0xF3)!),
+ "Ocirc": Character(Unicode.Scalar(0xD4)!), "ocirc": Character(Unicode.Scalar(0xF4)!), "Ograve": Character(Unicode.Scalar(0xD2)!), "ograve": Character(Unicode.Scalar(0xF2)!),
+ "ordf": Character(Unicode.Scalar(0xAA)!), "ordm": Character(Unicode.Scalar(0xBA)!), "Oslash": Character(Unicode.Scalar(0xD8)!), "oslash": Character(Unicode.Scalar(0xF8)!),
+ "Otilde": Character(Unicode.Scalar(0xD5)!), "otilde": Character(Unicode.Scalar(0xF5)!), "Ouml": Character(Unicode.Scalar(0xD6)!), "ouml": Character(Unicode.Scalar(0xF6)!),
+ "para": Character(Unicode.Scalar(0xB6)!), "plusmn": Character(Unicode.Scalar(0xB1)!), "pound": Character(Unicode.Scalar(0xA3)!), "QUOT": Character(Unicode.Scalar(0x22)!),
+ "quot": Character(Unicode.Scalar(0x22)!), "raquo": Character(Unicode.Scalar(0xBB)!), "REG": Character(Unicode.Scalar(0xAE)!), "reg": Character(Unicode.Scalar(0xAE)!),
+ "sect": Character(Unicode.Scalar(0xA7)!), "shy": Character(Unicode.Scalar(0xAD)!), "sup1": Character(Unicode.Scalar(0xB9)!), "sup2": Character(Unicode.Scalar(0xB2)!),
+ "sup3": Character(Unicode.Scalar(0xB3)!), "szlig": Character(Unicode.Scalar(0xDF)!), "THORN": Character(Unicode.Scalar(0xDE)!), "thorn": Character(Unicode.Scalar(0xFE)!),
+ "times": Character(Unicode.Scalar(0xD7)!), "Uacute": Character(Unicode.Scalar(0xDA)!), "uacute": Character(Unicode.Scalar(0xFA)!), "Ucirc": Character(Unicode.Scalar(0xDB)!),
+ "ucirc": Character(Unicode.Scalar(0xFB)!), "Ugrave": Character(Unicode.Scalar(0xD9)!), "ugrave": Character(Unicode.Scalar(0xF9)!), "uml": Character(Unicode.Scalar(0xA8)!),
+ "Uuml": Character(Unicode.Scalar(0xDC)!), "uuml": Character(Unicode.Scalar(0xFC)!), "Yacute": Character(Unicode.Scalar(0xDD)!), "yacute": Character(Unicode.Scalar(0xFD)!),
+ "yen": Character(Unicode.Scalar(0xA5)!), "yuml": Character(Unicode.Scalar(0xFF)!)
+]
+
+// Split map into two halves; otherwise, segmentation fault when compiling
+let namedCharactersDecodeMap = namedCharactersDecodeMap1.updating(namedCharactersDecodeMap2)
+
+let namedCharactersDecodeMap1: [String: Character] = [
+ "Aacute;": Character(Unicode.Scalar(0xC1)!), "aacute;": Character(Unicode.Scalar(0xE1)!), "Abreve;": Character(Unicode.Scalar(0x102)!), "abreve;": Character(Unicode.Scalar(0x103)!),
+ "ac;": Character(Unicode.Scalar(0x223E)!), "acd;": Character(Unicode.Scalar(0x223F)!), "acE;": "\u{223E}\u{333}", "Acirc;": Character(Unicode.Scalar(0xC2)!),
+ "acirc;": Character(Unicode.Scalar(0xE2)!), "acute;": Character(Unicode.Scalar(0xB4)!), "Acy;": Character(Unicode.Scalar(0x410)!), "acy;": Character(Unicode.Scalar(0x430)!),
+ "AElig;": Character(Unicode.Scalar(0xC6)!), "aelig;": Character(Unicode.Scalar(0xE6)!), "af;": Character(Unicode.Scalar(0x2061)!), "Afr;": Character(Unicode.Scalar(0x1D504)!),
+ "afr;": Character(Unicode.Scalar(0x1D51E)!), "Agrave;": Character(Unicode.Scalar(0xC0)!), "agrave;": Character(Unicode.Scalar(0xE0)!), "alefsym;": Character(Unicode.Scalar(0x2135)!),
+ "aleph;": Character(Unicode.Scalar(0x2135)!), "Alpha;": Character(Unicode.Scalar(0x391)!), "alpha;": Character(Unicode.Scalar(0x3B1)!), "Amacr;": Character(Unicode.Scalar(0x100)!),
+ "amacr;": Character(Unicode.Scalar(0x101)!), "amalg;": Character(Unicode.Scalar(0x2A3F)!), "AMP;": Character(Unicode.Scalar(0x26)!), "amp;": Character(Unicode.Scalar(0x26)!),
+ "And;": Character(Unicode.Scalar(0x2A53)!), "and;": Character(Unicode.Scalar(0x2227)!), "andand;": Character(Unicode.Scalar(0x2A55)!), "andd;": Character(Unicode.Scalar(0x2A5C)!),
+ "andslope;": Character(Unicode.Scalar(0x2A58)!), "andv;": Character(Unicode.Scalar(0x2A5A)!), "ang;": Character(Unicode.Scalar(0x2220)!), "ange;": Character(Unicode.Scalar(0x29A4)!),
+ "angle;": Character(Unicode.Scalar(0x2220)!), "angmsd;": Character(Unicode.Scalar(0x2221)!), "angmsdaa;": Character(Unicode.Scalar(0x29A8)!), "angmsdab;": Character(Unicode.Scalar(0x29A9)!),
+ "angmsdac;": Character(Unicode.Scalar(0x29AA)!), "angmsdad;": Character(Unicode.Scalar(0x29AB)!), "angmsdae;": Character(Unicode.Scalar(0x29AC)!), "angmsdaf;": Character(Unicode.Scalar(0x29AD)!),
+ "angmsdag;": Character(Unicode.Scalar(0x29AE)!), "angmsdah;": Character(Unicode.Scalar(0x29AF)!), "angrt;": Character(Unicode.Scalar(0x221F)!), "angrtvb;": Character(Unicode.Scalar(0x22BE)!),
+ "angrtvbd;": Character(Unicode.Scalar(0x299D)!), "angsph;": Character(Unicode.Scalar(0x2222)!), "angst;": Character(Unicode.Scalar(0xC5)!), "angzarr;": Character(Unicode.Scalar(0x237C)!),
+ "Aogon;": Character(Unicode.Scalar(0x104)!), "aogon;": Character(Unicode.Scalar(0x105)!), "Aopf;": Character(Unicode.Scalar(0x1D538)!), "aopf;": Character(Unicode.Scalar(0x1D552)!),
+ "ap;": Character(Unicode.Scalar(0x2248)!), "apacir;": Character(Unicode.Scalar(0x2A6F)!), "apE;": Character(Unicode.Scalar(0x2A70)!), "ape;": Character(Unicode.Scalar(0x224A)!),
+ "apid;": Character(Unicode.Scalar(0x224B)!), "apos;": Character(Unicode.Scalar(0x27)!), "ApplyFunction;": Character(Unicode.Scalar(0x2061)!), "approx;": Character(Unicode.Scalar(0x2248)!),
+ "approxeq;": Character(Unicode.Scalar(0x224A)!), "Aring;": Character(Unicode.Scalar(0xC5)!), "aring;": Character(Unicode.Scalar(0xE5)!), "Ascr;": Character(Unicode.Scalar(0x1D49C)!),
+ "ascr;": Character(Unicode.Scalar(0x1D4B6)!), "Assign;": Character(Unicode.Scalar(0x2254)!), "ast;": Character(Unicode.Scalar(0x2A)!), "asymp;": Character(Unicode.Scalar(0x2248)!),
+ "asympeq;": Character(Unicode.Scalar(0x224D)!), "Atilde;": Character(Unicode.Scalar(0xC3)!), "atilde;": Character(Unicode.Scalar(0xE3)!), "Auml;": Character(Unicode.Scalar(0xC4)!),
+ "auml;": Character(Unicode.Scalar(0xE4)!), "awconint;": Character(Unicode.Scalar(0x2233)!), "awint;": Character(Unicode.Scalar(0x2A11)!), "backcong;": Character(Unicode.Scalar(0x224C)!),
+ "backepsilon;": Character(Unicode.Scalar(0x3F6)!), "backprime;": Character(Unicode.Scalar(0x2035)!), "backsim;": Character(Unicode.Scalar(0x223D)!), "backsimeq;": Character(Unicode.Scalar(0x22CD)!),
+ "Backslash;": Character(Unicode.Scalar(0x2216)!), "Barv;": Character(Unicode.Scalar(0x2AE7)!), "barvee;": Character(Unicode.Scalar(0x22BD)!), "Barwed;": Character(Unicode.Scalar(0x2306)!),
+ "barwed;": Character(Unicode.Scalar(0x2305)!), "barwedge;": Character(Unicode.Scalar(0x2305)!), "bbrk;": Character(Unicode.Scalar(0x23B5)!), "bbrktbrk;": Character(Unicode.Scalar(0x23B6)!),
+ "bcong;": Character(Unicode.Scalar(0x224C)!), "Bcy;": Character(Unicode.Scalar(0x411)!), "bcy;": Character(Unicode.Scalar(0x431)!), "bdquo;": Character(Unicode.Scalar(0x201E)!),
+ "becaus;": Character(Unicode.Scalar(0x2235)!), "Because;": Character(Unicode.Scalar(0x2235)!), "because;": Character(Unicode.Scalar(0x2235)!), "bemptyv;": Character(Unicode.Scalar(0x29B0)!),
+ "bepsi;": Character(Unicode.Scalar(0x3F6)!), "bernou;": Character(Unicode.Scalar(0x212C)!), "Bernoullis;": Character(Unicode.Scalar(0x212C)!), "Beta;": Character(Unicode.Scalar(0x392)!),
+ "beta;": Character(Unicode.Scalar(0x3B2)!), "beth;": Character(Unicode.Scalar(0x2136)!), "between;": Character(Unicode.Scalar(0x226C)!), "Bfr;": Character(Unicode.Scalar(0x1D505)!),
+ "bfr;": Character(Unicode.Scalar(0x1D51F)!), "bigcap;": Character(Unicode.Scalar(0x22C2)!), "bigcirc;": Character(Unicode.Scalar(0x25EF)!), "bigcup;": Character(Unicode.Scalar(0x22C3)!),
+ "bigodot;": Character(Unicode.Scalar(0x2A00)!), "bigoplus;": Character(Unicode.Scalar(0x2A01)!), "bigotimes;": Character(Unicode.Scalar(0x2A02)!), "bigsqcup;": Character(Unicode.Scalar(0x2A06)!),
+ "bigstar;": Character(Unicode.Scalar(0x2605)!), "bigtriangledown;": Character(Unicode.Scalar(0x25BD)!), "bigtriangleup;": Character(Unicode.Scalar(0x25B3)!), "biguplus;": Character(Unicode.Scalar(0x2A04)!),
+ "bigvee;": Character(Unicode.Scalar(0x22C1)!), "bigwedge;": Character(Unicode.Scalar(0x22C0)!), "bkarow;": Character(Unicode.Scalar(0x290D)!), "blacklozenge;": Character(Unicode.Scalar(0x29EB)!),
+ "blacksquare;": Character(Unicode.Scalar(0x25AA)!), "blacktriangle;": Character(Unicode.Scalar(0x25B4)!), "blacktriangledown;": Character(Unicode.Scalar(0x25BE)!), "blacktriangleleft;": Character(Unicode.Scalar(0x25C2)!),
+ "blacktriangleright;": Character(Unicode.Scalar(0x25B8)!), "blank;": Character(Unicode.Scalar(0x2423)!), "blk12;": Character(Unicode.Scalar(0x2592)!), "blk14;": Character(Unicode.Scalar(0x2591)!),
+ "blk34;": Character(Unicode.Scalar(0x2593)!), "block;": Character(Unicode.Scalar(0x2588)!), "bne;": "\u{3D}\u{20E5}", "bnequiv;": "\u{2261}\u{20E5}",
+ "bNot;": Character(Unicode.Scalar(0x2AED)!), "bnot;": Character(Unicode.Scalar(0x2310)!), "Bopf;": Character(Unicode.Scalar(0x1D539)!), "bopf;": Character(Unicode.Scalar(0x1D553)!),
+ "bot;": Character(Unicode.Scalar(0x22A5)!), "bottom;": Character(Unicode.Scalar(0x22A5)!), "bowtie;": Character(Unicode.Scalar(0x22C8)!), "boxbox;": Character(Unicode.Scalar(0x29C9)!),
+ "boxDL;": Character(Unicode.Scalar(0x2557)!), "boxDl;": Character(Unicode.Scalar(0x2556)!), "boxdL;": Character(Unicode.Scalar(0x2555)!), "boxdl;": Character(Unicode.Scalar(0x2510)!),
+ "boxDR;": Character(Unicode.Scalar(0x2554)!), "boxDr;": Character(Unicode.Scalar(0x2553)!), "boxdR;": Character(Unicode.Scalar(0x2552)!), "boxdr;": Character(Unicode.Scalar(0x250C)!),
+ "boxH;": Character(Unicode.Scalar(0x2550)!), "boxh;": Character(Unicode.Scalar(0x2500)!), "boxHD;": Character(Unicode.Scalar(0x2566)!), "boxHd;": Character(Unicode.Scalar(0x2564)!),
+ "boxhD;": Character(Unicode.Scalar(0x2565)!), "boxhd;": Character(Unicode.Scalar(0x252C)!), "boxHU;": Character(Unicode.Scalar(0x2569)!), "boxHu;": Character(Unicode.Scalar(0x2567)!),
+ "boxhU;": Character(Unicode.Scalar(0x2568)!), "boxhu;": Character(Unicode.Scalar(0x2534)!), "boxminus;": Character(Unicode.Scalar(0x229F)!), "boxplus;": Character(Unicode.Scalar(0x229E)!),
+ "boxtimes;": Character(Unicode.Scalar(0x22A0)!), "boxUL;": Character(Unicode.Scalar(0x255D)!), "boxUl;": Character(Unicode.Scalar(0x255C)!), "boxuL;": Character(Unicode.Scalar(0x255B)!),
+ "boxul;": Character(Unicode.Scalar(0x2518)!), "boxUR;": Character(Unicode.Scalar(0x255A)!), "boxUr;": Character(Unicode.Scalar(0x2559)!), "boxuR;": Character(Unicode.Scalar(0x2558)!),
+ "boxur;": Character(Unicode.Scalar(0x2514)!), "boxV;": Character(Unicode.Scalar(0x2551)!), "boxv;": Character(Unicode.Scalar(0x2502)!), "boxVH;": Character(Unicode.Scalar(0x256C)!),
+ "boxVh;": Character(Unicode.Scalar(0x256B)!), "boxvH;": Character(Unicode.Scalar(0x256A)!), "boxvh;": Character(Unicode.Scalar(0x253C)!), "boxVL;": Character(Unicode.Scalar(0x2563)!),
+ "boxVl;": Character(Unicode.Scalar(0x2562)!), "boxvL;": Character(Unicode.Scalar(0x2561)!), "boxvl;": Character(Unicode.Scalar(0x2524)!), "boxVR;": Character(Unicode.Scalar(0x2560)!),
+ "boxVr;": Character(Unicode.Scalar(0x255F)!), "boxvR;": Character(Unicode.Scalar(0x255E)!), "boxvr;": Character(Unicode.Scalar(0x251C)!), "bprime;": Character(Unicode.Scalar(0x2035)!),
+ "Breve;": Character(Unicode.Scalar(0x2D8)!), "breve;": Character(Unicode.Scalar(0x2D8)!), "brvbar;": Character(Unicode.Scalar(0xA6)!), "Bscr;": Character(Unicode.Scalar(0x212C)!),
+ "bscr;": Character(Unicode.Scalar(0x1D4B7)!), "bsemi;": Character(Unicode.Scalar(0x204F)!), "bsim;": Character(Unicode.Scalar(0x223D)!), "bsime;": Character(Unicode.Scalar(0x22CD)!),
+ "bsol;": Character(Unicode.Scalar(0x5C)!), "bsolb;": Character(Unicode.Scalar(0x29C5)!), "bsolhsub;": Character(Unicode.Scalar(0x27C8)!), "bull;": Character(Unicode.Scalar(0x2022)!),
+ "bullet;": Character(Unicode.Scalar(0x2022)!), "bump;": Character(Unicode.Scalar(0x224E)!), "bumpE;": Character(Unicode.Scalar(0x2AAE)!), "bumpe;": Character(Unicode.Scalar(0x224F)!),
+ "Bumpeq;": Character(Unicode.Scalar(0x224E)!), "bumpeq;": Character(Unicode.Scalar(0x224F)!), "Cacute;": Character(Unicode.Scalar(0x106)!), "cacute;": Character(Unicode.Scalar(0x107)!),
+ "Cap;": Character(Unicode.Scalar(0x22D2)!), "cap;": Character(Unicode.Scalar(0x2229)!), "capand;": Character(Unicode.Scalar(0x2A44)!), "capbrcup;": Character(Unicode.Scalar(0x2A49)!),
+ "capcap;": Character(Unicode.Scalar(0x2A4B)!), "capcup;": Character(Unicode.Scalar(0x2A47)!), "capdot;": Character(Unicode.Scalar(0x2A40)!), "CapitalDifferentialD;": Character(Unicode.Scalar(0x2145)!),
+ "caps;": "\u{2229}\u{FE00}", "caret;": Character(Unicode.Scalar(0x2041)!), "caron;": Character(Unicode.Scalar(0x2C7)!), "Cayleys;": Character(Unicode.Scalar(0x212D)!),
+ "ccaps;": Character(Unicode.Scalar(0x2A4D)!), "Ccaron;": Character(Unicode.Scalar(0x10C)!), "ccaron;": Character(Unicode.Scalar(0x10D)!), "Ccedil;": Character(Unicode.Scalar(0xC7)!),
+ "ccedil;": Character(Unicode.Scalar(0xE7)!), "Ccirc;": Character(Unicode.Scalar(0x108)!), "ccirc;": Character(Unicode.Scalar(0x109)!), "Cconint;": Character(Unicode.Scalar(0x2230)!),
+ "ccups;": Character(Unicode.Scalar(0x2A4C)!), "ccupssm;": Character(Unicode.Scalar(0x2A50)!), "Cdot;": Character(Unicode.Scalar(0x10A)!), "cdot;": Character(Unicode.Scalar(0x10B)!),
+ "cedil;": Character(Unicode.Scalar(0xB8)!), "Cedilla;": Character(Unicode.Scalar(0xB8)!), "cemptyv;": Character(Unicode.Scalar(0x29B2)!), "cent;": Character(Unicode.Scalar(0xA2)!),
+ "CenterDot;": Character(Unicode.Scalar(0xB7)!), "centerdot;": Character(Unicode.Scalar(0xB7)!), "Cfr;": Character(Unicode.Scalar(0x212D)!), "cfr;": Character(Unicode.Scalar(0x1D520)!),
+ "CHcy;": Character(Unicode.Scalar(0x427)!), "chcy;": Character(Unicode.Scalar(0x447)!), "check;": Character(Unicode.Scalar(0x2713)!), "checkmark;": Character(Unicode.Scalar(0x2713)!),
+ "Chi;": Character(Unicode.Scalar(0x3A7)!), "chi;": Character(Unicode.Scalar(0x3C7)!), "cir;": Character(Unicode.Scalar(0x25CB)!), "circ;": Character(Unicode.Scalar(0x2C6)!),
+ "circeq;": Character(Unicode.Scalar(0x2257)!), "circlearrowleft;": Character(Unicode.Scalar(0x21BA)!), "circlearrowright;": Character(Unicode.Scalar(0x21BB)!), "circledast;": Character(Unicode.Scalar(0x229B)!),
+ "circledcirc;": Character(Unicode.Scalar(0x229A)!), "circleddash;": Character(Unicode.Scalar(0x229D)!), "CircleDot;": Character(Unicode.Scalar(0x2299)!), "circledR;": Character(Unicode.Scalar(0xAE)!),
+ "circledS;": Character(Unicode.Scalar(0x24C8)!), "CircleMinus;": Character(Unicode.Scalar(0x2296)!), "CirclePlus;": Character(Unicode.Scalar(0x2295)!), "CircleTimes;": Character(Unicode.Scalar(0x2297)!),
+ "cirE;": Character(Unicode.Scalar(0x29C3)!), "cire;": Character(Unicode.Scalar(0x2257)!), "cirfnint;": Character(Unicode.Scalar(0x2A10)!), "cirmid;": Character(Unicode.Scalar(0x2AEF)!),
+ "cirscir;": Character(Unicode.Scalar(0x29C2)!), "ClockwiseContourIntegral;": Character(Unicode.Scalar(0x2232)!), "CloseCurlyDoubleQuote;": Character(Unicode.Scalar(0x201D)!), "CloseCurlyQuote;": Character(Unicode.Scalar(0x2019)!),
+ "clubs;": Character(Unicode.Scalar(0x2663)!), "clubsuit;": Character(Unicode.Scalar(0x2663)!), "Colon;": Character(Unicode.Scalar(0x2237)!), "colon;": Character(Unicode.Scalar(0x3A)!),
+ "Colone;": Character(Unicode.Scalar(0x2A74)!), "colone;": Character(Unicode.Scalar(0x2254)!), "coloneq;": Character(Unicode.Scalar(0x2254)!), "comma;": Character(Unicode.Scalar(0x2C)!),
+ "commat;": Character(Unicode.Scalar(0x40)!), "comp;": Character(Unicode.Scalar(0x2201)!), "compfn;": Character(Unicode.Scalar(0x2218)!), "complement;": Character(Unicode.Scalar(0x2201)!),
+ "complexes;": Character(Unicode.Scalar(0x2102)!), "cong;": Character(Unicode.Scalar(0x2245)!), "congdot;": Character(Unicode.Scalar(0x2A6D)!), "Congruent;": Character(Unicode.Scalar(0x2261)!),
+ "Conint;": Character(Unicode.Scalar(0x222F)!), "conint;": Character(Unicode.Scalar(0x222E)!), "ContourIntegral;": Character(Unicode.Scalar(0x222E)!), "Copf;": Character(Unicode.Scalar(0x2102)!),
+ "copf;": Character(Unicode.Scalar(0x1D554)!), "coprod;": Character(Unicode.Scalar(0x2210)!), "Coproduct;": Character(Unicode.Scalar(0x2210)!), "COPY;": Character(Unicode.Scalar(0xA9)!),
+ "copy;": Character(Unicode.Scalar(0xA9)!), "copysr;": Character(Unicode.Scalar(0x2117)!), "CounterClockwiseContourIntegral;": Character(Unicode.Scalar(0x2233)!), "crarr;": Character(Unicode.Scalar(0x21B5)!),
+ "Cross;": Character(Unicode.Scalar(0x2A2F)!), "cross;": Character(Unicode.Scalar(0x2717)!), "Cscr;": Character(Unicode.Scalar(0x1D49E)!), "cscr;": Character(Unicode.Scalar(0x1D4B8)!),
+ "csub;": Character(Unicode.Scalar(0x2ACF)!), "csube;": Character(Unicode.Scalar(0x2AD1)!), "csup;": Character(Unicode.Scalar(0x2AD0)!), "csupe;": Character(Unicode.Scalar(0x2AD2)!),
+ "ctdot;": Character(Unicode.Scalar(0x22EF)!), "cudarrl;": Character(Unicode.Scalar(0x2938)!), "cudarrr;": Character(Unicode.Scalar(0x2935)!), "cuepr;": Character(Unicode.Scalar(0x22DE)!),
+ "cuesc;": Character(Unicode.Scalar(0x22DF)!), "cularr;": Character(Unicode.Scalar(0x21B6)!), "cularrp;": Character(Unicode.Scalar(0x293D)!), "Cup;": Character(Unicode.Scalar(0x22D3)!),
+ "cup;": Character(Unicode.Scalar(0x222A)!), "cupbrcap;": Character(Unicode.Scalar(0x2A48)!), "CupCap;": Character(Unicode.Scalar(0x224D)!), "cupcap;": Character(Unicode.Scalar(0x2A46)!),
+ "cupcup;": Character(Unicode.Scalar(0x2A4A)!), "cupdot;": Character(Unicode.Scalar(0x228D)!), "cupor;": Character(Unicode.Scalar(0x2A45)!), "cups;": "\u{222A}\u{FE00}",
+ "curarr;": Character(Unicode.Scalar(0x21B7)!), "curarrm;": Character(Unicode.Scalar(0x293C)!), "curlyeqprec;": Character(Unicode.Scalar(0x22DE)!), "curlyeqsucc;": Character(Unicode.Scalar(0x22DF)!),
+ "curlyvee;": Character(Unicode.Scalar(0x22CE)!), "curlywedge;": Character(Unicode.Scalar(0x22CF)!), "curren;": Character(Unicode.Scalar(0xA4)!), "curvearrowleft;": Character(Unicode.Scalar(0x21B6)!),
+ "curvearrowright;": Character(Unicode.Scalar(0x21B7)!), "cuvee;": Character(Unicode.Scalar(0x22CE)!), "cuwed;": Character(Unicode.Scalar(0x22CF)!), "cwconint;": Character(Unicode.Scalar(0x2232)!),
+ "cwint;": Character(Unicode.Scalar(0x2231)!), "cylcty;": Character(Unicode.Scalar(0x232D)!), "Dagger;": Character(Unicode.Scalar(0x2021)!), "dagger;": Character(Unicode.Scalar(0x2020)!),
+ "daleth;": Character(Unicode.Scalar(0x2138)!), "Darr;": Character(Unicode.Scalar(0x21A1)!), "dArr;": Character(Unicode.Scalar(0x21D3)!), "darr;": Character(Unicode.Scalar(0x2193)!),
+ "dash;": Character(Unicode.Scalar(0x2010)!), "Dashv;": Character(Unicode.Scalar(0x2AE4)!), "dashv;": Character(Unicode.Scalar(0x22A3)!), "dbkarow;": Character(Unicode.Scalar(0x290F)!),
+ "dblac;": Character(Unicode.Scalar(0x2DD)!), "Dcaron;": Character(Unicode.Scalar(0x10E)!), "dcaron;": Character(Unicode.Scalar(0x10F)!), "Dcy;": Character(Unicode.Scalar(0x414)!),
+ "dcy;": Character(Unicode.Scalar(0x434)!), "DD;": Character(Unicode.Scalar(0x2145)!), "dd;": Character(Unicode.Scalar(0x2146)!), "ddagger;": Character(Unicode.Scalar(0x2021)!),
+ "ddarr;": Character(Unicode.Scalar(0x21CA)!), "DDotrahd;": Character(Unicode.Scalar(0x2911)!), "ddotseq;": Character(Unicode.Scalar(0x2A77)!), "deg;": Character(Unicode.Scalar(0xB0)!),
+ "Del;": Character(Unicode.Scalar(0x2207)!), "Delta;": Character(Unicode.Scalar(0x394)!), "delta;": Character(Unicode.Scalar(0x3B4)!), "demptyv;": Character(Unicode.Scalar(0x29B1)!),
+ "dfisht;": Character(Unicode.Scalar(0x297F)!), "Dfr;": Character(Unicode.Scalar(0x1D507)!), "dfr;": Character(Unicode.Scalar(0x1D521)!), "dHar;": Character(Unicode.Scalar(0x2965)!),
+ "dharl;": Character(Unicode.Scalar(0x21C3)!), "dharr;": Character(Unicode.Scalar(0x21C2)!), "DiacriticalAcute;": Character(Unicode.Scalar(0xB4)!), "DiacriticalDot;": Character(Unicode.Scalar(0x2D9)!),
+ "DiacriticalDoubleAcute;": Character(Unicode.Scalar(0x2DD)!), "DiacriticalGrave;": Character(Unicode.Scalar(0x60)!), "DiacriticalTilde;": Character(Unicode.Scalar(0x2DC)!), "diam;": Character(Unicode.Scalar(0x22C4)!),
+ "Diamond;": Character(Unicode.Scalar(0x22C4)!), "diamond;": Character(Unicode.Scalar(0x22C4)!), "diamondsuit;": Character(Unicode.Scalar(0x2666)!), "diams;": Character(Unicode.Scalar(0x2666)!),
+ "die;": Character(Unicode.Scalar(0xA8)!), "DifferentialD;": Character(Unicode.Scalar(0x2146)!), "digamma;": Character(Unicode.Scalar(0x3DD)!), "disin;": Character(Unicode.Scalar(0x22F2)!),
+ "div;": Character(Unicode.Scalar(0xF7)!), "divide;": Character(Unicode.Scalar(0xF7)!), "divideontimes;": Character(Unicode.Scalar(0x22C7)!), "divonx;": Character(Unicode.Scalar(0x22C7)!),
+ "DJcy;": Character(Unicode.Scalar(0x402)!), "djcy;": Character(Unicode.Scalar(0x452)!), "dlcorn;": Character(Unicode.Scalar(0x231E)!), "dlcrop;": Character(Unicode.Scalar(0x230D)!),
+ "dollar;": Character(Unicode.Scalar(0x24)!), "Dopf;": Character(Unicode.Scalar(0x1D53B)!), "dopf;": Character(Unicode.Scalar(0x1D555)!), "Dot;": Character(Unicode.Scalar(0xA8)!),
+ "dot;": Character(Unicode.Scalar(0x2D9)!), "DotDot;": Character(Unicode.Scalar(0x20DC)!), "doteq;": Character(Unicode.Scalar(0x2250)!), "doteqdot;": Character(Unicode.Scalar(0x2251)!),
+ "DotEqual;": Character(Unicode.Scalar(0x2250)!), "dotminus;": Character(Unicode.Scalar(0x2238)!), "dotplus;": Character(Unicode.Scalar(0x2214)!), "dotsquare;": Character(Unicode.Scalar(0x22A1)!),
+ "doublebarwedge;": Character(Unicode.Scalar(0x2306)!), "DoubleContourIntegral;": Character(Unicode.Scalar(0x222F)!), "DoubleDot;": Character(Unicode.Scalar(0xA8)!), "DoubleDownArrow;": Character(Unicode.Scalar(0x21D3)!),
+ "DoubleLeftArrow;": Character(Unicode.Scalar(0x21D0)!), "DoubleLeftRightArrow;": Character(Unicode.Scalar(0x21D4)!), "DoubleLeftTee;": Character(Unicode.Scalar(0x2AE4)!), "DoubleLongLeftArrow;": Character(Unicode.Scalar(0x27F8)!),
+ "DoubleLongLeftRightArrow;": Character(Unicode.Scalar(0x27FA)!), "DoubleLongRightArrow;": Character(Unicode.Scalar(0x27F9)!), "DoubleRightArrow;": Character(Unicode.Scalar(0x21D2)!), "DoubleRightTee;": Character(Unicode.Scalar(0x22A8)!),
+ "DoubleUpArrow;": Character(Unicode.Scalar(0x21D1)!), "DoubleUpDownArrow;": Character(Unicode.Scalar(0x21D5)!), "DoubleVerticalBar;": Character(Unicode.Scalar(0x2225)!), "DownArrow;": Character(Unicode.Scalar(0x2193)!),
+ "Downarrow;": Character(Unicode.Scalar(0x21D3)!), "downarrow;": Character(Unicode.Scalar(0x2193)!), "DownArrowBar;": Character(Unicode.Scalar(0x2913)!), "DownArrowUpArrow;": Character(Unicode.Scalar(0x21F5)!),
+ "DownBreve;": Character(Unicode.Scalar(0x311)!), "downdownarrows;": Character(Unicode.Scalar(0x21CA)!), "downharpoonleft;": Character(Unicode.Scalar(0x21C3)!), "downharpoonright;": Character(Unicode.Scalar(0x21C2)!),
+ "DownLeftRightVector;": Character(Unicode.Scalar(0x2950)!), "DownLeftTeeVector;": Character(Unicode.Scalar(0x295E)!), "DownLeftVector;": Character(Unicode.Scalar(0x21BD)!), "DownLeftVectorBar;": Character(Unicode.Scalar(0x2956)!),
+ "DownRightTeeVector;": Character(Unicode.Scalar(0x295F)!), "DownRightVector;": Character(Unicode.Scalar(0x21C1)!), "DownRightVectorBar;": Character(Unicode.Scalar(0x2957)!), "DownTee;": Character(Unicode.Scalar(0x22A4)!),
+ "DownTeeArrow;": Character(Unicode.Scalar(0x21A7)!), "drbkarow;": Character(Unicode.Scalar(0x2910)!), "drcorn;": Character(Unicode.Scalar(0x231F)!), "drcrop;": Character(Unicode.Scalar(0x230C)!),
+ "Dscr;": Character(Unicode.Scalar(0x1D49F)!), "dscr;": Character(Unicode.Scalar(0x1D4B9)!), "DScy;": Character(Unicode.Scalar(0x405)!), "dscy;": Character(Unicode.Scalar(0x455)!),
+ "dsol;": Character(Unicode.Scalar(0x29F6)!), "Dstrok;": Character(Unicode.Scalar(0x110)!), "dstrok;": Character(Unicode.Scalar(0x111)!), "dtdot;": Character(Unicode.Scalar(0x22F1)!),
+ "dtri;": Character(Unicode.Scalar(0x25BF)!), "dtrif;": Character(Unicode.Scalar(0x25BE)!), "duarr;": Character(Unicode.Scalar(0x21F5)!), "duhar;": Character(Unicode.Scalar(0x296F)!),
+ "dwangle;": Character(Unicode.Scalar(0x29A6)!), "DZcy;": Character(Unicode.Scalar(0x40F)!), "dzcy;": Character(Unicode.Scalar(0x45F)!), "dzigrarr;": Character(Unicode.Scalar(0x27FF)!),
+ "Eacute;": Character(Unicode.Scalar(0xC9)!), "eacute;": Character(Unicode.Scalar(0xE9)!), "easter;": Character(Unicode.Scalar(0x2A6E)!), "Ecaron;": Character(Unicode.Scalar(0x11A)!),
+ "ecaron;": Character(Unicode.Scalar(0x11B)!), "ecir;": Character(Unicode.Scalar(0x2256)!), "Ecirc;": Character(Unicode.Scalar(0xCA)!), "ecirc;": Character(Unicode.Scalar(0xEA)!),
+ "ecolon;": Character(Unicode.Scalar(0x2255)!), "Ecy;": Character(Unicode.Scalar(0x42D)!), "ecy;": Character(Unicode.Scalar(0x44D)!), "eDDot;": Character(Unicode.Scalar(0x2A77)!),
+ "Edot;": Character(Unicode.Scalar(0x116)!), "eDot;": Character(Unicode.Scalar(0x2251)!), "edot;": Character(Unicode.Scalar(0x117)!), "ee;": Character(Unicode.Scalar(0x2147)!),
+ "efDot;": Character(Unicode.Scalar(0x2252)!), "Efr;": Character(Unicode.Scalar(0x1D508)!), "efr;": Character(Unicode.Scalar(0x1D522)!), "eg;": Character(Unicode.Scalar(0x2A9A)!),
+ "Egrave;": Character(Unicode.Scalar(0xC8)!), "egrave;": Character(Unicode.Scalar(0xE8)!), "egs;": Character(Unicode.Scalar(0x2A96)!), "egsdot;": Character(Unicode.Scalar(0x2A98)!),
+ "el;": Character(Unicode.Scalar(0x2A99)!), "Element;": Character(Unicode.Scalar(0x2208)!), "elinters;": Character(Unicode.Scalar(0x23E7)!), "ell;": Character(Unicode.Scalar(0x2113)!),
+ "els;": Character(Unicode.Scalar(0x2A95)!), "elsdot;": Character(Unicode.Scalar(0x2A97)!), "Emacr;": Character(Unicode.Scalar(0x112)!), "emacr;": Character(Unicode.Scalar(0x113)!),
+ "empty;": Character(Unicode.Scalar(0x2205)!), "emptyset;": Character(Unicode.Scalar(0x2205)!), "EmptySmallSquare;": Character(Unicode.Scalar(0x25FB)!), "emptyv;": Character(Unicode.Scalar(0x2205)!),
+ "EmptyVerySmallSquare;": Character(Unicode.Scalar(0x25AB)!), "emsp;": Character(Unicode.Scalar(0x2003)!), "emsp13;": Character(Unicode.Scalar(0x2004)!), "emsp14;": Character(Unicode.Scalar(0x2005)!),
+ "ENG;": Character(Unicode.Scalar(0x14A)!), "eng;": Character(Unicode.Scalar(0x14B)!), "ensp;": Character(Unicode.Scalar(0x2002)!), "Eogon;": Character(Unicode.Scalar(0x118)!),
+ "eogon;": Character(Unicode.Scalar(0x119)!), "Eopf;": Character(Unicode.Scalar(0x1D53C)!), "eopf;": Character(Unicode.Scalar(0x1D556)!), "epar;": Character(Unicode.Scalar(0x22D5)!),
+ "eparsl;": Character(Unicode.Scalar(0x29E3)!), "eplus;": Character(Unicode.Scalar(0x2A71)!), "epsi;": Character(Unicode.Scalar(0x3B5)!), "Epsilon;": Character(Unicode.Scalar(0x395)!),
+ "epsilon;": Character(Unicode.Scalar(0x3B5)!), "epsiv;": Character(Unicode.Scalar(0x3F5)!), "eqcirc;": Character(Unicode.Scalar(0x2256)!), "eqcolon;": Character(Unicode.Scalar(0x2255)!),
+ "eqsim;": Character(Unicode.Scalar(0x2242)!), "eqslantgtr;": Character(Unicode.Scalar(0x2A96)!), "eqslantless;": Character(Unicode.Scalar(0x2A95)!), "Equal;": Character(Unicode.Scalar(0x2A75)!),
+ "equals;": Character(Unicode.Scalar(0x3D)!), "EqualTilde;": Character(Unicode.Scalar(0x2242)!), "equest;": Character(Unicode.Scalar(0x225F)!), "Equilibrium;": Character(Unicode.Scalar(0x21CC)!),
+ "equiv;": Character(Unicode.Scalar(0x2261)!), "equivDD;": Character(Unicode.Scalar(0x2A78)!), "eqvparsl;": Character(Unicode.Scalar(0x29E5)!), "erarr;": Character(Unicode.Scalar(0x2971)!),
+ "erDot;": Character(Unicode.Scalar(0x2253)!), "Escr;": Character(Unicode.Scalar(0x2130)!), "escr;": Character(Unicode.Scalar(0x212F)!), "esdot;": Character(Unicode.Scalar(0x2250)!),
+ "Esim;": Character(Unicode.Scalar(0x2A73)!), "esim;": Character(Unicode.Scalar(0x2242)!), "Eta;": Character(Unicode.Scalar(0x397)!), "eta;": Character(Unicode.Scalar(0x3B7)!),
+ "ETH;": Character(Unicode.Scalar(0xD0)!), "eth;": Character(Unicode.Scalar(0xF0)!), "Euml;": Character(Unicode.Scalar(0xCB)!), "euml;": Character(Unicode.Scalar(0xEB)!),
+ "euro;": Character(Unicode.Scalar(0x20AC)!), "excl;": Character(Unicode.Scalar(0x21)!), "exist;": Character(Unicode.Scalar(0x2203)!), "Exists;": Character(Unicode.Scalar(0x2203)!),
+ "expectation;": Character(Unicode.Scalar(0x2130)!), "ExponentialE;": Character(Unicode.Scalar(0x2147)!), "exponentiale;": Character(Unicode.Scalar(0x2147)!), "fallingdotseq;": Character(Unicode.Scalar(0x2252)!),
+ "Fcy;": Character(Unicode.Scalar(0x424)!), "fcy;": Character(Unicode.Scalar(0x444)!), "female;": Character(Unicode.Scalar(0x2640)!), "ffilig;": Character(Unicode.Scalar(0xFB03)!),
+ "fflig;": Character(Unicode.Scalar(0xFB00)!), "ffllig;": Character(Unicode.Scalar(0xFB04)!), "Ffr;": Character(Unicode.Scalar(0x1D509)!), "ffr;": Character(Unicode.Scalar(0x1D523)!),
+ "filig;": Character(Unicode.Scalar(0xFB01)!), "FilledSmallSquare;": Character(Unicode.Scalar(0x25FC)!), "FilledVerySmallSquare;": Character(Unicode.Scalar(0x25AA)!),
+
+ // Skip "fjlig;" due to Swift not recognizing it as a single grapheme cluster
+ // "fjlig;":Character(Unicode.Scalar(0x66}\u{6A)!),
+
+ "flat;": Character(Unicode.Scalar(0x266D)!), "fllig;": Character(Unicode.Scalar(0xFB02)!), "fltns;": Character(Unicode.Scalar(0x25B1)!), "fnof;": Character(Unicode.Scalar(0x192)!),
+ "Fopf;": Character(Unicode.Scalar(0x1D53D)!), "fopf;": Character(Unicode.Scalar(0x1D557)!), "ForAll;": Character(Unicode.Scalar(0x2200)!), "forall;": Character(Unicode.Scalar(0x2200)!),
+ "fork;": Character(Unicode.Scalar(0x22D4)!), "forkv;": Character(Unicode.Scalar(0x2AD9)!), "Fouriertrf;": Character(Unicode.Scalar(0x2131)!), "fpartint;": Character(Unicode.Scalar(0x2A0D)!),
+ "frac12;": Character(Unicode.Scalar(0xBD)!), "frac13;": Character(Unicode.Scalar(0x2153)!), "frac14;": Character(Unicode.Scalar(0xBC)!), "frac15;": Character(Unicode.Scalar(0x2155)!),
+ "frac16;": Character(Unicode.Scalar(0x2159)!), "frac18;": Character(Unicode.Scalar(0x215B)!), "frac23;": Character(Unicode.Scalar(0x2154)!), "frac25;": Character(Unicode.Scalar(0x2156)!),
+ "frac34;": Character(Unicode.Scalar(0xBE)!), "frac35;": Character(Unicode.Scalar(0x2157)!), "frac38;": Character(Unicode.Scalar(0x215C)!), "frac45;": Character(Unicode.Scalar(0x2158)!),
+ "frac56;": Character(Unicode.Scalar(0x215A)!), "frac58;": Character(Unicode.Scalar(0x215D)!), "frac78;": Character(Unicode.Scalar(0x215E)!), "frasl;": Character(Unicode.Scalar(0x2044)!),
+ "frown;": Character(Unicode.Scalar(0x2322)!), "Fscr;": Character(Unicode.Scalar(0x2131)!), "fscr;": Character(Unicode.Scalar(0x1D4BB)!), "gacute;": Character(Unicode.Scalar(0x1F5)!),
+ "Gamma;": Character(Unicode.Scalar(0x393)!), "gamma;": Character(Unicode.Scalar(0x3B3)!), "Gammad;": Character(Unicode.Scalar(0x3DC)!), "gammad;": Character(Unicode.Scalar(0x3DD)!),
+ "gap;": Character(Unicode.Scalar(0x2A86)!), "Gbreve;": Character(Unicode.Scalar(0x11E)!), "gbreve;": Character(Unicode.Scalar(0x11F)!), "Gcedil;": Character(Unicode.Scalar(0x122)!),
+ "Gcirc;": Character(Unicode.Scalar(0x11C)!), "gcirc;": Character(Unicode.Scalar(0x11D)!), "Gcy;": Character(Unicode.Scalar(0x413)!), "gcy;": Character(Unicode.Scalar(0x433)!),
+ "Gdot;": Character(Unicode.Scalar(0x120)!), "gdot;": Character(Unicode.Scalar(0x121)!), "gE;": Character(Unicode.Scalar(0x2267)!), "ge;": Character(Unicode.Scalar(0x2265)!),
+ "gEl;": Character(Unicode.Scalar(0x2A8C)!), "gel;": Character(Unicode.Scalar(0x22DB)!), "geq;": Character(Unicode.Scalar(0x2265)!), "geqq;": Character(Unicode.Scalar(0x2267)!),
+ "geqslant;": Character(Unicode.Scalar(0x2A7E)!), "ges;": Character(Unicode.Scalar(0x2A7E)!), "gescc;": Character(Unicode.Scalar(0x2AA9)!), "gesdot;": Character(Unicode.Scalar(0x2A80)!),
+ "gesdoto;": Character(Unicode.Scalar(0x2A82)!), "gesdotol;": Character(Unicode.Scalar(0x2A84)!), "gesl;": "\u{22DB}\u{FE00}", "gesles;": Character(Unicode.Scalar(0x2A94)!),
+ "Gfr;": Character(Unicode.Scalar(0x1D50A)!), "gfr;": Character(Unicode.Scalar(0x1D524)!), "Gg;": Character(Unicode.Scalar(0x22D9)!), "gg;": Character(Unicode.Scalar(0x226B)!),
+ "ggg;": Character(Unicode.Scalar(0x22D9)!), "gimel;": Character(Unicode.Scalar(0x2137)!), "GJcy;": Character(Unicode.Scalar(0x403)!), "gjcy;": Character(Unicode.Scalar(0x453)!),
+ "gl;": Character(Unicode.Scalar(0x2277)!), "gla;": Character(Unicode.Scalar(0x2AA5)!), "glE;": Character(Unicode.Scalar(0x2A92)!), "glj;": Character(Unicode.Scalar(0x2AA4)!),
+ "gnap;": Character(Unicode.Scalar(0x2A8A)!), "gnapprox;": Character(Unicode.Scalar(0x2A8A)!), "gnE;": Character(Unicode.Scalar(0x2269)!), "gne;": Character(Unicode.Scalar(0x2A88)!),
+ "gneq;": Character(Unicode.Scalar(0x2A88)!), "gneqq;": Character(Unicode.Scalar(0x2269)!), "gnsim;": Character(Unicode.Scalar(0x22E7)!), "Gopf;": Character(Unicode.Scalar(0x1D53E)!),
+ "gopf;": Character(Unicode.Scalar(0x1D558)!), "grave;": Character(Unicode.Scalar(0x60)!), "GreaterEqual;": Character(Unicode.Scalar(0x2265)!), "GreaterEqualLess;": Character(Unicode.Scalar(0x22DB)!),
+ "GreaterFullEqual;": Character(Unicode.Scalar(0x2267)!), "GreaterGreater;": Character(Unicode.Scalar(0x2AA2)!), "GreaterLess;": Character(Unicode.Scalar(0x2277)!), "GreaterSlantEqual;": Character(Unicode.Scalar(0x2A7E)!),
+ "GreaterTilde;": Character(Unicode.Scalar(0x2273)!), "Gscr;": Character(Unicode.Scalar(0x1D4A2)!), "gscr;": Character(Unicode.Scalar(0x210A)!), "gsim;": Character(Unicode.Scalar(0x2273)!),
+ "gsime;": Character(Unicode.Scalar(0x2A8E)!), "gsiml;": Character(Unicode.Scalar(0x2A90)!), "GT;": Character(Unicode.Scalar(0x3E)!), "Gt;": Character(Unicode.Scalar(0x226B)!),
+ "gt;": Character(Unicode.Scalar(0x3E)!), "gtcc;": Character(Unicode.Scalar(0x2AA7)!), "gtcir;": Character(Unicode.Scalar(0x2A7A)!), "gtdot;": Character(Unicode.Scalar(0x22D7)!),
+ "gtlPar;": Character(Unicode.Scalar(0x2995)!), "gtquest;": Character(Unicode.Scalar(0x2A7C)!), "gtrapprox;": Character(Unicode.Scalar(0x2A86)!), "gtrarr;": Character(Unicode.Scalar(0x2978)!),
+ "gtrdot;": Character(Unicode.Scalar(0x22D7)!), "gtreqless;": Character(Unicode.Scalar(0x22DB)!), "gtreqqless;": Character(Unicode.Scalar(0x2A8C)!), "gtrless;": Character(Unicode.Scalar(0x2277)!),
+ "gtrsim;": Character(Unicode.Scalar(0x2273)!), "gvertneqq;": "\u{2269}\u{FE00}", "gvnE;": "\u{2269}\u{FE00}", "Hacek;": Character(Unicode.Scalar(0x2C7)!),
+ "hairsp;": Character(Unicode.Scalar(0x200A)!), "half;": Character(Unicode.Scalar(0xBD)!), "hamilt;": Character(Unicode.Scalar(0x210B)!), "HARDcy;": Character(Unicode.Scalar(0x42A)!),
+ "hardcy;": Character(Unicode.Scalar(0x44A)!), "hArr;": Character(Unicode.Scalar(0x21D4)!), "harr;": Character(Unicode.Scalar(0x2194)!), "harrcir;": Character(Unicode.Scalar(0x2948)!),
+ "harrw;": Character(Unicode.Scalar(0x21AD)!), "Hat;": Character(Unicode.Scalar(0x5E)!), "hbar;": Character(Unicode.Scalar(0x210F)!), "Hcirc;": Character(Unicode.Scalar(0x124)!),
+ "hcirc;": Character(Unicode.Scalar(0x125)!), "hearts;": Character(Unicode.Scalar(0x2665)!), "heartsuit;": Character(Unicode.Scalar(0x2665)!), "hellip;": Character(Unicode.Scalar(0x2026)!),
+ "hercon;": Character(Unicode.Scalar(0x22B9)!), "Hfr;": Character(Unicode.Scalar(0x210C)!), "hfr;": Character(Unicode.Scalar(0x1D525)!), "HilbertSpace;": Character(Unicode.Scalar(0x210B)!),
+ "hksearow;": Character(Unicode.Scalar(0x2925)!), "hkswarow;": Character(Unicode.Scalar(0x2926)!), "hoarr;": Character(Unicode.Scalar(0x21FF)!), "homtht;": Character(Unicode.Scalar(0x223B)!),
+ "hookleftarrow;": Character(Unicode.Scalar(0x21A9)!), "hookrightarrow;": Character(Unicode.Scalar(0x21AA)!), "Hopf;": Character(Unicode.Scalar(0x210D)!), "hopf;": Character(Unicode.Scalar(0x1D559)!),
+ "horbar;": Character(Unicode.Scalar(0x2015)!), "HorizontalLine;": Character(Unicode.Scalar(0x2500)!), "Hscr;": Character(Unicode.Scalar(0x210B)!), "hscr;": Character(Unicode.Scalar(0x1D4BD)!),
+ "hslash;": Character(Unicode.Scalar(0x210F)!), "Hstrok;": Character(Unicode.Scalar(0x126)!), "hstrok;": Character(Unicode.Scalar(0x127)!), "HumpDownHump;": Character(Unicode.Scalar(0x224E)!),
+ "HumpEqual;": Character(Unicode.Scalar(0x224F)!), "hybull;": Character(Unicode.Scalar(0x2043)!), "hyphen;": Character(Unicode.Scalar(0x2010)!), "Iacute;": Character(Unicode.Scalar(0xCD)!),
+ "iacute;": Character(Unicode.Scalar(0xED)!), "ic;": Character(Unicode.Scalar(0x2063)!), "Icirc;": Character(Unicode.Scalar(0xCE)!), "icirc;": Character(Unicode.Scalar(0xEE)!),
+ "Icy;": Character(Unicode.Scalar(0x418)!), "icy;": Character(Unicode.Scalar(0x438)!), "Idot;": Character(Unicode.Scalar(0x130)!), "IEcy;": Character(Unicode.Scalar(0x415)!),
+ "iecy;": Character(Unicode.Scalar(0x435)!), "iexcl;": Character(Unicode.Scalar(0xA1)!), "iff;": Character(Unicode.Scalar(0x21D4)!), "Ifr;": Character(Unicode.Scalar(0x2111)!),
+ "ifr;": Character(Unicode.Scalar(0x1D526)!), "Igrave;": Character(Unicode.Scalar(0xCC)!), "igrave;": Character(Unicode.Scalar(0xEC)!), "ii;": Character(Unicode.Scalar(0x2148)!),
+ "iiiint;": Character(Unicode.Scalar(0x2A0C)!), "iiint;": Character(Unicode.Scalar(0x222D)!), "iinfin;": Character(Unicode.Scalar(0x29DC)!), "iiota;": Character(Unicode.Scalar(0x2129)!),
+ "IJlig;": Character(Unicode.Scalar(0x132)!), "ijlig;": Character(Unicode.Scalar(0x133)!), "Im;": Character(Unicode.Scalar(0x2111)!), "Imacr;": Character(Unicode.Scalar(0x12A)!),
+ "imacr;": Character(Unicode.Scalar(0x12B)!), "image;": Character(Unicode.Scalar(0x2111)!), "ImaginaryI;": Character(Unicode.Scalar(0x2148)!), "imagline;": Character(Unicode.Scalar(0x2110)!),
+ "imagpart;": Character(Unicode.Scalar(0x2111)!), "imath;": Character(Unicode.Scalar(0x131)!), "imof;": Character(Unicode.Scalar(0x22B7)!), "imped;": Character(Unicode.Scalar(0x1B5)!),
+ "Implies;": Character(Unicode.Scalar(0x21D2)!), "in;": Character(Unicode.Scalar(0x2208)!), "incare;": Character(Unicode.Scalar(0x2105)!), "infin;": Character(Unicode.Scalar(0x221E)!),
+ "infintie;": Character(Unicode.Scalar(0x29DD)!), "inodot;": Character(Unicode.Scalar(0x131)!), "Int;": Character(Unicode.Scalar(0x222C)!), "int;": Character(Unicode.Scalar(0x222B)!),
+ "intcal;": Character(Unicode.Scalar(0x22BA)!), "integers;": Character(Unicode.Scalar(0x2124)!), "Integral;": Character(Unicode.Scalar(0x222B)!), "intercal;": Character(Unicode.Scalar(0x22BA)!),
+ "Intersection;": Character(Unicode.Scalar(0x22C2)!), "intlarhk;": Character(Unicode.Scalar(0x2A17)!), "intprod;": Character(Unicode.Scalar(0x2A3C)!), "InvisibleComma;": Character(Unicode.Scalar(0x2063)!),
+ "InvisibleTimes;": Character(Unicode.Scalar(0x2062)!), "IOcy;": Character(Unicode.Scalar(0x401)!), "iocy;": Character(Unicode.Scalar(0x451)!), "Iogon;": Character(Unicode.Scalar(0x12E)!),
+ "iogon;": Character(Unicode.Scalar(0x12F)!), "Iopf;": Character(Unicode.Scalar(0x1D540)!), "iopf;": Character(Unicode.Scalar(0x1D55A)!), "Iota;": Character(Unicode.Scalar(0x399)!),
+ "iota;": Character(Unicode.Scalar(0x3B9)!), "iprod;": Character(Unicode.Scalar(0x2A3C)!), "iquest;": Character(Unicode.Scalar(0xBF)!), "Iscr;": Character(Unicode.Scalar(0x2110)!),
+ "iscr;": Character(Unicode.Scalar(0x1D4BE)!), "isin;": Character(Unicode.Scalar(0x2208)!), "isindot;": Character(Unicode.Scalar(0x22F5)!), "isinE;": Character(Unicode.Scalar(0x22F9)!),
+ "isins;": Character(Unicode.Scalar(0x22F4)!), "isinsv;": Character(Unicode.Scalar(0x22F3)!), "isinv;": Character(Unicode.Scalar(0x2208)!), "it;": Character(Unicode.Scalar(0x2062)!),
+ "Itilde;": Character(Unicode.Scalar(0x128)!), "itilde;": Character(Unicode.Scalar(0x129)!), "Iukcy;": Character(Unicode.Scalar(0x406)!), "iukcy;": Character(Unicode.Scalar(0x456)!),
+ "Iuml;": Character(Unicode.Scalar(0xCF)!), "iuml;": Character(Unicode.Scalar(0xEF)!), "Jcirc;": Character(Unicode.Scalar(0x134)!), "jcirc;": Character(Unicode.Scalar(0x135)!),
+ "Jcy;": Character(Unicode.Scalar(0x419)!), "jcy;": Character(Unicode.Scalar(0x439)!), "Jfr;": Character(Unicode.Scalar(0x1D50D)!), "jfr;": Character(Unicode.Scalar(0x1D527)!),
+ "jmath;": Character(Unicode.Scalar(0x237)!), "Jopf;": Character(Unicode.Scalar(0x1D541)!), "jopf;": Character(Unicode.Scalar(0x1D55B)!), "Jscr;": Character(Unicode.Scalar(0x1D4A5)!),
+ "jscr;": Character(Unicode.Scalar(0x1D4BF)!), "Jsercy;": Character(Unicode.Scalar(0x408)!), "jsercy;": Character(Unicode.Scalar(0x458)!), "Jukcy;": Character(Unicode.Scalar(0x404)!),
+ "jukcy;": Character(Unicode.Scalar(0x454)!), "Kappa;": Character(Unicode.Scalar(0x39A)!), "kappa;": Character(Unicode.Scalar(0x3BA)!), "kappav;": Character(Unicode.Scalar(0x3F0)!),
+ "Kcedil;": Character(Unicode.Scalar(0x136)!), "kcedil;": Character(Unicode.Scalar(0x137)!), "Kcy;": Character(Unicode.Scalar(0x41A)!), "kcy;": Character(Unicode.Scalar(0x43A)!),
+ "Kfr;": Character(Unicode.Scalar(0x1D50E)!), "kfr;": Character(Unicode.Scalar(0x1D528)!), "kgreen;": Character(Unicode.Scalar(0x138)!), "KHcy;": Character(Unicode.Scalar(0x425)!),
+ "khcy;": Character(Unicode.Scalar(0x445)!), "KJcy;": Character(Unicode.Scalar(0x40C)!), "kjcy;": Character(Unicode.Scalar(0x45C)!), "Kopf;": Character(Unicode.Scalar(0x1D542)!),
+ "kopf;": Character(Unicode.Scalar(0x1D55C)!), "Kscr;": Character(Unicode.Scalar(0x1D4A6)!), "kscr;": Character(Unicode.Scalar(0x1D4C0)!), "lAarr;": Character(Unicode.Scalar(0x21DA)!),
+ "Lacute;": Character(Unicode.Scalar(0x139)!), "lacute;": Character(Unicode.Scalar(0x13A)!), "laemptyv;": Character(Unicode.Scalar(0x29B4)!), "lagran;": Character(Unicode.Scalar(0x2112)!),
+ "Lambda;": Character(Unicode.Scalar(0x39B)!), "lambda;": Character(Unicode.Scalar(0x3BB)!), "Lang;": Character(Unicode.Scalar(0x27EA)!), "lang;": Character(Unicode.Scalar(0x27E8)!),
+ "langd;": Character(Unicode.Scalar(0x2991)!), "langle;": Character(Unicode.Scalar(0x27E8)!), "lap;": Character(Unicode.Scalar(0x2A85)!), "Laplacetrf;": Character(Unicode.Scalar(0x2112)!),
+ "laquo;": Character(Unicode.Scalar(0xAB)!), "Larr;": Character(Unicode.Scalar(0x219E)!), "lArr;": Character(Unicode.Scalar(0x21D0)!), "larr;": Character(Unicode.Scalar(0x2190)!),
+ "larrb;": Character(Unicode.Scalar(0x21E4)!), "larrbfs;": Character(Unicode.Scalar(0x291F)!), "larrfs;": Character(Unicode.Scalar(0x291D)!), "larrhk;": Character(Unicode.Scalar(0x21A9)!),
+ "larrlp;": Character(Unicode.Scalar(0x21AB)!), "larrpl;": Character(Unicode.Scalar(0x2939)!), "larrsim;": Character(Unicode.Scalar(0x2973)!), "larrtl;": Character(Unicode.Scalar(0x21A2)!),
+ "lat;": Character(Unicode.Scalar(0x2AAB)!), "lAtail;": Character(Unicode.Scalar(0x291B)!), "latail;": Character(Unicode.Scalar(0x2919)!), "late;": Character(Unicode.Scalar(0x2AAD)!),
+ "lates;": "\u{2AAD}\u{FE00}", "lBarr;": Character(Unicode.Scalar(0x290E)!), "lbarr;": Character(Unicode.Scalar(0x290C)!), "lbbrk;": Character(Unicode.Scalar(0x2772)!),
+ "lbrace;": Character(Unicode.Scalar(0x7B)!), "lbrack;": Character(Unicode.Scalar(0x5B)!), "lbrke;": Character(Unicode.Scalar(0x298B)!), "lbrksld;": Character(Unicode.Scalar(0x298F)!),
+ "lbrkslu;": Character(Unicode.Scalar(0x298D)!), "Lcaron;": Character(Unicode.Scalar(0x13D)!), "lcaron;": Character(Unicode.Scalar(0x13E)!), "Lcedil;": Character(Unicode.Scalar(0x13B)!),
+ "lcedil;": Character(Unicode.Scalar(0x13C)!), "lceil;": Character(Unicode.Scalar(0x2308)!), "lcub;": Character(Unicode.Scalar(0x7B)!), "Lcy;": Character(Unicode.Scalar(0x41B)!),
+ "lcy;": Character(Unicode.Scalar(0x43B)!), "ldca;": Character(Unicode.Scalar(0x2936)!), "ldquo;": Character(Unicode.Scalar(0x201C)!), "ldquor;": Character(Unicode.Scalar(0x201E)!),
+ "ldrdhar;": Character(Unicode.Scalar(0x2967)!), "ldrushar;": Character(Unicode.Scalar(0x294B)!), "ldsh;": Character(Unicode.Scalar(0x21B2)!), "lE;": Character(Unicode.Scalar(0x2266)!),
+ "le;": Character(Unicode.Scalar(0x2264)!), "LeftAngleBracket;": Character(Unicode.Scalar(0x27E8)!), "LeftArrow;": Character(Unicode.Scalar(0x2190)!), "Leftarrow;": Character(Unicode.Scalar(0x21D0)!),
+ "leftarrow;": Character(Unicode.Scalar(0x2190)!), "LeftArrowBar;": Character(Unicode.Scalar(0x21E4)!), "LeftArrowRightArrow;": Character(Unicode.Scalar(0x21C6)!), "leftarrowtail;": Character(Unicode.Scalar(0x21A2)!),
+ "LeftCeiling;": Character(Unicode.Scalar(0x2308)!), "LeftDoubleBracket;": Character(Unicode.Scalar(0x27E6)!), "LeftDownTeeVector;": Character(Unicode.Scalar(0x2961)!), "LeftDownVector;": Character(Unicode.Scalar(0x21C3)!),
+ "LeftDownVectorBar;": Character(Unicode.Scalar(0x2959)!), "LeftFloor;": Character(Unicode.Scalar(0x230A)!), "leftharpoondown;": Character(Unicode.Scalar(0x21BD)!), "leftharpoonup;": Character(Unicode.Scalar(0x21BC)!),
+ "leftleftarrows;": Character(Unicode.Scalar(0x21C7)!), "LeftRightArrow;": Character(Unicode.Scalar(0x2194)!), "Leftrightarrow;": Character(Unicode.Scalar(0x21D4)!), "leftrightarrow;": Character(Unicode.Scalar(0x2194)!),
+ "leftrightarrows;": Character(Unicode.Scalar(0x21C6)!), "leftrightharpoons;": Character(Unicode.Scalar(0x21CB)!), "leftrightsquigarrow;": Character(Unicode.Scalar(0x21AD)!), "LeftRightVector;": Character(Unicode.Scalar(0x294E)!),
+ "LeftTee;": Character(Unicode.Scalar(0x22A3)!), "LeftTeeArrow;": Character(Unicode.Scalar(0x21A4)!), "LeftTeeVector;": Character(Unicode.Scalar(0x295A)!), "leftthreetimes;": Character(Unicode.Scalar(0x22CB)!),
+ "LeftTriangle;": Character(Unicode.Scalar(0x22B2)!), "LeftTriangleBar;": Character(Unicode.Scalar(0x29CF)!), "LeftTriangleEqual;": Character(Unicode.Scalar(0x22B4)!), "LeftUpDownVector;": Character(Unicode.Scalar(0x2951)!),
+ "LeftUpTeeVector;": Character(Unicode.Scalar(0x2960)!), "LeftUpVector;": Character(Unicode.Scalar(0x21BF)!), "LeftUpVectorBar;": Character(Unicode.Scalar(0x2958)!), "LeftVector;": Character(Unicode.Scalar(0x21BC)!),
+ "LeftVectorBar;": Character(Unicode.Scalar(0x2952)!), "lEg;": Character(Unicode.Scalar(0x2A8B)!), "leg;": Character(Unicode.Scalar(0x22DA)!), "leq;": Character(Unicode.Scalar(0x2264)!),
+ "leqq;": Character(Unicode.Scalar(0x2266)!), "leqslant;": Character(Unicode.Scalar(0x2A7D)!), "les;": Character(Unicode.Scalar(0x2A7D)!), "lescc;": Character(Unicode.Scalar(0x2AA8)!),
+ "lesdot;": Character(Unicode.Scalar(0x2A7F)!), "lesdoto;": Character(Unicode.Scalar(0x2A81)!), "lesdotor;": Character(Unicode.Scalar(0x2A83)!), "lesg;": "\u{22DA}\u{FE00}",
+ "lesges;": Character(Unicode.Scalar(0x2A93)!), "lessapprox;": Character(Unicode.Scalar(0x2A85)!), "lessdot;": Character(Unicode.Scalar(0x22D6)!), "lesseqgtr;": Character(Unicode.Scalar(0x22DA)!),
+ "lesseqqgtr;": Character(Unicode.Scalar(0x2A8B)!), "LessEqualGreater;": Character(Unicode.Scalar(0x22DA)!), "LessFullEqual;": Character(Unicode.Scalar(0x2266)!), "LessGreater;": Character(Unicode.Scalar(0x2276)!),
+ "lessgtr;": Character(Unicode.Scalar(0x2276)!), "LessLess;": Character(Unicode.Scalar(0x2AA1)!), "lesssim;": Character(Unicode.Scalar(0x2272)!), "LessSlantEqual;": Character(Unicode.Scalar(0x2A7D)!),
+ "LessTilde;": Character(Unicode.Scalar(0x2272)!), "lfisht;": Character(Unicode.Scalar(0x297C)!), "lfloor;": Character(Unicode.Scalar(0x230A)!), "Lfr;": Character(Unicode.Scalar(0x1D50F)!),
+ "lfr;": Character(Unicode.Scalar(0x1D529)!), "lg;": Character(Unicode.Scalar(0x2276)!), "lgE;": Character(Unicode.Scalar(0x2A91)!), "lHar;": Character(Unicode.Scalar(0x2962)!),
+ "lhard;": Character(Unicode.Scalar(0x21BD)!), "lharu;": Character(Unicode.Scalar(0x21BC)!), "lharul;": Character(Unicode.Scalar(0x296A)!), "lhblk;": Character(Unicode.Scalar(0x2584)!),
+ "LJcy;": Character(Unicode.Scalar(0x409)!), "ljcy;": Character(Unicode.Scalar(0x459)!), "Ll;": Character(Unicode.Scalar(0x22D8)!), "ll;": Character(Unicode.Scalar(0x226A)!),
+ "llarr;": Character(Unicode.Scalar(0x21C7)!), "llcorner;": Character(Unicode.Scalar(0x231E)!), "Lleftarrow;": Character(Unicode.Scalar(0x21DA)!), "llhard;": Character(Unicode.Scalar(0x296B)!),
+ "lltri;": Character(Unicode.Scalar(0x25FA)!), "Lmidot;": Character(Unicode.Scalar(0x13F)!), "lmidot;": Character(Unicode.Scalar(0x140)!), "lmoust;": Character(Unicode.Scalar(0x23B0)!),
+ "lmoustache;": Character(Unicode.Scalar(0x23B0)!), "lnap;": Character(Unicode.Scalar(0x2A89)!), "lnapprox;": Character(Unicode.Scalar(0x2A89)!), "lnE;": Character(Unicode.Scalar(0x2268)!),
+ "lne;": Character(Unicode.Scalar(0x2A87)!), "lneq;": Character(Unicode.Scalar(0x2A87)!), "lneqq;": Character(Unicode.Scalar(0x2268)!), "lnsim;": Character(Unicode.Scalar(0x22E6)!),
+ "loang;": Character(Unicode.Scalar(0x27EC)!), "loarr;": Character(Unicode.Scalar(0x21FD)!), "lobrk;": Character(Unicode.Scalar(0x27E6)!), "LongLeftArrow;": Character(Unicode.Scalar(0x27F5)!),
+ "Longleftarrow;": Character(Unicode.Scalar(0x27F8)!), "longleftarrow;": Character(Unicode.Scalar(0x27F5)!), "LongLeftRightArrow;": Character(Unicode.Scalar(0x27F7)!), "Longleftrightarrow;": Character(Unicode.Scalar(0x27FA)!),
+ "longleftrightarrow;": Character(Unicode.Scalar(0x27F7)!), "longmapsto;": Character(Unicode.Scalar(0x27FC)!), "LongRightArrow;": Character(Unicode.Scalar(0x27F6)!), "Longrightarrow;": Character(Unicode.Scalar(0x27F9)!),
+ "longrightarrow;": Character(Unicode.Scalar(0x27F6)!), "looparrowleft;": Character(Unicode.Scalar(0x21AB)!), "looparrowright;": Character(Unicode.Scalar(0x21AC)!), "lopar;": Character(Unicode.Scalar(0x2985)!),
+ "Lopf;": Character(Unicode.Scalar(0x1D543)!), "lopf;": Character(Unicode.Scalar(0x1D55D)!), "loplus;": Character(Unicode.Scalar(0x2A2D)!), "lotimes;": Character(Unicode.Scalar(0x2A34)!),
+ "lowast;": Character(Unicode.Scalar(0x2217)!), "lowbar;": Character(Unicode.Scalar(0x5F)!), "LowerLeftArrow;": Character(Unicode.Scalar(0x2199)!), "LowerRightArrow;": Character(Unicode.Scalar(0x2198)!),
+ "loz;": Character(Unicode.Scalar(0x25CA)!), "lozenge;": Character(Unicode.Scalar(0x25CA)!), "lozf;": Character(Unicode.Scalar(0x29EB)!), "lpar;": Character(Unicode.Scalar(0x28)!),
+ "lparlt;": Character(Unicode.Scalar(0x2993)!), "lrarr;": Character(Unicode.Scalar(0x21C6)!), "lrcorner;": Character(Unicode.Scalar(0x231F)!), "lrhar;": Character(Unicode.Scalar(0x21CB)!),
+ "lrhard;": Character(Unicode.Scalar(0x296D)!), "lrm;": Character(Unicode.Scalar(0x200E)!), "lrtri;": Character(Unicode.Scalar(0x22BF)!), "lsaquo;": Character(Unicode.Scalar(0x2039)!),
+ "Lscr;": Character(Unicode.Scalar(0x2112)!), "lscr;": Character(Unicode.Scalar(0x1D4C1)!), "Lsh;": Character(Unicode.Scalar(0x21B0)!), "lsh;": Character(Unicode.Scalar(0x21B0)!),
+ "lsim;": Character(Unicode.Scalar(0x2272)!), "lsime;": Character(Unicode.Scalar(0x2A8D)!), "lsimg;": Character(Unicode.Scalar(0x2A8F)!), "lsqb;": Character(Unicode.Scalar(0x5B)!),
+ "lsquo;": Character(Unicode.Scalar(0x2018)!), "lsquor;": Character(Unicode.Scalar(0x201A)!), "Lstrok;": Character(Unicode.Scalar(0x141)!), "lstrok;": Character(Unicode.Scalar(0x142)!),
+ "LT;": Character(Unicode.Scalar(0x3C)!), "Lt;": Character(Unicode.Scalar(0x226A)!), "lt;": Character(Unicode.Scalar(0x3C)!), "ltcc;": Character(Unicode.Scalar(0x2AA6)!),
+ "ltcir;": Character(Unicode.Scalar(0x2A79)!), "ltdot;": Character(Unicode.Scalar(0x22D6)!), "lthree;": Character(Unicode.Scalar(0x22CB)!), "ltimes;": Character(Unicode.Scalar(0x22C9)!),
+ "ltlarr;": Character(Unicode.Scalar(0x2976)!), "ltquest;": Character(Unicode.Scalar(0x2A7B)!), "ltri;": Character(Unicode.Scalar(0x25C3)!), "ltrie;": Character(Unicode.Scalar(0x22B4)!),
+ "ltrif;": Character(Unicode.Scalar(0x25C2)!), "ltrPar;": Character(Unicode.Scalar(0x2996)!), "lurdshar;": Character(Unicode.Scalar(0x294A)!), "luruhar;": Character(Unicode.Scalar(0x2966)!),
+ "lvertneqq;": "\u{2268}\u{FE00}", "lvnE;": "\u{2268}\u{FE00}", "macr;": Character(Unicode.Scalar(0xAF)!), "male;": Character(Unicode.Scalar(0x2642)!),
+ "malt;": Character(Unicode.Scalar(0x2720)!), "maltese;": Character(Unicode.Scalar(0x2720)!), "Map;": Character(Unicode.Scalar(0x2905)!), "map;": Character(Unicode.Scalar(0x21A6)!),
+ "mapsto;": Character(Unicode.Scalar(0x21A6)!), "mapstodown;": Character(Unicode.Scalar(0x21A7)!), "mapstoleft;": Character(Unicode.Scalar(0x21A4)!), "mapstoup;": Character(Unicode.Scalar(0x21A5)!),
+ "marker;": Character(Unicode.Scalar(0x25AE)!), "mcomma;": Character(Unicode.Scalar(0x2A29)!), "Mcy;": Character(Unicode.Scalar(0x41C)!), "mcy;": Character(Unicode.Scalar(0x43C)!),
+ "mdash;": Character(Unicode.Scalar(0x2014)!), "mDDot;": Character(Unicode.Scalar(0x223A)!), "measuredangle;": Character(Unicode.Scalar(0x2221)!), "MediumSpace;": Character(Unicode.Scalar(0x205F)!),
+ "Mellintrf;": Character(Unicode.Scalar(0x2133)!), "Mfr;": Character(Unicode.Scalar(0x1D510)!), "mfr;": Character(Unicode.Scalar(0x1D52A)!), "mho;": Character(Unicode.Scalar(0x2127)!),
+ "micro;": Character(Unicode.Scalar(0xB5)!), "mid;": Character(Unicode.Scalar(0x2223)!), "midast;": Character(Unicode.Scalar(0x2A)!), "midcir;": Character(Unicode.Scalar(0x2AF0)!),
+ "middot;": Character(Unicode.Scalar(0xB7)!), "minus;": Character(Unicode.Scalar(0x2212)!), "minusb;": Character(Unicode.Scalar(0x229F)!), "minusd;": Character(Unicode.Scalar(0x2238)!),
+ "minusdu;": Character(Unicode.Scalar(0x2A2A)!), "MinusPlus;": Character(Unicode.Scalar(0x2213)!), "mlcp;": Character(Unicode.Scalar(0x2ADB)!), "mldr;": Character(Unicode.Scalar(0x2026)!)
+]
+
+let namedCharactersDecodeMap2: [String: Character] = [
+ "mnplus;": Character(Unicode.Scalar(0x2213)!), "models;": Character(Unicode.Scalar(0x22A7)!), "Mopf;": Character(Unicode.Scalar(0x1D544)!), "mopf;": Character(Unicode.Scalar(0x1D55E)!),
+ "mp;": Character(Unicode.Scalar(0x2213)!), "Mscr;": Character(Unicode.Scalar(0x2133)!), "mscr;": Character(Unicode.Scalar(0x1D4C2)!), "mstpos;": Character(Unicode.Scalar(0x223E)!),
+ "Mu;": Character(Unicode.Scalar(0x39C)!), "mu;": Character(Unicode.Scalar(0x3BC)!), "multimap;": Character(Unicode.Scalar(0x22B8)!), "mumap;": Character(Unicode.Scalar(0x22B8)!),
+ "nabla;": Character(Unicode.Scalar(0x2207)!), "Nacute;": Character(Unicode.Scalar(0x143)!), "nacute;": Character(Unicode.Scalar(0x144)!), "nang;": "\u{2220}\u{20D2}",
+ "nap;": Character(Unicode.Scalar(0x2249)!), "napE;": "\u{2A70}\u{338}", "napid;": "\u{224B}\u{338}", "napos;": Character(Unicode.Scalar(0x149)!),
+ "napprox;": Character(Unicode.Scalar(0x2249)!), "natur;": Character(Unicode.Scalar(0x266E)!), "natural;": Character(Unicode.Scalar(0x266E)!), "naturals;": Character(Unicode.Scalar(0x2115)!),
+ "nbsp;": Character(Unicode.Scalar(0xA0)!), "nbump;": "\u{224E}\u{338}", "nbumpe;": "\u{224F}\u{338}", "ncap;": Character(Unicode.Scalar(0x2A43)!),
+ "Ncaron;": Character(Unicode.Scalar(0x147)!), "ncaron;": Character(Unicode.Scalar(0x148)!), "Ncedil;": Character(Unicode.Scalar(0x145)!), "ncedil;": Character(Unicode.Scalar(0x146)!),
+ "ncong;": Character(Unicode.Scalar(0x2247)!), "ncongdot;": "\u{2A6D}\u{338}", "ncup;": Character(Unicode.Scalar(0x2A42)!), "Ncy;": Character(Unicode.Scalar(0x41D)!),
+ "ncy;": Character(Unicode.Scalar(0x43D)!), "ndash;": Character(Unicode.Scalar(0x2013)!), "ne;": Character(Unicode.Scalar(0x2260)!), "nearhk;": Character(Unicode.Scalar(0x2924)!),
+ "neArr;": Character(Unicode.Scalar(0x21D7)!), "nearr;": Character(Unicode.Scalar(0x2197)!), "nearrow;": Character(Unicode.Scalar(0x2197)!), "nedot;": "\u{2250}\u{338}",
+ "NegativeMediumSpace;": Character(Unicode.Scalar(0x200B)!), "NegativeThickSpace;": Character(Unicode.Scalar(0x200B)!), "NegativeThinSpace;": Character(Unicode.Scalar(0x200B)!), "NegativeVeryThinSpace;": Character(Unicode.Scalar(0x200B)!),
+ "nequiv;": Character(Unicode.Scalar(0x2262)!), "nesear;": Character(Unicode.Scalar(0x2928)!), "nesim;": "\u{2242}\u{338}", "NestedGreaterGreater;": Character(Unicode.Scalar(0x226B)!),
+ "NestedLessLess;": Character(Unicode.Scalar(0x226A)!), "NewLine;": Character(Unicode.Scalar(0xA)!), "nexist;": Character(Unicode.Scalar(0x2204)!), "nexists;": Character(Unicode.Scalar(0x2204)!),
+ "Nfr;": Character(Unicode.Scalar(0x1D511)!), "nfr;": Character(Unicode.Scalar(0x1D52B)!), "ngE;": "\u{2267}\u{338}", "nge;": Character(Unicode.Scalar(0x2271)!),
+ "ngeq;": Character(Unicode.Scalar(0x2271)!), "ngeqq;": "\u{2267}\u{338}", "ngeqslant;": "\u{2A7E}\u{338}", "nges;": "\u{2A7E}\u{338}",
+ "nGg;": "\u{22D9}\u{338}", "ngsim;": Character(Unicode.Scalar(0x2275)!), "nGt;": "\u{226B}\u{20D2}", "ngt;": Character(Unicode.Scalar(0x226F)!),
+ "ngtr;": Character(Unicode.Scalar(0x226F)!), "nGtv;": "\u{226B}\u{338}", "nhArr;": Character(Unicode.Scalar(0x21CE)!), "nharr;": Character(Unicode.Scalar(0x21AE)!),
+ "nhpar;": Character(Unicode.Scalar(0x2AF2)!), "ni;": Character(Unicode.Scalar(0x220B)!), "nis;": Character(Unicode.Scalar(0x22FC)!), "nisd;": Character(Unicode.Scalar(0x22FA)!),
+ "niv;": Character(Unicode.Scalar(0x220B)!), "NJcy;": Character(Unicode.Scalar(0x40A)!), "njcy;": Character(Unicode.Scalar(0x45A)!), "nlArr;": Character(Unicode.Scalar(0x21CD)!),
+ "nlarr;": Character(Unicode.Scalar(0x219A)!), "nldr;": Character(Unicode.Scalar(0x2025)!), "nlE;": "\u{2266}\u{338}", "nle;": Character(Unicode.Scalar(0x2270)!),
+ "nLeftarrow;": Character(Unicode.Scalar(0x21CD)!), "nleftarrow;": Character(Unicode.Scalar(0x219A)!), "nLeftrightarrow;": Character(Unicode.Scalar(0x21CE)!), "nleftrightarrow;": Character(Unicode.Scalar(0x21AE)!),
+ "nleq;": Character(Unicode.Scalar(0x2270)!), "nleqq;": "\u{2266}\u{338}", "nleqslant;": "\u{2A7D}\u{338}", "nles;": "\u{2A7D}\u{338}",
+ "nless;": Character(Unicode.Scalar(0x226E)!), "nLl;": "\u{22D8}\u{338}", "nlsim;": Character(Unicode.Scalar(0x2274)!), "nLt;": "\u{226A}\u{338}",
+ "nlt;": Character(Unicode.Scalar(0x226E)!), "nltri;": Character(Unicode.Scalar(0x22EA)!), "nltrie;": Character(Unicode.Scalar(0x22EC)!), "nLtv;": "\u{226A}\u{338}",
+ "nmid;": Character(Unicode.Scalar(0x2224)!), "NoBreak;": Character(Unicode.Scalar(0x2060)!), "NonBreakingSpace;": Character(Unicode.Scalar(0xA0)!), "Nopf;": Character(Unicode.Scalar(0x2115)!),
+ "nopf;": Character(Unicode.Scalar(0x1D55F)!), "Not;": Character(Unicode.Scalar(0x2AEC)!), "not;": Character(Unicode.Scalar(0xAC)!), "NotCongruent;": Character(Unicode.Scalar(0x2262)!),
+ "NotCupCap;": Character(Unicode.Scalar(0x226D)!), "NotDoubleVerticalBar;": Character(Unicode.Scalar(0x2226)!), "NotElement;": Character(Unicode.Scalar(0x2209)!), "NotEqual;": Character(Unicode.Scalar(0x2260)!),
+ "NotEqualTilde;": "\u{2242}\u{338}", "NotExists;": Character(Unicode.Scalar(0x2204)!), "NotGreater;": Character(Unicode.Scalar(0x226F)!), "NotGreaterEqual;": Character(Unicode.Scalar(0x2271)!),
+ "NotGreaterFullEqual;": "\u{2267}\u{338}", "NotGreaterGreater;": "\u{226B}\u{338}", "NotGreaterLess;": Character(Unicode.Scalar(0x2279)!), "NotGreaterSlantEqual;": "\u{2A7E}\u{338}",
+ "NotGreaterTilde;": Character(Unicode.Scalar(0x2275)!), "NotHumpDownHump;": "\u{224E}\u{338}", "NotHumpEqual;": "\u{224F}\u{338}", "notin;": Character(Unicode.Scalar(0x2209)!),
+ "notindot;": "\u{22F5}\u{338}", "notinE;": "\u{22F9}\u{338}", "notinva;": Character(Unicode.Scalar(0x2209)!), "notinvb;": Character(Unicode.Scalar(0x22F7)!),
+ "notinvc;": Character(Unicode.Scalar(0x22F6)!), "NotLeftTriangle;": Character(Unicode.Scalar(0x22EA)!), "NotLeftTriangleBar;": "\u{29CF}\u{338}", "NotLeftTriangleEqual;": Character(Unicode.Scalar(0x22EC)!),
+ "NotLess;": Character(Unicode.Scalar(0x226E)!), "NotLessEqual;": Character(Unicode.Scalar(0x2270)!), "NotLessGreater;": Character(Unicode.Scalar(0x2278)!), "NotLessLess;": "\u{226A}\u{338}",
+ "NotLessSlantEqual;": "\u{2A7D}\u{338}", "NotLessTilde;": Character(Unicode.Scalar(0x2274)!), "NotNestedGreaterGreater;": "\u{2AA2}\u{338}", "NotNestedLessLess;": "\u{2AA1}\u{338}",
+ "notni;": Character(Unicode.Scalar(0x220C)!), "notniva;": Character(Unicode.Scalar(0x220C)!), "notnivb;": Character(Unicode.Scalar(0x22FE)!), "notnivc;": Character(Unicode.Scalar(0x22FD)!),
+ "NotPrecedes;": Character(Unicode.Scalar(0x2280)!), "NotPrecedesEqual;": "\u{2AAF}\u{338}", "NotPrecedesSlantEqual;": Character(Unicode.Scalar(0x22E0)!), "NotReverseElement;": Character(Unicode.Scalar(0x220C)!),
+ "NotRightTriangle;": Character(Unicode.Scalar(0x22EB)!), "NotRightTriangleBar;": "\u{29D0}\u{338}", "NotRightTriangleEqual;": Character(Unicode.Scalar(0x22ED)!), "NotSquareSubset;": "\u{228F}\u{338}",
+ "NotSquareSubsetEqual;": Character(Unicode.Scalar(0x22E2)!), "NotSquareSuperset;": "\u{2290}\u{338}", "NotSquareSupersetEqual;": Character(Unicode.Scalar(0x22E3)!), "NotSubset;": "\u{2282}\u{20D2}",
+ "NotSubsetEqual;": Character(Unicode.Scalar(0x2288)!), "NotSucceeds;": Character(Unicode.Scalar(0x2281)!), "NotSucceedsEqual;": "\u{2AB0}\u{338}", "NotSucceedsSlantEqual;": Character(Unicode.Scalar(0x22E1)!),
+ "NotSucceedsTilde;": "\u{227F}\u{338}", "NotSuperset;": "\u{2283}\u{20D2}", "NotSupersetEqual;": Character(Unicode.Scalar(0x2289)!), "NotTilde;": Character(Unicode.Scalar(0x2241)!),
+ "NotTildeEqual;": Character(Unicode.Scalar(0x2244)!), "NotTildeFullEqual;": Character(Unicode.Scalar(0x2247)!), "NotTildeTilde;": Character(Unicode.Scalar(0x2249)!), "NotVerticalBar;": Character(Unicode.Scalar(0x2224)!),
+ "npar;": Character(Unicode.Scalar(0x2226)!), "nparallel;": Character(Unicode.Scalar(0x2226)!), "nparsl;": "\u{2AFD}\u{20E5}", "npart;": "\u{2202}\u{338}",
+ "npolint;": Character(Unicode.Scalar(0x2A14)!), "npr;": Character(Unicode.Scalar(0x2280)!), "nprcue;": Character(Unicode.Scalar(0x22E0)!), "npre;": "\u{2AAF}\u{338}",
+ "nprec;": Character(Unicode.Scalar(0x2280)!), "npreceq;": "\u{2AAF}\u{338}", "nrArr;": Character(Unicode.Scalar(0x21CF)!), "nrarr;": Character(Unicode.Scalar(0x219B)!),
+ "nrarrc;": "\u{2933}\u{338}", "nrarrw;": "\u{219D}\u{338}", "nRightarrow;": Character(Unicode.Scalar(0x21CF)!), "nrightarrow;": Character(Unicode.Scalar(0x219B)!),
+ "nrtri;": Character(Unicode.Scalar(0x22EB)!), "nrtrie;": Character(Unicode.Scalar(0x22ED)!), "nsc;": Character(Unicode.Scalar(0x2281)!), "nsccue;": Character(Unicode.Scalar(0x22E1)!),
+ "nsce;": "\u{2AB0}\u{338}", "Nscr;": Character(Unicode.Scalar(0x1D4A9)!), "nscr;": Character(Unicode.Scalar(0x1D4C3)!), "nshortmid;": Character(Unicode.Scalar(0x2224)!),
+ "nshortparallel;": Character(Unicode.Scalar(0x2226)!), "nsim;": Character(Unicode.Scalar(0x2241)!), "nsime;": Character(Unicode.Scalar(0x2244)!), "nsimeq;": Character(Unicode.Scalar(0x2244)!),
+ "nsmid;": Character(Unicode.Scalar(0x2224)!), "nspar;": Character(Unicode.Scalar(0x2226)!), "nsqsube;": Character(Unicode.Scalar(0x22E2)!), "nsqsupe;": Character(Unicode.Scalar(0x22E3)!),
+ "nsub;": Character(Unicode.Scalar(0x2284)!), "nsubE;": "\u{2AC5}\u{338}", "nsube;": Character(Unicode.Scalar(0x2288)!), "nsubset;": "\u{2282}\u{20D2}",
+ "nsubseteq;": Character(Unicode.Scalar(0x2288)!), "nsubseteqq;": "\u{2AC5}\u{338}", "nsucc;": Character(Unicode.Scalar(0x2281)!), "nsucceq;": "\u{2AB0}\u{338}",
+ "nsup;": Character(Unicode.Scalar(0x2285)!), "nsupE;": "\u{2AC6}\u{338}", "nsupe;": Character(Unicode.Scalar(0x2289)!), "nsupset;": "\u{2283}\u{20D2}",
+ "nsupseteq;": Character(Unicode.Scalar(0x2289)!), "nsupseteqq;": "\u{2AC6}\u{338}", "ntgl;": Character(Unicode.Scalar(0x2279)!), "Ntilde;": Character(Unicode.Scalar(0xD1)!),
+ "ntilde;": Character(Unicode.Scalar(0xF1)!), "ntlg;": Character(Unicode.Scalar(0x2278)!), "ntriangleleft;": Character(Unicode.Scalar(0x22EA)!), "ntrianglelefteq;": Character(Unicode.Scalar(0x22EC)!),
+ "ntriangleright;": Character(Unicode.Scalar(0x22EB)!), "ntrianglerighteq;": Character(Unicode.Scalar(0x22ED)!), "Nu;": Character(Unicode.Scalar(0x39D)!), "nu;": Character(Unicode.Scalar(0x3BD)!),
+ "num;": Character(Unicode.Scalar(0x23)!), "numero;": Character(Unicode.Scalar(0x2116)!), "numsp;": Character(Unicode.Scalar(0x2007)!), "nvap;": "\u{224D}\u{20D2}",
+ "nVDash;": Character(Unicode.Scalar(0x22AF)!), "nVdash;": Character(Unicode.Scalar(0x22AE)!), "nvDash;": Character(Unicode.Scalar(0x22AD)!), "nvdash;": Character(Unicode.Scalar(0x22AC)!),
+ "nvge;": "\u{2265}\u{20D2}", "nvgt;": "\u{3E}\u{20D2}", "nvHarr;": Character(Unicode.Scalar(0x2904)!), "nvinfin;": Character(Unicode.Scalar(0x29DE)!),
+ "nvlArr;": Character(Unicode.Scalar(0x2902)!), "nvle;": "\u{2264}\u{20D2}", "nvlt;": "\u{3C}\u{20D2}", "nvltrie;": "\u{22B4}\u{20D2}",
+ "nvrArr;": Character(Unicode.Scalar(0x2903)!), "nvrtrie;": "\u{22B5}\u{20D2}", "nvsim;": "\u{223C}\u{20D2}", "nwarhk;": Character(Unicode.Scalar(0x2923)!),
+ "nwArr;": Character(Unicode.Scalar(0x21D6)!), "nwarr;": Character(Unicode.Scalar(0x2196)!), "nwarrow;": Character(Unicode.Scalar(0x2196)!), "nwnear;": Character(Unicode.Scalar(0x2927)!),
+ "Oacute;": Character(Unicode.Scalar(0xD3)!), "oacute;": Character(Unicode.Scalar(0xF3)!), "oast;": Character(Unicode.Scalar(0x229B)!), "ocir;": Character(Unicode.Scalar(0x229A)!),
+ "Ocirc;": Character(Unicode.Scalar(0xD4)!), "ocirc;": Character(Unicode.Scalar(0xF4)!), "Ocy;": Character(Unicode.Scalar(0x41E)!), "ocy;": Character(Unicode.Scalar(0x43E)!),
+ "odash;": Character(Unicode.Scalar(0x229D)!), "Odblac;": Character(Unicode.Scalar(0x150)!), "odblac;": Character(Unicode.Scalar(0x151)!), "odiv;": Character(Unicode.Scalar(0x2A38)!),
+ "odot;": Character(Unicode.Scalar(0x2299)!), "odsold;": Character(Unicode.Scalar(0x29BC)!), "OElig;": Character(Unicode.Scalar(0x152)!), "oelig;": Character(Unicode.Scalar(0x153)!),
+ "ofcir;": Character(Unicode.Scalar(0x29BF)!), "Ofr;": Character(Unicode.Scalar(0x1D512)!), "ofr;": Character(Unicode.Scalar(0x1D52C)!), "ogon;": Character(Unicode.Scalar(0x2DB)!),
+ "Ograve;": Character(Unicode.Scalar(0xD2)!), "ograve;": Character(Unicode.Scalar(0xF2)!), "ogt;": Character(Unicode.Scalar(0x29C1)!), "ohbar;": Character(Unicode.Scalar(0x29B5)!),
+ "ohm;": Character(Unicode.Scalar(0x3A9)!), "oint;": Character(Unicode.Scalar(0x222E)!), "olarr;": Character(Unicode.Scalar(0x21BA)!), "olcir;": Character(Unicode.Scalar(0x29BE)!),
+ "olcross;": Character(Unicode.Scalar(0x29BB)!), "oline;": Character(Unicode.Scalar(0x203E)!), "olt;": Character(Unicode.Scalar(0x29C0)!), "Omacr;": Character(Unicode.Scalar(0x14C)!),
+ "omacr;": Character(Unicode.Scalar(0x14D)!), "Omega;": Character(Unicode.Scalar(0x3A9)!), "omega;": Character(Unicode.Scalar(0x3C9)!), "Omicron;": Character(Unicode.Scalar(0x39F)!),
+ "omicron;": Character(Unicode.Scalar(0x3BF)!), "omid;": Character(Unicode.Scalar(0x29B6)!), "ominus;": Character(Unicode.Scalar(0x2296)!), "Oopf;": Character(Unicode.Scalar(0x1D546)!),
+ "oopf;": Character(Unicode.Scalar(0x1D560)!), "opar;": Character(Unicode.Scalar(0x29B7)!), "OpenCurlyDoubleQuote;": Character(Unicode.Scalar(0x201C)!), "OpenCurlyQuote;": Character(Unicode.Scalar(0x2018)!),
+ "operp;": Character(Unicode.Scalar(0x29B9)!), "oplus;": Character(Unicode.Scalar(0x2295)!), "Or;": Character(Unicode.Scalar(0x2A54)!), "or;": Character(Unicode.Scalar(0x2228)!),
+ "orarr;": Character(Unicode.Scalar(0x21BB)!), "ord;": Character(Unicode.Scalar(0x2A5D)!), "order;": Character(Unicode.Scalar(0x2134)!), "orderof;": Character(Unicode.Scalar(0x2134)!),
+ "ordf;": Character(Unicode.Scalar(0xAA)!), "ordm;": Character(Unicode.Scalar(0xBA)!), "origof;": Character(Unicode.Scalar(0x22B6)!), "oror;": Character(Unicode.Scalar(0x2A56)!),
+ "orslope;": Character(Unicode.Scalar(0x2A57)!), "orv;": Character(Unicode.Scalar(0x2A5B)!), "oS;": Character(Unicode.Scalar(0x24C8)!), "Oscr;": Character(Unicode.Scalar(0x1D4AA)!),
+ "oscr;": Character(Unicode.Scalar(0x2134)!), "Oslash;": Character(Unicode.Scalar(0xD8)!), "oslash;": Character(Unicode.Scalar(0xF8)!), "osol;": Character(Unicode.Scalar(0x2298)!),
+ "Otilde;": Character(Unicode.Scalar(0xD5)!), "otilde;": Character(Unicode.Scalar(0xF5)!), "Otimes;": Character(Unicode.Scalar(0x2A37)!), "otimes;": Character(Unicode.Scalar(0x2297)!),
+ "otimesas;": Character(Unicode.Scalar(0x2A36)!), "Ouml;": Character(Unicode.Scalar(0xD6)!), "ouml;": Character(Unicode.Scalar(0xF6)!), "ovbar;": Character(Unicode.Scalar(0x233D)!),
+ "OverBar;": Character(Unicode.Scalar(0x203E)!), "OverBrace;": Character(Unicode.Scalar(0x23DE)!), "OverBracket;": Character(Unicode.Scalar(0x23B4)!), "OverParenthesis;": Character(Unicode.Scalar(0x23DC)!),
+ "par;": Character(Unicode.Scalar(0x2225)!), "para;": Character(Unicode.Scalar(0xB6)!), "parallel;": Character(Unicode.Scalar(0x2225)!), "parsim;": Character(Unicode.Scalar(0x2AF3)!),
+ "parsl;": Character(Unicode.Scalar(0x2AFD)!), "part;": Character(Unicode.Scalar(0x2202)!), "PartialD;": Character(Unicode.Scalar(0x2202)!), "Pcy;": Character(Unicode.Scalar(0x41F)!),
+ "pcy;": Character(Unicode.Scalar(0x43F)!), "percnt;": Character(Unicode.Scalar(0x25)!), "period;": Character(Unicode.Scalar(0x2E)!), "permil;": Character(Unicode.Scalar(0x2030)!),
+ "perp;": Character(Unicode.Scalar(0x22A5)!), "pertenk;": Character(Unicode.Scalar(0x2031)!), "Pfr;": Character(Unicode.Scalar(0x1D513)!), "pfr;": Character(Unicode.Scalar(0x1D52D)!),
+ "Phi;": Character(Unicode.Scalar(0x3A6)!), "phi;": Character(Unicode.Scalar(0x3C6)!), "phiv;": Character(Unicode.Scalar(0x3D5)!), "phmmat;": Character(Unicode.Scalar(0x2133)!),
+ "phone;": Character(Unicode.Scalar(0x260E)!), "Pi;": Character(Unicode.Scalar(0x3A0)!), "pi;": Character(Unicode.Scalar(0x3C0)!), "pitchfork;": Character(Unicode.Scalar(0x22D4)!),
+ "piv;": Character(Unicode.Scalar(0x3D6)!), "planck;": Character(Unicode.Scalar(0x210F)!), "planckh;": Character(Unicode.Scalar(0x210E)!), "plankv;": Character(Unicode.Scalar(0x210F)!),
+ "plus;": Character(Unicode.Scalar(0x2B)!), "plusacir;": Character(Unicode.Scalar(0x2A23)!), "plusb;": Character(Unicode.Scalar(0x229E)!), "pluscir;": Character(Unicode.Scalar(0x2A22)!),
+ "plusdo;": Character(Unicode.Scalar(0x2214)!), "plusdu;": Character(Unicode.Scalar(0x2A25)!), "pluse;": Character(Unicode.Scalar(0x2A72)!), "PlusMinus;": Character(Unicode.Scalar(0xB1)!),
+ "plusmn;": Character(Unicode.Scalar(0xB1)!), "plussim;": Character(Unicode.Scalar(0x2A26)!), "plustwo;": Character(Unicode.Scalar(0x2A27)!), "pm;": Character(Unicode.Scalar(0xB1)!),
+ "Poincareplane;": Character(Unicode.Scalar(0x210C)!), "pointint;": Character(Unicode.Scalar(0x2A15)!), "Popf;": Character(Unicode.Scalar(0x2119)!), "popf;": Character(Unicode.Scalar(0x1D561)!),
+ "pound;": Character(Unicode.Scalar(0xA3)!), "Pr;": Character(Unicode.Scalar(0x2ABB)!), "pr;": Character(Unicode.Scalar(0x227A)!), "prap;": Character(Unicode.Scalar(0x2AB7)!),
+ "prcue;": Character(Unicode.Scalar(0x227C)!), "prE;": Character(Unicode.Scalar(0x2AB3)!), "pre;": Character(Unicode.Scalar(0x2AAF)!), "prec;": Character(Unicode.Scalar(0x227A)!),
+ "precapprox;": Character(Unicode.Scalar(0x2AB7)!), "preccurlyeq;": Character(Unicode.Scalar(0x227C)!), "Precedes;": Character(Unicode.Scalar(0x227A)!), "PrecedesEqual;": Character(Unicode.Scalar(0x2AAF)!),
+ "PrecedesSlantEqual;": Character(Unicode.Scalar(0x227C)!), "PrecedesTilde;": Character(Unicode.Scalar(0x227E)!), "preceq;": Character(Unicode.Scalar(0x2AAF)!), "precnapprox;": Character(Unicode.Scalar(0x2AB9)!),
+ "precneqq;": Character(Unicode.Scalar(0x2AB5)!), "precnsim;": Character(Unicode.Scalar(0x22E8)!), "precsim;": Character(Unicode.Scalar(0x227E)!), "Prime;": Character(Unicode.Scalar(0x2033)!),
+ "prime;": Character(Unicode.Scalar(0x2032)!), "primes;": Character(Unicode.Scalar(0x2119)!), "prnap;": Character(Unicode.Scalar(0x2AB9)!), "prnE;": Character(Unicode.Scalar(0x2AB5)!),
+ "prnsim;": Character(Unicode.Scalar(0x22E8)!), "prod;": Character(Unicode.Scalar(0x220F)!), "Product;": Character(Unicode.Scalar(0x220F)!), "profalar;": Character(Unicode.Scalar(0x232E)!),
+ "profline;": Character(Unicode.Scalar(0x2312)!), "profsurf;": Character(Unicode.Scalar(0x2313)!), "prop;": Character(Unicode.Scalar(0x221D)!), "Proportion;": Character(Unicode.Scalar(0x2237)!),
+ "Proportional;": Character(Unicode.Scalar(0x221D)!), "propto;": Character(Unicode.Scalar(0x221D)!), "prsim;": Character(Unicode.Scalar(0x227E)!), "prurel;": Character(Unicode.Scalar(0x22B0)!),
+ "Pscr;": Character(Unicode.Scalar(0x1D4AB)!), "pscr;": Character(Unicode.Scalar(0x1D4C5)!), "Psi;": Character(Unicode.Scalar(0x3A8)!), "psi;": Character(Unicode.Scalar(0x3C8)!),
+ "puncsp;": Character(Unicode.Scalar(0x2008)!), "Qfr;": Character(Unicode.Scalar(0x1D514)!), "qfr;": Character(Unicode.Scalar(0x1D52E)!), "qint;": Character(Unicode.Scalar(0x2A0C)!),
+ "Qopf;": Character(Unicode.Scalar(0x211A)!), "qopf;": Character(Unicode.Scalar(0x1D562)!), "qprime;": Character(Unicode.Scalar(0x2057)!), "Qscr;": Character(Unicode.Scalar(0x1D4AC)!),
+ "qscr;": Character(Unicode.Scalar(0x1D4C6)!), "quaternions;": Character(Unicode.Scalar(0x210D)!), "quatint;": Character(Unicode.Scalar(0x2A16)!), "quest;": Character(Unicode.Scalar(0x3F)!),
+ "questeq;": Character(Unicode.Scalar(0x225F)!), "QUOT;": Character(Unicode.Scalar(0x22)!), "quot;": Character(Unicode.Scalar(0x22)!), "rAarr;": Character(Unicode.Scalar(0x21DB)!),
+ "race;": "\u{223D}\u{331}", "Racute;": Character(Unicode.Scalar(0x154)!), "racute;": Character(Unicode.Scalar(0x155)!), "radic;": Character(Unicode.Scalar(0x221A)!),
+ "raemptyv;": Character(Unicode.Scalar(0x29B3)!), "Rang;": Character(Unicode.Scalar(0x27EB)!), "rang;": Character(Unicode.Scalar(0x27E9)!), "rangd;": Character(Unicode.Scalar(0x2992)!),
+ "range;": Character(Unicode.Scalar(0x29A5)!), "rangle;": Character(Unicode.Scalar(0x27E9)!), "raquo;": Character(Unicode.Scalar(0xBB)!), "Rarr;": Character(Unicode.Scalar(0x21A0)!),
+ "rArr;": Character(Unicode.Scalar(0x21D2)!), "rarr;": Character(Unicode.Scalar(0x2192)!), "rarrap;": Character(Unicode.Scalar(0x2975)!), "rarrb;": Character(Unicode.Scalar(0x21E5)!),
+ "rarrbfs;": Character(Unicode.Scalar(0x2920)!), "rarrc;": Character(Unicode.Scalar(0x2933)!), "rarrfs;": Character(Unicode.Scalar(0x291E)!), "rarrhk;": Character(Unicode.Scalar(0x21AA)!),
+ "rarrlp;": Character(Unicode.Scalar(0x21AC)!), "rarrpl;": Character(Unicode.Scalar(0x2945)!), "rarrsim;": Character(Unicode.Scalar(0x2974)!), "Rarrtl;": Character(Unicode.Scalar(0x2916)!),
+ "rarrtl;": Character(Unicode.Scalar(0x21A3)!), "rarrw;": Character(Unicode.Scalar(0x219D)!), "rAtail;": Character(Unicode.Scalar(0x291C)!), "ratail;": Character(Unicode.Scalar(0x291A)!),
+ "ratio;": Character(Unicode.Scalar(0x2236)!), "rationals;": Character(Unicode.Scalar(0x211A)!), "RBarr;": Character(Unicode.Scalar(0x2910)!), "rBarr;": Character(Unicode.Scalar(0x290F)!),
+ "rbarr;": Character(Unicode.Scalar(0x290D)!), "rbbrk;": Character(Unicode.Scalar(0x2773)!), "rbrace;": Character(Unicode.Scalar(0x7D)!), "rbrack;": Character(Unicode.Scalar(0x5D)!),
+ "rbrke;": Character(Unicode.Scalar(0x298C)!), "rbrksld;": Character(Unicode.Scalar(0x298E)!), "rbrkslu;": Character(Unicode.Scalar(0x2990)!), "Rcaron;": Character(Unicode.Scalar(0x158)!),
+ "rcaron;": Character(Unicode.Scalar(0x159)!), "Rcedil;": Character(Unicode.Scalar(0x156)!), "rcedil;": Character(Unicode.Scalar(0x157)!), "rceil;": Character(Unicode.Scalar(0x2309)!),
+ "rcub;": Character(Unicode.Scalar(0x7D)!), "Rcy;": Character(Unicode.Scalar(0x420)!), "rcy;": Character(Unicode.Scalar(0x440)!), "rdca;": Character(Unicode.Scalar(0x2937)!),
+ "rdldhar;": Character(Unicode.Scalar(0x2969)!), "rdquo;": Character(Unicode.Scalar(0x201D)!), "rdquor;": Character(Unicode.Scalar(0x201D)!), "rdsh;": Character(Unicode.Scalar(0x21B3)!),
+ "Re;": Character(Unicode.Scalar(0x211C)!), "real;": Character(Unicode.Scalar(0x211C)!), "realine;": Character(Unicode.Scalar(0x211B)!), "realpart;": Character(Unicode.Scalar(0x211C)!),
+ "reals;": Character(Unicode.Scalar(0x211D)!), "rect;": Character(Unicode.Scalar(0x25AD)!), "REG;": Character(Unicode.Scalar(0xAE)!), "reg;": Character(Unicode.Scalar(0xAE)!),
+ "ReverseElement;": Character(Unicode.Scalar(0x220B)!), "ReverseEquilibrium;": Character(Unicode.Scalar(0x21CB)!), "ReverseUpEquilibrium;": Character(Unicode.Scalar(0x296F)!), "rfisht;": Character(Unicode.Scalar(0x297D)!),
+ "rfloor;": Character(Unicode.Scalar(0x230B)!), "Rfr;": Character(Unicode.Scalar(0x211C)!), "rfr;": Character(Unicode.Scalar(0x1D52F)!), "rHar;": Character(Unicode.Scalar(0x2964)!),
+ "rhard;": Character(Unicode.Scalar(0x21C1)!), "rharu;": Character(Unicode.Scalar(0x21C0)!), "rharul;": Character(Unicode.Scalar(0x296C)!), "Rho;": Character(Unicode.Scalar(0x3A1)!),
+ "rho;": Character(Unicode.Scalar(0x3C1)!), "rhov;": Character(Unicode.Scalar(0x3F1)!), "RightAngleBracket;": Character(Unicode.Scalar(0x27E9)!), "RightArrow;": Character(Unicode.Scalar(0x2192)!),
+ "Rightarrow;": Character(Unicode.Scalar(0x21D2)!), "rightarrow;": Character(Unicode.Scalar(0x2192)!), "RightArrowBar;": Character(Unicode.Scalar(0x21E5)!), "RightArrowLeftArrow;": Character(Unicode.Scalar(0x21C4)!),
+ "rightarrowtail;": Character(Unicode.Scalar(0x21A3)!), "RightCeiling;": Character(Unicode.Scalar(0x2309)!), "RightDoubleBracket;": Character(Unicode.Scalar(0x27E7)!), "RightDownTeeVector;": Character(Unicode.Scalar(0x295D)!),
+ "RightDownVector;": Character(Unicode.Scalar(0x21C2)!), "RightDownVectorBar;": Character(Unicode.Scalar(0x2955)!), "RightFloor;": Character(Unicode.Scalar(0x230B)!), "rightharpoondown;": Character(Unicode.Scalar(0x21C1)!),
+ "rightharpoonup;": Character(Unicode.Scalar(0x21C0)!), "rightleftarrows;": Character(Unicode.Scalar(0x21C4)!), "rightleftharpoons;": Character(Unicode.Scalar(0x21CC)!), "rightrightarrows;": Character(Unicode.Scalar(0x21C9)!),
+ "rightsquigarrow;": Character(Unicode.Scalar(0x219D)!), "RightTee;": Character(Unicode.Scalar(0x22A2)!), "RightTeeArrow;": Character(Unicode.Scalar(0x21A6)!), "RightTeeVector;": Character(Unicode.Scalar(0x295B)!),
+ "rightthreetimes;": Character(Unicode.Scalar(0x22CC)!), "RightTriangle;": Character(Unicode.Scalar(0x22B3)!), "RightTriangleBar;": Character(Unicode.Scalar(0x29D0)!), "RightTriangleEqual;": Character(Unicode.Scalar(0x22B5)!),
+ "RightUpDownVector;": Character(Unicode.Scalar(0x294F)!), "RightUpTeeVector;": Character(Unicode.Scalar(0x295C)!), "RightUpVector;": Character(Unicode.Scalar(0x21BE)!), "RightUpVectorBar;": Character(Unicode.Scalar(0x2954)!),
+ "RightVector;": Character(Unicode.Scalar(0x21C0)!), "RightVectorBar;": Character(Unicode.Scalar(0x2953)!), "ring;": Character(Unicode.Scalar(0x2DA)!), "risingdotseq;": Character(Unicode.Scalar(0x2253)!),
+ "rlarr;": Character(Unicode.Scalar(0x21C4)!), "rlhar;": Character(Unicode.Scalar(0x21CC)!), "rlm;": Character(Unicode.Scalar(0x200F)!), "rmoust;": Character(Unicode.Scalar(0x23B1)!),
+ "rmoustache;": Character(Unicode.Scalar(0x23B1)!), "rnmid;": Character(Unicode.Scalar(0x2AEE)!), "roang;": Character(Unicode.Scalar(0x27ED)!), "roarr;": Character(Unicode.Scalar(0x21FE)!),
+ "robrk;": Character(Unicode.Scalar(0x27E7)!), "ropar;": Character(Unicode.Scalar(0x2986)!), "Ropf;": Character(Unicode.Scalar(0x211D)!), "ropf;": Character(Unicode.Scalar(0x1D563)!),
+ "roplus;": Character(Unicode.Scalar(0x2A2E)!), "rotimes;": Character(Unicode.Scalar(0x2A35)!), "RoundImplies;": Character(Unicode.Scalar(0x2970)!), "rpar;": Character(Unicode.Scalar(0x29)!),
+ "rpargt;": Character(Unicode.Scalar(0x2994)!), "rppolint;": Character(Unicode.Scalar(0x2A12)!), "rrarr;": Character(Unicode.Scalar(0x21C9)!), "Rrightarrow;": Character(Unicode.Scalar(0x21DB)!),
+ "rsaquo;": Character(Unicode.Scalar(0x203A)!), "Rscr;": Character(Unicode.Scalar(0x211B)!), "rscr;": Character(Unicode.Scalar(0x1D4C7)!), "Rsh;": Character(Unicode.Scalar(0x21B1)!),
+ "rsh;": Character(Unicode.Scalar(0x21B1)!), "rsqb;": Character(Unicode.Scalar(0x5D)!), "rsquo;": Character(Unicode.Scalar(0x2019)!), "rsquor;": Character(Unicode.Scalar(0x2019)!),
+ "rthree;": Character(Unicode.Scalar(0x22CC)!), "rtimes;": Character(Unicode.Scalar(0x22CA)!), "rtri;": Character(Unicode.Scalar(0x25B9)!), "rtrie;": Character(Unicode.Scalar(0x22B5)!),
+ "rtrif;": Character(Unicode.Scalar(0x25B8)!), "rtriltri;": Character(Unicode.Scalar(0x29CE)!), "RuleDelayed;": Character(Unicode.Scalar(0x29F4)!), "ruluhar;": Character(Unicode.Scalar(0x2968)!),
+ "rx;": Character(Unicode.Scalar(0x211E)!), "Sacute;": Character(Unicode.Scalar(0x15A)!), "sacute;": Character(Unicode.Scalar(0x15B)!), "sbquo;": Character(Unicode.Scalar(0x201A)!),
+ "Sc;": Character(Unicode.Scalar(0x2ABC)!), "sc;": Character(Unicode.Scalar(0x227B)!), "scap;": Character(Unicode.Scalar(0x2AB8)!), "Scaron;": Character(Unicode.Scalar(0x160)!),
+ "scaron;": Character(Unicode.Scalar(0x161)!), "sccue;": Character(Unicode.Scalar(0x227D)!), "scE;": Character(Unicode.Scalar(0x2AB4)!), "sce;": Character(Unicode.Scalar(0x2AB0)!),
+ "Scedil;": Character(Unicode.Scalar(0x15E)!), "scedil;": Character(Unicode.Scalar(0x15F)!), "Scirc;": Character(Unicode.Scalar(0x15C)!), "scirc;": Character(Unicode.Scalar(0x15D)!),
+ "scnap;": Character(Unicode.Scalar(0x2ABA)!), "scnE;": Character(Unicode.Scalar(0x2AB6)!), "scnsim;": Character(Unicode.Scalar(0x22E9)!), "scpolint;": Character(Unicode.Scalar(0x2A13)!),
+ "scsim;": Character(Unicode.Scalar(0x227F)!), "Scy;": Character(Unicode.Scalar(0x421)!), "scy;": Character(Unicode.Scalar(0x441)!), "sdot;": Character(Unicode.Scalar(0x22C5)!),
+ "sdotb;": Character(Unicode.Scalar(0x22A1)!), "sdote;": Character(Unicode.Scalar(0x2A66)!), "searhk;": Character(Unicode.Scalar(0x2925)!), "seArr;": Character(Unicode.Scalar(0x21D8)!),
+ "searr;": Character(Unicode.Scalar(0x2198)!), "searrow;": Character(Unicode.Scalar(0x2198)!), "sect;": Character(Unicode.Scalar(0xA7)!), "semi;": Character(Unicode.Scalar(0x3B)!),
+ "seswar;": Character(Unicode.Scalar(0x2929)!), "setminus;": Character(Unicode.Scalar(0x2216)!), "setmn;": Character(Unicode.Scalar(0x2216)!), "sext;": Character(Unicode.Scalar(0x2736)!),
+ "Sfr;": Character(Unicode.Scalar(0x1D516)!), "sfr;": Character(Unicode.Scalar(0x1D530)!), "sfrown;": Character(Unicode.Scalar(0x2322)!), "sharp;": Character(Unicode.Scalar(0x266F)!),
+ "SHCHcy;": Character(Unicode.Scalar(0x429)!), "shchcy;": Character(Unicode.Scalar(0x449)!), "SHcy;": Character(Unicode.Scalar(0x428)!), "shcy;": Character(Unicode.Scalar(0x448)!),
+ "ShortDownArrow;": Character(Unicode.Scalar(0x2193)!), "ShortLeftArrow;": Character(Unicode.Scalar(0x2190)!), "shortmid;": Character(Unicode.Scalar(0x2223)!), "shortparallel;": Character(Unicode.Scalar(0x2225)!),
+ "ShortRightArrow;": Character(Unicode.Scalar(0x2192)!), "ShortUpArrow;": Character(Unicode.Scalar(0x2191)!), "shy;": Character(Unicode.Scalar(0xAD)!), "Sigma;": Character(Unicode.Scalar(0x3A3)!),
+ "sigma;": Character(Unicode.Scalar(0x3C3)!), "sigmaf;": Character(Unicode.Scalar(0x3C2)!), "sigmav;": Character(Unicode.Scalar(0x3C2)!), "sim;": Character(Unicode.Scalar(0x223C)!),
+ "simdot;": Character(Unicode.Scalar(0x2A6A)!), "sime;": Character(Unicode.Scalar(0x2243)!), "simeq;": Character(Unicode.Scalar(0x2243)!), "simg;": Character(Unicode.Scalar(0x2A9E)!),
+ "simgE;": Character(Unicode.Scalar(0x2AA0)!), "siml;": Character(Unicode.Scalar(0x2A9D)!), "simlE;": Character(Unicode.Scalar(0x2A9F)!), "simne;": Character(Unicode.Scalar(0x2246)!),
+ "simplus;": Character(Unicode.Scalar(0x2A24)!), "simrarr;": Character(Unicode.Scalar(0x2972)!), "slarr;": Character(Unicode.Scalar(0x2190)!), "SmallCircle;": Character(Unicode.Scalar(0x2218)!),
+ "smallsetminus;": Character(Unicode.Scalar(0x2216)!), "smashp;": Character(Unicode.Scalar(0x2A33)!), "smeparsl;": Character(Unicode.Scalar(0x29E4)!), "smid;": Character(Unicode.Scalar(0x2223)!),
+ "smile;": Character(Unicode.Scalar(0x2323)!), "smt;": Character(Unicode.Scalar(0x2AAA)!), "smte;": Character(Unicode.Scalar(0x2AAC)!), "smtes;": "\u{2AAC}\u{FE00}",
+ "SOFTcy;": Character(Unicode.Scalar(0x42C)!), "softcy;": Character(Unicode.Scalar(0x44C)!), "sol;": Character(Unicode.Scalar(0x2F)!), "solb;": Character(Unicode.Scalar(0x29C4)!),
+ "solbar;": Character(Unicode.Scalar(0x233F)!), "Sopf;": Character(Unicode.Scalar(0x1D54A)!), "sopf;": Character(Unicode.Scalar(0x1D564)!), "spades;": Character(Unicode.Scalar(0x2660)!),
+ "spadesuit;": Character(Unicode.Scalar(0x2660)!), "spar;": Character(Unicode.Scalar(0x2225)!), "sqcap;": Character(Unicode.Scalar(0x2293)!), "sqcaps;": "\u{2293}\u{FE00}",
+ "sqcup;": Character(Unicode.Scalar(0x2294)!), "sqcups;": "\u{2294}\u{FE00}", "Sqrt;": Character(Unicode.Scalar(0x221A)!), "sqsub;": Character(Unicode.Scalar(0x228F)!),
+ "sqsube;": Character(Unicode.Scalar(0x2291)!), "sqsubset;": Character(Unicode.Scalar(0x228F)!), "sqsubseteq;": Character(Unicode.Scalar(0x2291)!), "sqsup;": Character(Unicode.Scalar(0x2290)!),
+ "sqsupe;": Character(Unicode.Scalar(0x2292)!), "sqsupset;": Character(Unicode.Scalar(0x2290)!), "sqsupseteq;": Character(Unicode.Scalar(0x2292)!), "squ;": Character(Unicode.Scalar(0x25A1)!),
+ "Square;": Character(Unicode.Scalar(0x25A1)!), "square;": Character(Unicode.Scalar(0x25A1)!), "SquareIntersection;": Character(Unicode.Scalar(0x2293)!), "SquareSubset;": Character(Unicode.Scalar(0x228F)!),
+ "SquareSubsetEqual;": Character(Unicode.Scalar(0x2291)!), "SquareSuperset;": Character(Unicode.Scalar(0x2290)!), "SquareSupersetEqual;": Character(Unicode.Scalar(0x2292)!), "SquareUnion;": Character(Unicode.Scalar(0x2294)!),
+ "squarf;": Character(Unicode.Scalar(0x25AA)!), "squf;": Character(Unicode.Scalar(0x25AA)!), "srarr;": Character(Unicode.Scalar(0x2192)!), "Sscr;": Character(Unicode.Scalar(0x1D4AE)!),
+ "sscr;": Character(Unicode.Scalar(0x1D4C8)!), "ssetmn;": Character(Unicode.Scalar(0x2216)!), "ssmile;": Character(Unicode.Scalar(0x2323)!), "sstarf;": Character(Unicode.Scalar(0x22C6)!),
+ "Star;": Character(Unicode.Scalar(0x22C6)!), "star;": Character(Unicode.Scalar(0x2606)!), "starf;": Character(Unicode.Scalar(0x2605)!), "straightepsilon;": Character(Unicode.Scalar(0x3F5)!),
+ "straightphi;": Character(Unicode.Scalar(0x3D5)!), "strns;": Character(Unicode.Scalar(0xAF)!), "Sub;": Character(Unicode.Scalar(0x22D0)!), "sub;": Character(Unicode.Scalar(0x2282)!),
+ "subdot;": Character(Unicode.Scalar(0x2ABD)!), "subE;": Character(Unicode.Scalar(0x2AC5)!), "sube;": Character(Unicode.Scalar(0x2286)!), "subedot;": Character(Unicode.Scalar(0x2AC3)!),
+ "submult;": Character(Unicode.Scalar(0x2AC1)!), "subnE;": Character(Unicode.Scalar(0x2ACB)!), "subne;": Character(Unicode.Scalar(0x228A)!), "subplus;": Character(Unicode.Scalar(0x2ABF)!),
+ "subrarr;": Character(Unicode.Scalar(0x2979)!), "Subset;": Character(Unicode.Scalar(0x22D0)!), "subset;": Character(Unicode.Scalar(0x2282)!), "subseteq;": Character(Unicode.Scalar(0x2286)!),
+ "subseteqq;": Character(Unicode.Scalar(0x2AC5)!), "SubsetEqual;": Character(Unicode.Scalar(0x2286)!), "subsetneq;": Character(Unicode.Scalar(0x228A)!), "subsetneqq;": Character(Unicode.Scalar(0x2ACB)!),
+ "subsim;": Character(Unicode.Scalar(0x2AC7)!), "subsub;": Character(Unicode.Scalar(0x2AD5)!), "subsup;": Character(Unicode.Scalar(0x2AD3)!), "succ;": Character(Unicode.Scalar(0x227B)!),
+ "succapprox;": Character(Unicode.Scalar(0x2AB8)!), "succcurlyeq;": Character(Unicode.Scalar(0x227D)!), "Succeeds;": Character(Unicode.Scalar(0x227B)!), "SucceedsEqual;": Character(Unicode.Scalar(0x2AB0)!),
+ "SucceedsSlantEqual;": Character(Unicode.Scalar(0x227D)!), "SucceedsTilde;": Character(Unicode.Scalar(0x227F)!), "succeq;": Character(Unicode.Scalar(0x2AB0)!), "succnapprox;": Character(Unicode.Scalar(0x2ABA)!),
+ "succneqq;": Character(Unicode.Scalar(0x2AB6)!), "succnsim;": Character(Unicode.Scalar(0x22E9)!), "succsim;": Character(Unicode.Scalar(0x227F)!), "SuchThat;": Character(Unicode.Scalar(0x220B)!),
+ "Sum;": Character(Unicode.Scalar(0x2211)!), "sum;": Character(Unicode.Scalar(0x2211)!), "sung;": Character(Unicode.Scalar(0x266A)!), "Sup;": Character(Unicode.Scalar(0x22D1)!),
+ "sup;": Character(Unicode.Scalar(0x2283)!), "sup1;": Character(Unicode.Scalar(0xB9)!), "sup2;": Character(Unicode.Scalar(0xB2)!), "sup3;": Character(Unicode.Scalar(0xB3)!),
+ "supdot;": Character(Unicode.Scalar(0x2ABE)!), "supdsub;": Character(Unicode.Scalar(0x2AD8)!), "supE;": Character(Unicode.Scalar(0x2AC6)!), "supe;": Character(Unicode.Scalar(0x2287)!),
+ "supedot;": Character(Unicode.Scalar(0x2AC4)!), "Superset;": Character(Unicode.Scalar(0x2283)!), "SupersetEqual;": Character(Unicode.Scalar(0x2287)!), "suphsol;": Character(Unicode.Scalar(0x27C9)!),
+ "suphsub;": Character(Unicode.Scalar(0x2AD7)!), "suplarr;": Character(Unicode.Scalar(0x297B)!), "supmult;": Character(Unicode.Scalar(0x2AC2)!), "supnE;": Character(Unicode.Scalar(0x2ACC)!),
+ "supne;": Character(Unicode.Scalar(0x228B)!), "supplus;": Character(Unicode.Scalar(0x2AC0)!), "Supset;": Character(Unicode.Scalar(0x22D1)!), "supset;": Character(Unicode.Scalar(0x2283)!),
+ "supseteq;": Character(Unicode.Scalar(0x2287)!), "supseteqq;": Character(Unicode.Scalar(0x2AC6)!), "supsetneq;": Character(Unicode.Scalar(0x228B)!), "supsetneqq;": Character(Unicode.Scalar(0x2ACC)!),
+ "supsim;": Character(Unicode.Scalar(0x2AC8)!), "supsub;": Character(Unicode.Scalar(0x2AD4)!), "supsup;": Character(Unicode.Scalar(0x2AD6)!), "swarhk;": Character(Unicode.Scalar(0x2926)!),
+ "swArr;": Character(Unicode.Scalar(0x21D9)!), "swarr;": Character(Unicode.Scalar(0x2199)!), "swarrow;": Character(Unicode.Scalar(0x2199)!), "swnwar;": Character(Unicode.Scalar(0x292A)!),
+ "szlig;": Character(Unicode.Scalar(0xDF)!), "Tab;": Character(Unicode.Scalar(0x9)!), "target;": Character(Unicode.Scalar(0x2316)!), "Tau;": Character(Unicode.Scalar(0x3A4)!),
+ "tau;": Character(Unicode.Scalar(0x3C4)!), "tbrk;": Character(Unicode.Scalar(0x23B4)!), "Tcaron;": Character(Unicode.Scalar(0x164)!), "tcaron;": Character(Unicode.Scalar(0x165)!),
+ "Tcedil;": Character(Unicode.Scalar(0x162)!), "tcedil;": Character(Unicode.Scalar(0x163)!), "Tcy;": Character(Unicode.Scalar(0x422)!), "tcy;": Character(Unicode.Scalar(0x442)!),
+ "tdot;": Character(Unicode.Scalar(0x20DB)!), "telrec;": Character(Unicode.Scalar(0x2315)!), "Tfr;": Character(Unicode.Scalar(0x1D517)!), "tfr;": Character(Unicode.Scalar(0x1D531)!),
+ "there4;": Character(Unicode.Scalar(0x2234)!), "Therefore;": Character(Unicode.Scalar(0x2234)!), "therefore;": Character(Unicode.Scalar(0x2234)!), "Theta;": Character(Unicode.Scalar(0x398)!),
+ "theta;": Character(Unicode.Scalar(0x3B8)!), "thetasym;": Character(Unicode.Scalar(0x3D1)!), "thetav;": Character(Unicode.Scalar(0x3D1)!), "thickapprox;": Character(Unicode.Scalar(0x2248)!),
+ "thicksim;": Character(Unicode.Scalar(0x223C)!),
+
+ // Skip "ThickSpace;" due to Swift not recognizing it as a single grapheme cluster
+ // "ThickSpace;":Character(Unicode.Scalar(0x205F}\u{200A)!),
+
+ "thinsp;": Character(Unicode.Scalar(0x2009)!), "ThinSpace;": Character(Unicode.Scalar(0x2009)!), "thkap;": Character(Unicode.Scalar(0x2248)!), "thksim;": Character(Unicode.Scalar(0x223C)!),
+ "THORN;": Character(Unicode.Scalar(0xDE)!), "thorn;": Character(Unicode.Scalar(0xFE)!), "Tilde;": Character(Unicode.Scalar(0x223C)!), "tilde;": Character(Unicode.Scalar(0x2DC)!),
+ "TildeEqual;": Character(Unicode.Scalar(0x2243)!), "TildeFullEqual;": Character(Unicode.Scalar(0x2245)!), "TildeTilde;": Character(Unicode.Scalar(0x2248)!), "times;": Character(Unicode.Scalar(0xD7)!),
+ "timesb;": Character(Unicode.Scalar(0x22A0)!), "timesbar;": Character(Unicode.Scalar(0x2A31)!), "timesd;": Character(Unicode.Scalar(0x2A30)!), "tint;": Character(Unicode.Scalar(0x222D)!),
+ "toea;": Character(Unicode.Scalar(0x2928)!), "top;": Character(Unicode.Scalar(0x22A4)!), "topbot;": Character(Unicode.Scalar(0x2336)!), "topcir;": Character(Unicode.Scalar(0x2AF1)!),
+ "Topf;": Character(Unicode.Scalar(0x1D54B)!), "topf;": Character(Unicode.Scalar(0x1D565)!), "topfork;": Character(Unicode.Scalar(0x2ADA)!), "tosa;": Character(Unicode.Scalar(0x2929)!),
+ "tprime;": Character(Unicode.Scalar(0x2034)!), "TRADE;": Character(Unicode.Scalar(0x2122)!), "trade;": Character(Unicode.Scalar(0x2122)!), "triangle;": Character(Unicode.Scalar(0x25B5)!),
+ "triangledown;": Character(Unicode.Scalar(0x25BF)!), "triangleleft;": Character(Unicode.Scalar(0x25C3)!), "trianglelefteq;": Character(Unicode.Scalar(0x22B4)!), "triangleq;": Character(Unicode.Scalar(0x225C)!),
+ "triangleright;": Character(Unicode.Scalar(0x25B9)!), "trianglerighteq;": Character(Unicode.Scalar(0x22B5)!), "tridot;": Character(Unicode.Scalar(0x25EC)!), "trie;": Character(Unicode.Scalar(0x225C)!),
+ "triminus;": Character(Unicode.Scalar(0x2A3A)!), "TripleDot;": Character(Unicode.Scalar(0x20DB)!), "triplus;": Character(Unicode.Scalar(0x2A39)!), "trisb;": Character(Unicode.Scalar(0x29CD)!),
+ "tritime;": Character(Unicode.Scalar(0x2A3B)!), "trpezium;": Character(Unicode.Scalar(0x23E2)!), "Tscr;": Character(Unicode.Scalar(0x1D4AF)!), "tscr;": Character(Unicode.Scalar(0x1D4C9)!),
+ "TScy;": Character(Unicode.Scalar(0x426)!), "tscy;": Character(Unicode.Scalar(0x446)!), "TSHcy;": Character(Unicode.Scalar(0x40B)!), "tshcy;": Character(Unicode.Scalar(0x45B)!),
+ "Tstrok;": Character(Unicode.Scalar(0x166)!), "tstrok;": Character(Unicode.Scalar(0x167)!), "twixt;": Character(Unicode.Scalar(0x226C)!), "twoheadleftarrow;": Character(Unicode.Scalar(0x219E)!),
+ "twoheadrightarrow;": Character(Unicode.Scalar(0x21A0)!), "Uacute;": Character(Unicode.Scalar(0xDA)!), "uacute;": Character(Unicode.Scalar(0xFA)!), "Uarr;": Character(Unicode.Scalar(0x219F)!),
+ "uArr;": Character(Unicode.Scalar(0x21D1)!), "uarr;": Character(Unicode.Scalar(0x2191)!), "Uarrocir;": Character(Unicode.Scalar(0x2949)!), "Ubrcy;": Character(Unicode.Scalar(0x40E)!),
+ "ubrcy;": Character(Unicode.Scalar(0x45E)!), "Ubreve;": Character(Unicode.Scalar(0x16C)!), "ubreve;": Character(Unicode.Scalar(0x16D)!), "Ucirc;": Character(Unicode.Scalar(0xDB)!),
+ "ucirc;": Character(Unicode.Scalar(0xFB)!), "Ucy;": Character(Unicode.Scalar(0x423)!), "ucy;": Character(Unicode.Scalar(0x443)!), "udarr;": Character(Unicode.Scalar(0x21C5)!),
+ "Udblac;": Character(Unicode.Scalar(0x170)!), "udblac;": Character(Unicode.Scalar(0x171)!), "udhar;": Character(Unicode.Scalar(0x296E)!), "ufisht;": Character(Unicode.Scalar(0x297E)!),
+ "Ufr;": Character(Unicode.Scalar(0x1D518)!), "ufr;": Character(Unicode.Scalar(0x1D532)!), "Ugrave;": Character(Unicode.Scalar(0xD9)!), "ugrave;": Character(Unicode.Scalar(0xF9)!),
+ "uHar;": Character(Unicode.Scalar(0x2963)!), "uharl;": Character(Unicode.Scalar(0x21BF)!), "uharr;": Character(Unicode.Scalar(0x21BE)!), "uhblk;": Character(Unicode.Scalar(0x2580)!),
+ "ulcorn;": Character(Unicode.Scalar(0x231C)!), "ulcorner;": Character(Unicode.Scalar(0x231C)!), "ulcrop;": Character(Unicode.Scalar(0x230F)!), "ultri;": Character(Unicode.Scalar(0x25F8)!),
+ "Umacr;": Character(Unicode.Scalar(0x16A)!), "umacr;": Character(Unicode.Scalar(0x16B)!), "uml;": Character(Unicode.Scalar(0xA8)!), "UnderBar;": Character(Unicode.Scalar(0x5F)!),
+ "UnderBrace;": Character(Unicode.Scalar(0x23DF)!), "UnderBracket;": Character(Unicode.Scalar(0x23B5)!), "UnderParenthesis;": Character(Unicode.Scalar(0x23DD)!), "Union;": Character(Unicode.Scalar(0x22C3)!),
+ "UnionPlus;": Character(Unicode.Scalar(0x228E)!), "Uogon;": Character(Unicode.Scalar(0x172)!), "uogon;": Character(Unicode.Scalar(0x173)!), "Uopf;": Character(Unicode.Scalar(0x1D54C)!),
+ "uopf;": Character(Unicode.Scalar(0x1D566)!), "UpArrow;": Character(Unicode.Scalar(0x2191)!), "Uparrow;": Character(Unicode.Scalar(0x21D1)!), "uparrow;": Character(Unicode.Scalar(0x2191)!),
+ "UpArrowBar;": Character(Unicode.Scalar(0x2912)!), "UpArrowDownArrow;": Character(Unicode.Scalar(0x21C5)!), "UpDownArrow;": Character(Unicode.Scalar(0x2195)!), "Updownarrow;": Character(Unicode.Scalar(0x21D5)!),
+ "updownarrow;": Character(Unicode.Scalar(0x2195)!), "UpEquilibrium;": Character(Unicode.Scalar(0x296E)!), "upharpoonleft;": Character(Unicode.Scalar(0x21BF)!), "upharpoonright;": Character(Unicode.Scalar(0x21BE)!),
+ "uplus;": Character(Unicode.Scalar(0x228E)!), "UpperLeftArrow;": Character(Unicode.Scalar(0x2196)!), "UpperRightArrow;": Character(Unicode.Scalar(0x2197)!), "Upsi;": Character(Unicode.Scalar(0x3D2)!),
+ "upsi;": Character(Unicode.Scalar(0x3C5)!), "upsih;": Character(Unicode.Scalar(0x3D2)!), "Upsilon;": Character(Unicode.Scalar(0x3A5)!), "upsilon;": Character(Unicode.Scalar(0x3C5)!),
+ "UpTee;": Character(Unicode.Scalar(0x22A5)!), "UpTeeArrow;": Character(Unicode.Scalar(0x21A5)!), "upuparrows;": Character(Unicode.Scalar(0x21C8)!), "urcorn;": Character(Unicode.Scalar(0x231D)!),
+ "urcorner;": Character(Unicode.Scalar(0x231D)!), "urcrop;": Character(Unicode.Scalar(0x230E)!), "Uring;": Character(Unicode.Scalar(0x16E)!), "uring;": Character(Unicode.Scalar(0x16F)!),
+ "urtri;": Character(Unicode.Scalar(0x25F9)!), "Uscr;": Character(Unicode.Scalar(0x1D4B0)!), "uscr;": Character(Unicode.Scalar(0x1D4CA)!), "utdot;": Character(Unicode.Scalar(0x22F0)!),
+ "Utilde;": Character(Unicode.Scalar(0x168)!), "utilde;": Character(Unicode.Scalar(0x169)!), "utri;": Character(Unicode.Scalar(0x25B5)!), "utrif;": Character(Unicode.Scalar(0x25B4)!),
+ "uuarr;": Character(Unicode.Scalar(0x21C8)!), "Uuml;": Character(Unicode.Scalar(0xDC)!), "uuml;": Character(Unicode.Scalar(0xFC)!), "uwangle;": Character(Unicode.Scalar(0x29A7)!),
+ "vangrt;": Character(Unicode.Scalar(0x299C)!), "varepsilon;": Character(Unicode.Scalar(0x3F5)!), "varkappa;": Character(Unicode.Scalar(0x3F0)!), "varnothing;": Character(Unicode.Scalar(0x2205)!),
+ "varphi;": Character(Unicode.Scalar(0x3D5)!), "varpi;": Character(Unicode.Scalar(0x3D6)!), "varpropto;": Character(Unicode.Scalar(0x221D)!), "vArr;": Character(Unicode.Scalar(0x21D5)!),
+ "varr;": Character(Unicode.Scalar(0x2195)!), "varrho;": Character(Unicode.Scalar(0x3F1)!), "varsigma;": Character(Unicode.Scalar(0x3C2)!), "varsubsetneq;": "\u{228A}\u{FE00}",
+ "varsubsetneqq;": "\u{2ACB}\u{FE00}", "varsupsetneq;": "\u{228B}\u{FE00}", "varsupsetneqq;": "\u{2ACC}\u{FE00}", "vartheta;": Character(Unicode.Scalar(0x3D1)!),
+ "vartriangleleft;": Character(Unicode.Scalar(0x22B2)!), "vartriangleright;": Character(Unicode.Scalar(0x22B3)!), "Vbar;": Character(Unicode.Scalar(0x2AEB)!), "vBar;": Character(Unicode.Scalar(0x2AE8)!),
+ "vBarv;": Character(Unicode.Scalar(0x2AE9)!), "Vcy;": Character(Unicode.Scalar(0x412)!), "vcy;": Character(Unicode.Scalar(0x432)!), "VDash;": Character(Unicode.Scalar(0x22AB)!),
+ "Vdash;": Character(Unicode.Scalar(0x22A9)!), "vDash;": Character(Unicode.Scalar(0x22A8)!), "vdash;": Character(Unicode.Scalar(0x22A2)!), "Vdashl;": Character(Unicode.Scalar(0x2AE6)!),
+ "Vee;": Character(Unicode.Scalar(0x22C1)!), "vee;": Character(Unicode.Scalar(0x2228)!), "veebar;": Character(Unicode.Scalar(0x22BB)!), "veeeq;": Character(Unicode.Scalar(0x225A)!),
+ "vellip;": Character(Unicode.Scalar(0x22EE)!), "Verbar;": Character(Unicode.Scalar(0x2016)!), "verbar;": Character(Unicode.Scalar(0x7C)!), "Vert;": Character(Unicode.Scalar(0x2016)!),
+ "vert;": Character(Unicode.Scalar(0x7C)!), "VerticalBar;": Character(Unicode.Scalar(0x2223)!), "VerticalLine;": Character(Unicode.Scalar(0x7C)!), "VerticalSeparator;": Character(Unicode.Scalar(0x2758)!),
+ "VerticalTilde;": Character(Unicode.Scalar(0x2240)!), "VeryThinSpace;": Character(Unicode.Scalar(0x200A)!), "Vfr;": Character(Unicode.Scalar(0x1D519)!), "vfr;": Character(Unicode.Scalar(0x1D533)!),
+ "vltri;": Character(Unicode.Scalar(0x22B2)!), "vnsub;": "\u{2282}\u{20D2}", "vnsup;": "\u{2283}\u{20D2}", "Vopf;": Character(Unicode.Scalar(0x1D54D)!),
+ "vopf;": Character(Unicode.Scalar(0x1D567)!), "vprop;": Character(Unicode.Scalar(0x221D)!), "vrtri;": Character(Unicode.Scalar(0x22B3)!), "Vscr;": Character(Unicode.Scalar(0x1D4B1)!),
+ "vscr;": Character(Unicode.Scalar(0x1D4CB)!), "vsubnE;": "\u{2ACB}\u{FE00}", "vsubne;": "\u{228A}\u{FE00}", "vsupnE;": "\u{2ACC}\u{FE00}",
+ "vsupne;": "\u{228B}\u{FE00}", "Vvdash;": Character(Unicode.Scalar(0x22AA)!), "vzigzag;": Character(Unicode.Scalar(0x299A)!), "Wcirc;": Character(Unicode.Scalar(0x174)!),
+ "wcirc;": Character(Unicode.Scalar(0x175)!), "wedbar;": Character(Unicode.Scalar(0x2A5F)!), "Wedge;": Character(Unicode.Scalar(0x22C0)!), "wedge;": Character(Unicode.Scalar(0x2227)!),
+ "wedgeq;": Character(Unicode.Scalar(0x2259)!), "weierp;": Character(Unicode.Scalar(0x2118)!), "Wfr;": Character(Unicode.Scalar(0x1D51A)!), "wfr;": Character(Unicode.Scalar(0x1D534)!),
+ "Wopf;": Character(Unicode.Scalar(0x1D54E)!), "wopf;": Character(Unicode.Scalar(0x1D568)!), "wp;": Character(Unicode.Scalar(0x2118)!), "wr;": Character(Unicode.Scalar(0x2240)!),
+ "wreath;": Character(Unicode.Scalar(0x2240)!), "Wscr;": Character(Unicode.Scalar(0x1D4B2)!), "wscr;": Character(Unicode.Scalar(0x1D4CC)!), "xcap;": Character(Unicode.Scalar(0x22C2)!),
+ "xcirc;": Character(Unicode.Scalar(0x25EF)!), "xcup;": Character(Unicode.Scalar(0x22C3)!), "xdtri;": Character(Unicode.Scalar(0x25BD)!), "Xfr;": Character(Unicode.Scalar(0x1D51B)!),
+ "xfr;": Character(Unicode.Scalar(0x1D535)!), "xhArr;": Character(Unicode.Scalar(0x27FA)!), "xharr;": Character(Unicode.Scalar(0x27F7)!), "Xi;": Character(Unicode.Scalar(0x39E)!),
+ "xi;": Character(Unicode.Scalar(0x3BE)!), "xlArr;": Character(Unicode.Scalar(0x27F8)!), "xlarr;": Character(Unicode.Scalar(0x27F5)!), "xmap;": Character(Unicode.Scalar(0x27FC)!),
+ "xnis;": Character(Unicode.Scalar(0x22FB)!), "xodot;": Character(Unicode.Scalar(0x2A00)!), "Xopf;": Character(Unicode.Scalar(0x1D54F)!), "xopf;": Character(Unicode.Scalar(0x1D569)!),
+ "xoplus;": Character(Unicode.Scalar(0x2A01)!), "xotime;": Character(Unicode.Scalar(0x2A02)!), "xrArr;": Character(Unicode.Scalar(0x27F9)!), "xrarr;": Character(Unicode.Scalar(0x27F6)!),
+ "Xscr;": Character(Unicode.Scalar(0x1D4B3)!), "xscr;": Character(Unicode.Scalar(0x1D4CD)!), "xsqcup;": Character(Unicode.Scalar(0x2A06)!), "xuplus;": Character(Unicode.Scalar(0x2A04)!),
+ "xutri;": Character(Unicode.Scalar(0x25B3)!), "xvee;": Character(Unicode.Scalar(0x22C1)!), "xwedge;": Character(Unicode.Scalar(0x22C0)!), "Yacute;": Character(Unicode.Scalar(0xDD)!),
+ "yacute;": Character(Unicode.Scalar(0xFD)!), "YAcy;": Character(Unicode.Scalar(0x42F)!), "yacy;": Character(Unicode.Scalar(0x44F)!), "Ycirc;": Character(Unicode.Scalar(0x176)!),
+ "ycirc;": Character(Unicode.Scalar(0x177)!), "Ycy;": Character(Unicode.Scalar(0x42B)!), "ycy;": Character(Unicode.Scalar(0x44B)!), "yen;": Character(Unicode.Scalar(0xA5)!),
+ "Yfr;": Character(Unicode.Scalar(0x1D51C)!), "yfr;": Character(Unicode.Scalar(0x1D536)!), "YIcy;": Character(Unicode.Scalar(0x407)!), "yicy;": Character(Unicode.Scalar(0x457)!),
+ "Yopf;": Character(Unicode.Scalar(0x1D550)!), "yopf;": Character(Unicode.Scalar(0x1D56A)!), "Yscr;": Character(Unicode.Scalar(0x1D4B4)!), "yscr;": Character(Unicode.Scalar(0x1D4CE)!),
+ "YUcy;": Character(Unicode.Scalar(0x42E)!), "yucy;": Character(Unicode.Scalar(0x44E)!), "Yuml;": Character(Unicode.Scalar(0x178)!), "yuml;": Character(Unicode.Scalar(0xFF)!),
+ "Zacute;": Character(Unicode.Scalar(0x179)!), "zacute;": Character(Unicode.Scalar(0x17A)!), "Zcaron;": Character(Unicode.Scalar(0x17D)!), "zcaron;": Character(Unicode.Scalar(0x17E)!),
+ "Zcy;": Character(Unicode.Scalar(0x417)!), "zcy;": Character(Unicode.Scalar(0x437)!), "Zdot;": Character(Unicode.Scalar(0x17B)!), "zdot;": Character(Unicode.Scalar(0x17C)!),
+ "zeetrf;": Character(Unicode.Scalar(0x2128)!), "ZeroWidthSpace;": Character(Unicode.Scalar(0x200B)!), "Zeta;": Character(Unicode.Scalar(0x396)!), "zeta;": Character(Unicode.Scalar(0x3B6)!),
+ "Zfr;": Character(Unicode.Scalar(0x2128)!), "zfr;": Character(Unicode.Scalar(0x1D537)!), "ZHcy;": Character(Unicode.Scalar(0x416)!), "zhcy;": Character(Unicode.Scalar(0x436)!),
+ "zigrarr;": Character(Unicode.Scalar(0x21DD)!), "Zopf;": Character(Unicode.Scalar(0x2124)!), "zopf;": Character(Unicode.Scalar(0x1D56B)!), "Zscr;": Character(Unicode.Scalar(0x1D4B5)!),
+ "zscr;": Character(Unicode.Scalar(0x1D4CF)!), "zwj;": Character(Unicode.Scalar(0x200D)!), "zwnj;": Character(Unicode.Scalar(0x200C)!)
+]
diff --git a/Sources/Liquid/HTMLEntities/ParseError.swift b/Sources/Liquid/HTMLEntities/ParseError.swift
new file mode 100644
index 0000000..52d2444
--- /dev/null
+++ b/Sources/Liquid/HTMLEntities/ParseError.swift
@@ -0,0 +1,56 @@
+/*
+ * Copyright IBM Corporation 2016, 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/// Enums used to delineate the different kinds of parse errors
+/// that may be encountered during HTML unescaping. See
+/// https://www.w3.org/TR/html5/syntax.html#tokenizing-character-references
+/// for an explanation of the different parse errors.
+public enum ParseError: Error {
+ /// "If that number is one of the numbers in the first column of the following
+ /// table, then this is a parse error."
+ case DeprecatedNumericReference(String)
+
+ /// "[I]f the number is in the range 0x0001 to 0x0008, 0x000D to 0x001F, 0x007F
+ /// to 0x009F, 0xFDD0 to 0xFDEF, or is one of 0x000B, 0xFFFE, 0xFFFF, 0x1FFFE,
+ /// 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE,
+ /// 0x5FFFF, 0x6FFFE, 0x6FFFF, 0x7FFFE, 0x7FFFF, 0x8FFFE, 0x8FFFF, 0x9FFFE,
+ /// 0x9FFFF, 0xAFFFE, 0xAFFFF, 0xBFFFE, 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE,
+ /// 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF, 0x10FFFE, or 0x10FFFF, then
+ /// this is a parse error."
+ case DisallowedNumericReference(String)
+
+ /// This should NEVER be hit in code execution. If this error is thrown, then
+ /// decoder has faulty logic
+ case IllegalArgument(String)
+
+ /// "[I]f the characters after the U+0026 AMPERSAND character (&) consist of
+ /// a sequence of one or more alphanumeric ASCII characters followed by a
+ /// U+003B SEMICOLON character (;), then this is a parse error."
+ case InvalidNamedReference(String)
+
+ /// "If no characters match the range, then don't consume any characters
+ /// (and unconsume the U+0023 NUMBER SIGN character and, if appropriate,
+ /// the X character). This is a parse error; nothing is returned."
+ case MalformedNumericReference(String)
+
+ /// "[I]f the next character is a U+003B SEMICOLON, consume that too.
+ /// If it isn't, there is a parse error."
+ case MissingSemicolon(String)
+
+ /// "[I]f the number is in the range 0xD800 to 0xDFFF or is greater
+ /// than 0x10FFFF, then this is a parse error."
+ case OutsideValidUnicodeRange(String)
+}
diff --git a/Sources/Liquid/HTMLEntities/String+HTMLEntities.swift b/Sources/Liquid/HTMLEntities/String+HTMLEntities.swift
new file mode 100644
index 0000000..40aac0b
--- /dev/null
+++ b/Sources/Liquid/HTMLEntities/String+HTMLEntities.swift
@@ -0,0 +1,494 @@
+/*
+ * Copyright IBM Corporation 2016, 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/// This String extension provides utility functions to convert strings to their
+/// HTML escaped equivalents and vice versa.
+public extension String {
+ /// Global HTML escape options
+ struct HTMLEscapeOptions {
+ /// Specifies if all ASCII characters should be skipped when escaping text
+ public static var allowUnsafeSymbols = false
+
+ /// Specifies if decimal escapes should be used instead of hexadecimal escapes
+ public static var decimal = false
+
+ /// Specifies if all characters should be escaped, even if some are safe characters
+ public static var encodeEverything = false
+
+ /// Specifies if named character references should be used whenever possible
+ public static var useNamedReferences = false
+ }
+
+ // Private enum used by the parser state machine
+ private enum EntityParseState {
+ case Dec
+ case Hex
+ case Invalid
+ case Named
+ case Number
+ case Unknown
+ }
+
+ /// Return string as HTML escaped by replacing non-ASCII and unsafe characters
+ /// with their numeric character escapes, or if such exists, their HTML named
+ /// character reference equivalents. For example, this function turns
+ ///
+ /// `""`
+ ///
+ /// into
+ ///
+ /// `"<script>alert("abc")</script>"`
+ ///
+ /// You can view/change default option values globally via `String.HTMLEscapeOptions`.
+ ///
+ /// - Parameter allowUnsafeSymbols: Specifies if all ASCII characters should be skipped
+ /// when escaping text. *Optional*
+ /// - Parameter decimal: Specifies if decimal escapes should be used instead of
+ /// hexadecimal escapes. *Optional*
+ /// - Parameter encodeEverything: Specifies if all characters should be escaped, even if
+ /// some are safe characters. *Optional*
+ /// - Parameter useNamedReferences: Specifies if named character references
+ /// should be used whenever possible. *Optional*
+ func htmlEscape(allowUnsafeSymbols: Bool = HTMLEscapeOptions.allowUnsafeSymbols,
+ decimal: Bool = HTMLEscapeOptions.decimal,
+ encodeEverything: Bool = HTMLEscapeOptions.encodeEverything,
+ useNamedReferences: Bool = HTMLEscapeOptions.useNamedReferences)
+ -> String {
+ // result buffer
+ var str: String = ""
+
+ #if swift(>=3.2)
+ let characters = self
+ #else
+ let characters = self.characters
+ #endif
+
+ for c in characters {
+ let unicodes = String(c).unicodeScalars
+
+ if !encodeEverything,
+ unicodes.count == 1,
+ let unicode = unicodes.first?.value,
+ unicode.isASCII && allowUnsafeSymbols || unicode.isSafeASCII {
+ // character consists of only one unicode,
+ // and is a safe ASCII character,
+ // or allowUnsafeSymbols is true
+ str += String(c)
+ } else if useNamedReferences,
+ let entity = namedCharactersEncodeMap[c] {
+ // character has a named character reference equivalent
+ str += "&" + entity
+ } else {
+ // all other cases:
+ // deconstruct character into unicodes,
+ // then escape each unicode individually
+ for scalar in unicodes {
+ let unicode = scalar.value
+
+ if !encodeEverything && unicode.isSafeASCII {
+ str += String(scalar)
+ } else {
+ let codeStr = decimal ? String(unicode, radix: 10) :
+ "x" + String(unicode, radix: 16, uppercase: true)
+
+ str += "" + codeStr + ";"
+ }
+ }
+ }
+ }
+
+ return str
+ }
+
+ /// Return string as HTML unescaped by replacing HTML character references with their
+ /// unicode character equivalents. For example, this function turns
+ ///
+ /// `"<script>alert("abc")</script>"`
+ ///
+ /// into
+ ///
+ /// `""`
+ ///
+ /// - Parameter strict: Specifies if escapes MUST always end with `;`.
+ /// - Throws: (Only if `strict == true`) The first `ParseError` encountered during parsing.
+ func htmlUnescape(strict: Bool) throws -> String {
+ // result buffer
+ var str = ""
+
+ // entity buffers
+ var entityPrefix = ""
+ var entity = ""
+
+ // current parse state
+ var state = EntityParseState.Invalid
+
+ for u in self.unicodeScalars {
+ let unicodeAsString = String(u)
+ let unicode = u.value
+
+ // nondeterminstic finite automaton for parsing entity
+ switch state {
+ case .Invalid:
+ if unicode.isAmpersand {
+ // start of a possible character reference
+ state = .Unknown
+ entityPrefix = unicodeAsString
+ } else {
+ // move unicode to result buffer
+ str += unicodeAsString
+ }
+ case .Unknown:
+ // previously parsed &
+ // need to determine type of character reference
+ if unicode.isAmpersand {
+ // parsed & again
+ // move previous & to result buffer
+ str += unicodeAsString
+ } else if unicode.isHash {
+ // numeric character reference
+ state = .Number
+ entityPrefix += unicodeAsString
+ } else if unicode.isAlphaNumeric {
+ // named character reference
+ state = .Named
+
+ // move current unicode to entity buffer
+ entity += unicodeAsString
+ } else {
+ // false alarm, not a character reference
+ // move back to invalid state
+ state = .Invalid
+
+ // move the consumed & and current unicode to result buffer
+ str += entityPrefix + unicodeAsString
+
+ // clear entityPrefix buffer
+ entityPrefix = ""
+ }
+ case .Number:
+ // previously parsed
+ // need to determine dec or hex
+ if unicode.isAmpersand {
+ // parsed & again
+ if strict {
+ // https://www.w3.org/TR/html5/syntax.html#tokenizing-character-references
+ // "If no characters match the range, then don't consume any characters
+ // (and unconsume the U+0023 NUMBER SIGN character and, if appropriate,
+ // the X character). This is a parse error; nothing is returned."
+ throw ParseError.MalformedNumericReference(entityPrefix + unicodeAsString)
+ }
+
+ // move the consume to result buffer
+ str += entityPrefix
+
+ // move to unknown state
+ state = .Unknown
+ entityPrefix = unicodeAsString
+ } else if unicode.isX {
+ // hexadecimal numeric character reference
+ state = .Hex
+ entityPrefix += unicodeAsString
+ } else if unicode.isNumeral {
+ // decimal numeric character reference
+ state = .Dec
+ entity += unicodeAsString
+ } else {
+ // false alarm, not a character reference
+ if strict {
+ // https://www.w3.org/TR/html5/syntax.html#tokenizing-character-references
+ // "If no characters match the range, then don't consume any characters
+ // (and unconsume the U+0023 NUMBER SIGN character and, if appropriate,
+ // the X character). This is a parse error; nothing is returned."
+ throw ParseError.MalformedNumericReference(entityPrefix + unicodeAsString)
+ }
+
+ // move the consumed and current unicode to result buffer
+ str += entityPrefix + unicodeAsString
+
+ // move to invalid state
+ state = .Invalid
+ entityPrefix = ""
+ entity = ""
+ }
+ case .Dec, .Hex:
+ // previously parsed [0-9]+ or [xX][0-9A-Fa-f]*
+ if state == .Dec && unicode.isNumeral || state == .Hex && unicode.isHexNumeral {
+ // greedy matching
+ // consume as many valid characters as possible before unescaping
+ entity += unicodeAsString
+ } else {
+ // current character is not in matching range
+ if strict {
+ if entity == "" {
+ // no characters matching range was parsed
+ // https://www.w3.org/TR/html5/syntax.html#tokenizing-character-references
+ // "If no characters match the range, then don't consume any characters
+ // (and unconsume the U+0023 NUMBER SIGN character and, if appropriate,
+ // the X character). This is a parse error; nothing is returned."
+ throw ParseError.MalformedNumericReference(entityPrefix + unicodeAsString)
+ }
+
+ if !unicode.isSemicolon {
+ // entity did not end with ;
+ // https://www.w3.org/TR/html5/syntax.html#tokenizing-character-references
+ // "[I]f the next character is a U+003B SEMICOLON, consume that too.
+ // If it isn't, there is a parse error."
+ throw ParseError.MissingSemicolon(entityPrefix + entity)
+ }
+ }
+
+ let unescaped = try decode(entity: entity, entityPrefix: entityPrefix, strict: strict)
+
+ // append unescaped numeric reference to result buffer
+ str += unescaped
+
+ if unicode.isAmpersand {
+ // parsed & again
+ // move to unknown state
+ state = .Unknown
+ entityPrefix = unicodeAsString
+ entity = ""
+ } else {
+ if !unicode.isSemicolon {
+ // move current unicode to result buffer
+ str += unicodeAsString
+ }
+
+ // move back to invalid state
+ state = .Invalid
+ entityPrefix = ""
+ entity = ""
+ }
+ }
+ case .Named:
+ // previously parsed &[0-9A-Za-z]+
+ if unicode.isAlphaNumeric {
+ // keep consuming alphanumeric unicodes
+ // only try to decode it when we encounter a nonalphanumeric unicode
+ entity += unicodeAsString
+ } else {
+ if unicode.isSemicolon {
+ entity += unicodeAsString
+ }
+
+ // try to decode parsed chunk of alphanumeric unicodes
+ let unescaped = try decode(entity: entity, entityPrefix: entityPrefix, strict: strict)
+
+ str += unescaped
+
+ if unicode.isAmpersand {
+ // parsed & again
+ // move to unknown state
+ state = .Unknown
+ entityPrefix = unicodeAsString
+ entity = ""
+
+ break
+ } else if !unicode.isSemicolon {
+ // move current unicode to result buffer
+ str += unicodeAsString
+ }
+
+ // move back to invalid state
+ state = .Invalid
+ entityPrefix = ""
+ entity = ""
+ }
+ }
+ }
+
+ // one more round of finite automaton to catch the edge case where the original string
+ // ends with a character reference that isn't terminated by ;
+ switch state {
+ case .Dec, .Hex:
+ // parsed a partial numeric character reference
+ if strict {
+ if entity == "" {
+ // no characters matching range was parsed
+ // https://www.w3.org/TR/html5/syntax.html#tokenizing-character-references
+ // "If no characters match the range, then don't consume any characters
+ // (and unconsume the U+0023 NUMBER SIGN character and, if appropriate,
+ // the X character). This is a parse error; nothing is returned."
+ throw ParseError.MalformedNumericReference(entityPrefix)
+ }
+
+ // by this point in code, entity is not empty and did not end with ;
+ // if it did, the numeric character reference would've been unescaped inside the loop
+ // https://www.w3.org/TR/html5/syntax.html#tokenizing-character-references
+ // "[I]f the next character is a U+003B SEMICOLON, consume that too.
+ // If it isn't, there is a parse error."
+ throw ParseError.MissingSemicolon(entityPrefix + entity)
+ }
+
+ fallthrough
+ case .Named:
+ // parsed a partial character reference
+ // unescape what we have left
+ str += try decode(entity: entity, entityPrefix: entityPrefix, strict: strict)
+ default:
+ // all other states
+ // dump partial buffers into result string
+ str += entityPrefix + entity
+ }
+
+ return str
+ }
+
+ /// Return string as HTML unescaped by replacing HTML character references with their
+ /// unicode character equivalents. For example, this function turns
+ ///
+ /// `"<script>alert("abc")</script>"`
+ ///
+ /// into
+ ///
+ /// `""`
+ ///
+ /// Equivalent to `htmlUnescape(strict: false)`, but does NOT throw parse error.
+ func htmlUnescape() -> String {
+ // non-strict mode should never throw error
+ return try! self.htmlUnescape(strict: false)
+ }
+}
+
+// Utility function to decode a single entity
+private func decode(entity: String, entityPrefix: String, strict: Bool) throws -> String {
+ switch entityPrefix {
+ case "", "", "":
+ // numeric character reference
+ let radix = entityPrefix == "" ? 10 : 16
+
+ if strict && entity == "" {
+ // https://www.w3.org/TR/html5/syntax.html#tokenizing-character-references
+ // "If no characters match the range, then don't consume any characters
+ // (and unconsume the U+0023 NUMBER SIGN character and, if appropriate,
+ // the X character). This is a parse error; nothing is returned."
+ throw ParseError.MalformedNumericReference(entityPrefix)
+ } else if var code = UInt32(entity, radix: radix) {
+ if code.isReplacementCharacterEquivalent {
+ code = replacementCharacterAsUInt32
+
+ if strict {
+ // https://www.w3.org/TR/html5/syntax.html#tokenizing-character-references
+ // "[I]f the number is in the range 0xD800 to 0xDFFF or is greater
+ // than 0x10FFFF, then this is a parse error."
+ throw ParseError.OutsideValidUnicodeRange(entityPrefix + entity)
+ }
+ } else if let c = deprecatedNumericDecodeMap[code] {
+ code = c
+
+ if strict {
+ // https://www.w3.org/TR/html5/syntax.html#tokenizing-character-references
+ // "If that number is one of the numbers in the first column of the
+ // following table, then this is a parse error."
+ throw ParseError.DeprecatedNumericReference(entityPrefix + entity)
+ }
+ } else if strict && code.isDisallowedReference {
+ // https://www.w3.org/TR/html5/syntax.html#tokenizing-character-references
+ // "[I]f the number is in the range 0x0001 to 0x0008, 0x000D to 0x001F, 0x007F
+ // to 0x009F, 0xFDD0 to 0xFDEF, or is one of 0x000B, 0xFFFE, 0xFFFF, 0x1FFFE,
+ // 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE,
+ // 0x5FFFF, 0x6FFFE, 0x6FFFF, 0x7FFFE, 0x7FFFF, 0x8FFFE, 0x8FFFF, 0x9FFFE,
+ // 0x9FFFF, 0xAFFFE, 0xAFFFF, 0xBFFFE, 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE,
+ // 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF, 0x10FFFE, or 0x10FFFF, then
+ // this is a parse error."
+ throw ParseError.DisallowedNumericReference(entityPrefix + entity)
+ }
+
+ return String(UnicodeScalar(code)!)
+ } else {
+ // Assume entity is nonempty and only contains valid characters for the given type
+ // of numeric character reference. Given this assumption, at this point in the code
+ // the numeric character reference must be greater than `UInt32.max`, i.e., it is
+ // not representable by UInt32 (and it is, by transitivity, greater than 0x10FFFF);
+ // therefore, the numeric character reference should be replaced by U+FFFD
+ if strict {
+ // https://www.w3.org/TR/html5/syntax.html#tokenizing-character-references
+ // "[I]f the number is in the range 0xD800 to 0xDFFF or is greater
+ // than 0x10FFFF, then this is a parse error."
+ throw ParseError.OutsideValidUnicodeRange(entityPrefix + entity)
+ }
+
+ return String(UnicodeScalar(replacementCharacterAsUInt32)!)
+ }
+ case "&":
+ // named character reference
+ if entity == "" {
+ return entityPrefix
+ }
+
+ if entity.hasSuffix(";") {
+ // Step 1: check all other named characters first
+ // Assume special case is rare, always check regular case first to minimize
+ // search time cost amortization
+ if let c = namedCharactersDecodeMap[entity] {
+ return String(c)
+ }
+
+ // Step 2: check special named characters if entity didn't match any regular
+ // named character references
+ if let s = specialNamedCharactersDecodeMap[entity] {
+ return s
+ }
+ }
+
+ for length in legacyNamedCharactersLengthRange {
+ #if swift(>=3.2)
+ let count = entity.count
+ #else
+ let count = entity.characters.count
+ #endif
+
+ guard length <= count else {
+ break
+ }
+
+ let upperIndex = entity.index(entity.startIndex, offsetBy: length)
+
+ #if swift(>=3.2)
+ let reference = String(entity[.. in the argument will override
+ /// the current dictionary's if the keys match
+ func updating(_ dict: [Key: Value]) -> [Key: Value] {
+ var newDict = self
+
+ for (key, value) in dict {
+ newDict[key] = value
+ }
+
+ return newDict
+ }
+}
+
+extension Dictionary where Value: Hashable {
+ /// Invert a dictionary: ->
+ /// Note: Does not check for uniqueness among values
+ func inverting(_ pick: (Key, Key) -> Key = { existingValue, newValue in
+ return newValue
+ }) -> [Value: Key] {
+ var inverseDict: [Value: Key] = [:]
+
+ for (key, value) in self {
+ if let existing = inverseDict[value] {
+ inverseDict[value] = pick(existing, key)
+ } else {
+ inverseDict[value] = key
+ }
+ }
+
+ return inverseDict
+ }
+}
+
+extension UInt32 {
+ var isAlphaNumeric: Bool {
+ // unicode values of [0-9], [A-Z], and [a-z]
+ return self.isNumeral || 0x41...0x5A ~= self || 0x61...0x7A ~= self
+ }
+
+ var isAmpersand: Bool {
+ // unicode value of &
+ return self == 0x26
+ }
+
+ var isASCII: Bool {
+ // Less than 0x80
+ return self < 0x80
+ }
+
+ /// https://www.w3.org/International/questions/qa-escapes#use
+ var isAttributeSyntax: Bool {
+ // unicode values of [", ']
+ return self == 0x22 || self == 0x27
+ }
+
+ var isDisallowedReference: Bool {
+ // unicode values of [0x1-0x8], [0xD-0x1F], [0x7F-0x9F], [0xFDD0-0xFDEF],
+ // 0xB, 0xFFFE, 0xFFFF, 0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, 0x3FFFF,
+ // 0x4FFFE, 0x4FFFF, 0x5FFFE, 0x5FFFF, 0x6FFFE, 0x6FFFF, 0x7FFFE, 0x7FFFF,
+ // 0x8FFFE, 0x8FFFF, 0x9FFFE, 0x9FFFF, 0xAFFFE, 0xAFFFF, 0xBFFFE, 0xBFFFF,
+ // 0xCFFFE, 0xCFFFF, 0xDFFFE, 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF,
+ // 0x10FFFE, and 0x10FFFF
+ return disallowedNumericReferences.contains(self)
+ }
+
+ var isHash: Bool {
+ // unicode value of #
+ return self == 0x23
+ }
+
+ var isHexNumeral: Bool {
+ // unicode values of [0-9], [A-F], and [a-f]
+ return isNumeral || 0x41...0x46 ~= self || 0x61...0x66 ~= self
+ }
+
+ var isNumeral: Bool {
+ // unicode values of [0-9]
+ return 0x30...0x39 ~= self
+ }
+
+ /// https://www.w3.org/TR/html5/syntax.html#tokenizing-character-references
+ var isReplacementCharacterEquivalent: Bool {
+ // UInt32 values of [0xD800-0xDFFF], (0x10FFFF-∞]
+ return 0xD800...0xDFFF ~= self || 0x10FFFF < self
+ }
+
+ var isSafeASCII: Bool {
+ return self.isASCII && !self.isAttributeSyntax && !self.isTagSyntax
+ }
+
+ var isSemicolon: Bool {
+ // unicode value of ;
+ return self == 0x3B
+ }
+
+ /// https://www.w3.org/International/questions/qa-escapes#use
+ var isTagSyntax: Bool {
+ // unicode values of [&, < , >]
+ return self.isAmpersand || self == 0x3C || self == 0x3E
+ }
+
+ var isX: Bool {
+ // unicode values of X and x
+ return self == 0x58 || self == 0x78
+ }
+}
diff --git a/Sources/Liquid/Lexer.swift b/Sources/Liquid/Lexer.swift
new file mode 100644
index 0000000..352096e
--- /dev/null
+++ b/Sources/Liquid/Lexer.swift
@@ -0,0 +1,89 @@
+//
+// Lexer.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-02.
+//
+
+import Foundation
+
+enum Lexer {
+ struct Token {
+ enum Kind {
+ case pipe
+ case dot
+ case colon
+ case comma
+ case openSquare
+ case closeSquare
+ case openRound
+ case closeRound
+ case question
+ case dash
+ case equal
+ case comparison
+ case string
+ case integer
+ case decimal
+ case id
+ case dotDot
+ case endOfString
+ }
+
+ let kind: Kind
+ let value: String?
+
+ init(kind: Kind, value: String? = nil) {
+ self.kind = kind
+ self.value = value
+ }
+ }
+
+ private static let specials: [Character: Token.Kind] = [
+ "|": .pipe,
+ ".": .dot,
+ ":": .colon,
+ ",": .comma,
+ "[": .openSquare,
+ "]": .closeSquare,
+ "(": .openRound,
+ ")": .closeRound,
+ "?": .question,
+ "-": .dash,
+ "=": .equal
+ ]
+
+ static func tokenize(_ string: String) throws -> [Token] {
+ var tokens: [Token] = []
+ let scanner = Scanner(string: string)
+ while !scanner.isAtEnd {
+ _ = scanner.liquid_scanCharacters(from: .whitespacesAndNewlines)
+ guard !scanner.isAtEnd else { break }
+
+ if let t = try? scanner.liquid_scanRegex(#"==|!=|<>|<=?|>=?|contains(?=\s)"#) {
+ tokens.append(Token(kind: .comparison, value: t))
+ } else if let t = try? scanner.liquid_scanRegex(#"'([^\']*)'"#, captureGroup: 1) {
+ tokens.append(Token(kind: .string, value: t))
+ } else if let t = try? scanner.liquid_scanRegex(#""([^\"]*)""#, captureGroup: 1) {
+ tokens.append(Token(kind: .string, value: t))
+ } else if let t = scanner.liquid_scanDecimal() {
+ tokens.append(Token(kind: .decimal, value: t))
+ } else if let t = scanner.liquid_scanInt() {
+ tokens.append(Token(kind: .integer, value: t))
+ } else if let t = try? scanner.liquid_scanRegex(#"[a-zA-Z_][\w-]*\??"#) {
+ tokens.append(Token(kind: .id, value: t))
+ } else if let _ = try? scanner.liquid_scanRegex(#"\.\."#) {
+ tokens.append(Token(kind: .dotDot))
+ } else if let c = scanner.liquid_scanCharacter() {
+ if let tokenKind = specials[c] {
+ tokens.append(Token(kind: tokenKind))
+ } else {
+ throw SyntaxError.unexpectedToken(c)
+ }
+ }
+ }
+
+ tokens.append(Token(kind: .endOfString))
+ return tokens
+ }
+}
diff --git a/Sources/Liquid/Node.swift b/Sources/Liquid/Node.swift
new file mode 100644
index 0000000..80af112
--- /dev/null
+++ b/Sources/Liquid/Node.swift
@@ -0,0 +1,35 @@
+//
+// File.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-02.
+//
+
+import Foundation
+
+public protocol Node {
+ func render(context: Context) throws -> [String]
+}
+
+struct VariableNode: Node {
+ private let variable: Variable
+
+ init(_ variable: Variable) {
+ self.variable = variable
+ }
+
+ func render(context: Context) throws -> [String] {
+ return [try variable.evaluate(context: context).liquidString(encoder: context.encoder)]
+ }
+}
+
+struct StringNode: Node {
+ private let rawValue: String
+ init(_ rawValue: String) {
+ self.rawValue = rawValue
+ }
+
+ func render(context: Context) throws -> [String] {
+ return [rawValue]
+ }
+}
diff --git a/Sources/Liquid/Parser.swift b/Sources/Liquid/Parser.swift
new file mode 100644
index 0000000..2ed4108
--- /dev/null
+++ b/Sources/Liquid/Parser.swift
@@ -0,0 +1,65 @@
+//
+// Parser.swift
+//
+//
+// Created by Geoffrey Foster on 2019-08-29.
+//
+
+import Foundation
+
+final class Parser {
+ private let tokens: [Lexer.Token]
+ private var index: Array.Index
+ init(tokens: [Lexer.Token]) {
+ self.tokens = tokens
+ self.index = tokens.startIndex
+ }
+
+ convenience init(string: String) throws {
+ self.init(tokens: try Lexer.tokenize(string))
+ }
+
+ func look(_ tokenKind: Lexer.Token.Kind) -> Bool {
+ guard index != tokens.endIndex else { return false }
+ return tokens[index].kind == tokenKind
+ }
+
+ func consumeId(_ id: String) -> Bool {
+ guard index != tokens.endIndex else { return false }
+ guard tokens[index].kind == .id else { return false }
+ guard tokens[index].value == id else { return false }
+ defer { index = tokens.index(after: index) }
+ return true
+ }
+
+ func consume() -> String {
+ defer {
+ index = tokens.index(after: index)
+ }
+ return tokens[index].value ?? ""
+ }
+
+ @discardableResult
+ func consume(_ tokenKind: Lexer.Token.Kind) -> String? {
+ guard index != tokens.endIndex else { return nil }
+ guard tokens[index].kind == tokenKind else { return nil }
+ defer {
+ index = tokens.index(after: index)
+ }
+ return tokens[index].value ?? ""
+ }
+
+ func unsafeConsume(_ tokenKind: Lexer.Token.Kind) throws -> String {
+ guard tokens[index].kind == tokenKind else {
+ throw SyntaxError.reason("Expected \(tokenKind) but found \(tokens[index].kind)")
+ }
+ defer {
+ index = tokens.index(after: index)
+ }
+ return tokens[index].value ?? ""
+ }
+}
+
+public class ParseContext {
+ var tags: [String: TagBuilder] = [:]
+}
diff --git a/Sources/Liquid/RawTag.swift b/Sources/Liquid/RawTag.swift
new file mode 100644
index 0000000..72ca8f8
--- /dev/null
+++ b/Sources/Liquid/RawTag.swift
@@ -0,0 +1,24 @@
+//
+// RawTag.swift
+//
+//
+// Created by Geoffrey Foster on 2019-08-30.
+//
+
+import Foundation
+
+public struct RawTag: Equatable {
+ public let name: String
+ public let markup: String?
+
+ init(_ string: String) {
+ let split = string.split(separator: " ", maxSplits: 1)
+ if split.count == 2 {
+ name = String(split.first!)
+ markup = String(split.last!)
+ } else {
+ name = string
+ markup = nil
+ }
+ }
+}
diff --git a/Sources/Liquid/SyntaxError.swift b/Sources/Liquid/SyntaxError.swift
new file mode 100644
index 0000000..1a04df2
--- /dev/null
+++ b/Sources/Liquid/SyntaxError.swift
@@ -0,0 +1,30 @@
+//
+// SyntaxError.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-02.
+//
+
+import Foundation
+
+public enum SyntaxError: Error {
+ case unknownTag(String)
+ case unclosedTag(String)
+ case missingMarkup
+ case unexpectedToken(Character)
+ case reason(String) // TODO: this is just a raw string explaining the failure
+}
+
+public enum RuntimeError: Error {
+ case wrongType(String)
+ case unknownFilter(String)
+
+ case invalidArgCount(expected: Int, received: Int, tag: String)
+
+ case unimplemented
+}
+
+func tagName(function: String = #function) -> String {
+ guard let index = function.firstIndex(of: "(") else { return function }
+ return String(function[.. Tag
+
+func defaultUnknownTagHandler(tag: String?, markup: String?) throws {
+ if let tag = tag {
+ throw SyntaxError.reason("Unknown tag \(tag)")
+ }
+}
diff --git a/Sources/Liquid/Tags/Assign.swift b/Sources/Liquid/Tags/Assign.swift
new file mode 100644
index 0000000..306e515
--- /dev/null
+++ b/Sources/Liquid/Tags/Assign.swift
@@ -0,0 +1,28 @@
+//
+// Assign.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-06.
+//
+
+import Foundation
+
+struct Assign: Tag {
+ private let variableName: String
+ private let variable: Variable
+
+ init(name: String, markup: String?, context: ParseContext) throws {
+ guard let markup = markup else { throw SyntaxError.missingMarkup }
+ let parser = try Parser(string: markup)
+ self.variableName = try parser.unsafeConsume(.id)
+ parser.consume(.equal)
+ self.variable = Variable(parser: parser)
+ }
+
+ func parse(_ tokenizer: Tokenizer, context: ParseContext) throws {}
+
+ func render(context: Context) throws -> [String] {
+ context.setValue(try variable.evaluate(context: context), named: variableName)
+ return []
+ }
+}
diff --git a/Sources/Liquid/Tags/Break.swift b/Sources/Liquid/Tags/Break.swift
new file mode 100644
index 0000000..f7131da
--- /dev/null
+++ b/Sources/Liquid/Tags/Break.swift
@@ -0,0 +1,18 @@
+//
+// Break.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-04.
+//
+
+import Foundation
+
+struct Break: Tag {
+ init(name: String, markup: String?, context: ParseContext) throws {}
+ func parse(_ tokenizer: Tokenizer, context: ParseContext) throws {}
+
+ func render(context: Context) -> [String] {
+ context.push(interrupt: .break)
+ return []
+ }
+}
diff --git a/Sources/Liquid/Tags/Capture.swift b/Sources/Liquid/Tags/Capture.swift
new file mode 100644
index 0000000..ce1d5fa
--- /dev/null
+++ b/Sources/Liquid/Tags/Capture.swift
@@ -0,0 +1,31 @@
+//
+// Capture.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-06.
+//
+
+import Foundation
+
+class Capture: Block, Tag {
+ private let variableName: String
+ private let body: BlockBody = BlockBody()
+
+ init(name: String, markup: String?, context: ParseContext) throws {
+ guard let markup = markup else { throw SyntaxError.missingMarkup }
+ let parser = try Parser(string: markup)
+ self.variableName = try parser.unsafeConsume(.id)
+ parser.consume(.endOfString)
+ super.init(name: name)
+ }
+
+ func parse(_ tokenizer: Tokenizer, context: ParseContext) throws {
+ _ = try super.parse(body: body, tokenizer: tokenizer, context: context)
+ }
+
+ func render(context: Context) throws -> [String] {
+ let result = try body.render(context: context)
+ context.setValue(Value(result.joined()), named: variableName)
+ return []
+ }
+}
diff --git a/Sources/Liquid/Tags/Case.swift b/Sources/Liquid/Tags/Case.swift
new file mode 100644
index 0000000..e71c1a7
--- /dev/null
+++ b/Sources/Liquid/Tags/Case.swift
@@ -0,0 +1,92 @@
+//
+// Case.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-02.
+//
+
+import Foundation
+
+class Case: Block, Tag {
+ private struct CaseCondition {
+ let expressions: [Expression]
+ let body: BlockBody
+ let isElse: Bool
+ }
+ private let expression: Expression
+ private var conditions: [CaseCondition] = []
+
+ init(name: String, markup: String?, context: ParseContext) throws {
+ guard let markup = markup else {
+ throw SyntaxError.missingMarkup
+ }
+ let parser = try Parser(string: markup)
+ self.expression = Expression.parse(parser)
+ parser.consume(.endOfString)
+ super.init(name: name)
+ }
+
+ func parse(_ tokenizer: Tokenizer, context: ParseContext) throws {
+ var body = BlockBody()
+ while try parse(body: body, tokenizer: tokenizer, context: context) {
+ body = conditions.last!.body
+ }
+ }
+
+ override func handleUnknown(tag: String, markup: String?) throws {
+ if tag == "when" {
+ try recordWhenCondition(markup)
+ } else if tag == "else" {
+ recordElseCondition(markup)
+ } else {
+ try super.handleUnknown(tag: tag, markup: markup)
+ }
+ }
+
+ private func recordWhenCondition(_ condition: String?) throws {
+ guard let condition = condition else {
+ throw SyntaxError.missingMarkup
+ }
+ let parser = try Parser(string: condition)
+ var expressions: [Expression] = [Expression.parse(parser)]
+ while let id = parser.consume(.id) {
+ if id != "or" {
+ throw SyntaxError.reason("Expected \"or\" but found \(id)")
+ }
+ expressions.append(Expression.parse(parser))
+ }
+ while parser.consume(.comma) != nil {
+ expressions.append(Expression.parse(parser))
+ }
+ parser.consume(.endOfString)
+ conditions.append(CaseCondition(expressions: expressions, body: BlockBody(), isElse: false))
+ }
+
+ private func recordElseCondition(_ condition: String?) {
+ conditions.append(CaseCondition(expressions: [], body: BlockBody(), isElse: true))
+ }
+
+ func render(context: Context) throws -> [String] {
+ var output: [String] = []
+ let expressionValue = expression.evaluate(context: context)
+ try context.withScope {
+ var executeElse = true
+ for condition in conditions {
+ if condition.isElse {
+ if executeElse {
+ output = try condition.body.render(context: context)
+ return
+ }
+ } else {
+ for expression in condition.expressions {
+ if expression.evaluate(context: context) == expressionValue {
+ executeElse = false
+ output.append(contentsOf: try condition.body.render(context: context))
+ }
+ }
+ }
+ }
+ }
+ return output
+ }
+}
diff --git a/Sources/Liquid/Tags/Comment.swift b/Sources/Liquid/Tags/Comment.swift
new file mode 100644
index 0000000..415ee20
--- /dev/null
+++ b/Sources/Liquid/Tags/Comment.swift
@@ -0,0 +1,22 @@
+//
+// Comment.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-09.
+//
+
+import Foundation
+
+struct Comment: Tag {
+ init(name: String, markup: String?, context: ParseContext) throws {
+ fatalError()
+ }
+
+ func parse(_ tokenizer: Tokenizer, context: ParseContext) throws {
+ fatalError()
+ }
+
+ func render(context: Context) -> [String] {
+ fatalError()
+ }
+}
diff --git a/Sources/Liquid/Tags/Continue.swift b/Sources/Liquid/Tags/Continue.swift
new file mode 100644
index 0000000..f1a9ef4
--- /dev/null
+++ b/Sources/Liquid/Tags/Continue.swift
@@ -0,0 +1,18 @@
+//
+// Continue.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-04.
+//
+
+import Foundation
+
+struct Continue: Tag {
+ init(name: String, markup: String?, context: ParseContext) throws {}
+ func parse(_ tokenizer: Tokenizer, context: ParseContext) throws {}
+
+ func render(context: Context) -> [String] {
+ context.push(interrupt: .continue)
+ return []
+ }
+}
diff --git a/Sources/Liquid/Tags/Cycle.swift b/Sources/Liquid/Tags/Cycle.swift
new file mode 100644
index 0000000..02b0aad
--- /dev/null
+++ b/Sources/Liquid/Tags/Cycle.swift
@@ -0,0 +1,53 @@
+//
+// Cycle.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-06.
+//
+
+import Foundation
+
+struct Cycle: Tag {
+ private let name: String
+ private let nameExpression: Expression?
+ private let expressions: [Expression]
+ init(name: String, markup: String?, context: ParseContext) throws {
+ self.name = name
+ guard let markup = markup else { throw SyntaxError.missingMarkup }
+
+ var expressions: [Expression] = []
+ let parser = try Parser(string: markup)
+ let firstExpression = Expression.parse(parser)
+
+ if let _ = parser.consume(.colon) {
+ self.nameExpression = firstExpression
+ expressions.append(Expression.parse(parser))
+ } else {
+ self.nameExpression = nil
+ expressions.append(firstExpression)
+ }
+
+ while parser.consume(.comma) != nil {
+ expressions.append(Expression.parse(parser))
+ }
+
+ parser.consume(.endOfString)
+
+ self.expressions = expressions
+ }
+
+ func parse(_ tokenizer: Tokenizer, context: ParseContext) throws {}
+
+ func render(context: Context) -> [String] {
+ let registerKey = RegisterKey(name)
+ let lookupKey = nameExpression?.evaluate(context: context).toString() ?? expressions.map { $0.description }.joined(separator: ", ")
+ var registration = context[registerKey]?.toDictionary() ?? [:]
+ let iteration = registration[lookupKey]?.toInteger() ?? 0
+ let result = expressions[iteration].evaluate(context: context).toString()
+
+ registration[lookupKey] = Value((iteration + 1) % expressions.count)
+ context[registerKey] = Value(registration)
+
+ return [result]
+ }
+}
diff --git a/Sources/Liquid/Tags/Decrement.swift b/Sources/Liquid/Tags/Decrement.swift
new file mode 100644
index 0000000..b7d44c1
--- /dev/null
+++ b/Sources/Liquid/Tags/Decrement.swift
@@ -0,0 +1,27 @@
+//
+// Decrement.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-06.
+//
+
+import Foundation
+
+struct Decrement: Tag {
+ private let variableName: String
+
+ init(name: String, markup: String?, context: ParseContext) throws {
+ guard let markup = markup else { throw SyntaxError.missingMarkup }
+ let parser = try Parser(string: markup)
+ self.variableName = try parser.unsafeConsume(.id)
+ _ = parser.consume(.endOfString)
+ }
+
+ func parse(_ tokenizer: Tokenizer, context: ParseContext) throws {}
+
+ func render(context: Context) throws -> [String] {
+ let value = (context[EnvironmentKey(variableName)]?.toInteger() ?? 0) - 1
+ context[EnvironmentKey(variableName)] = Value(value)
+ return ["\(value)"]
+ }
+}
diff --git a/Sources/Liquid/Tags/For.swift b/Sources/Liquid/Tags/For.swift
new file mode 100644
index 0000000..e83bdad
--- /dev/null
+++ b/Sources/Liquid/Tags/For.swift
@@ -0,0 +1,269 @@
+//
+// For.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-02.
+//
+
+import Foundation
+
+class For: Block, Tag {
+ private var forBlock = BlockBody()
+ private var elseBlock: BlockBody?
+
+ private let variableName: String
+ private let reversed: Bool
+ private var offset: Expression?
+ private var limit: Expression?
+
+ private enum CollectionType {
+ case range(start: Expression, end: Expression)
+ case collection(Expression)
+ }
+
+ private var collection: CollectionType
+
+ public init(name: String, markup: String?, context: ParseContext) throws {
+ guard let markup = markup else { throw SyntaxError.missingMarkup }
+
+ let parser = try Parser(string: markup)
+ guard let variableName = parser.consume(.id) else {
+ throw SyntaxError.reason("Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]")
+ }
+ self.variableName = variableName
+ guard parser.consume(.id) == "in" else {
+ throw SyntaxError.reason("Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]")
+ }
+
+ if parser.consume(.openRound) != nil {
+ let rangeStart = Expression.parse(parser)
+ parser.consume(.dotDot)
+ let rangeEnd = Expression.parse(parser)
+ parser.consume(.closeRound)
+ self.collection = .range(start: rangeStart, end: rangeEnd)
+ } else {
+ self.collection = .collection(Expression.parse(parser))
+ }
+
+ self.reversed = parser.consumeId("reversed")
+
+ while let attribute = parser.consume(.id) {
+ parser.consume(.colon)
+
+ let value = Expression.parse(parser)
+ if attribute == "offset" {
+ self.offset = value
+ } else if attribute == "limit" {
+ self.limit = value
+ } else {
+ throw SyntaxError.reason("Invalid attribute in for loop. Valid attributes are limit and offset")
+ }
+ }
+
+ parser.consume(.endOfString)
+
+ super.init(name: name)
+ }
+
+ func parse(_ tokenizer: Tokenizer, context: ParseContext) throws {
+ guard try parse(body: forBlock, tokenizer: tokenizer, context: context) else { return }
+ if let elseBlock = elseBlock {
+ _ = try parse(body: elseBlock, tokenizer: tokenizer, context: context)
+ }
+ }
+
+ override func handleUnknown(tag: String, markup: String?) throws {
+ if tag == "else" {
+ elseBlock = BlockBody()
+ } else {
+ try super.handleUnknown(tag: tag, markup: markup)
+ }
+ }
+
+ func render(context: Context) throws -> [String] {
+ let item: (Int) -> Value
+ var startIndex: Int
+ var endIndex: Int
+
+ switch collection {
+ case .range(let start, let end):
+ startIndex = start.evaluate(context: context).toInteger()
+ endIndex = end.evaluate(context: context).toInteger() + 1
+ item = { Value($0) }
+ case .collection(let collection):
+ let array = collection.evaluate(context: context).toArray()
+ startIndex = array.startIndex
+ endIndex = array.endIndex
+ item = { array[$0] }
+ }
+
+ let offsetValue = offset?.evaluate(context: context).toInteger()
+ let limitValue = limit?.evaluate(context: context).toInteger()
+
+ let parent = context[RegisterKey(name)]?.toArray().last?.toDrop()
+ let loop = ForLoop(item: item, body: forBlock, variableName: variableName, startIndex: startIndex, endIndex: endIndex, reversed: reversed, offset: offsetValue, limit: limitValue, parent: parent)
+ context[RegisterKey(name), default: Value([])].push(value: Value(loop.drop))
+ defer {
+ context[RegisterKey(name)]?.pop()
+ }
+ if loop.isEmpty {
+ return try elseBlock?.render(context: context) ?? []
+ }
+
+ return try loop.render(context: context)
+ }
+}
+
+private struct ForLoop {
+ private let item: (Int) -> Value
+ private let body: BlockBody
+ private let variableName: String
+ private let range: Range
+ private let reversed: Bool
+ let drop: ForDrop
+
+ init(item: @escaping (Int) -> Value, body: BlockBody, variableName: String, startIndex: Int, endIndex: Int, reversed: Bool, offset: Int?, limit: Int?, parent: Drop?) {
+ self.item = item
+ self.body = body
+ self.variableName = variableName
+ self.reversed = reversed
+
+ func makeRange(start: Int, end: Int, offset: Int?, limit: Int?, reversed: Bool) -> Range {
+ var r = start.. AnyIterator {
+ var helper = ForHelper(range: range, reversed: reversed)
+
+ var iterator = AnyIterator {
+ defer { helper.advance() }
+ return helper.test ? helper.index : nil
+ }
+ return iterator
+ }
+
+ var isEmpty: Bool {
+ return range.isEmpty
+ }
+
+ func render(context: Context) throws -> [String] {
+ var output: [String] = []
+
+ try context.withScope(Scope(mutable: false, values: ["forloop": Value(drop)])) {
+ outerLoop: for index in makeIterator() {
+ output.append(contentsOf: try renderItemAtIndex(index, context: context))
+ drop.increment()
+
+ if context.hasInterrupt {
+ switch context.popInterrupt() {
+ case .break:
+ break outerLoop
+ case .continue:
+ continue outerLoop
+ }
+ }
+ }
+ }
+ return output
+ }
+
+ private func renderItemAtIndex(_ index: Int, context: Context) throws -> [String] {
+ let value = item(index)
+ let scope = Scope(mutable: false, values: [variableName: value])
+ let results = try context.withScope(scope) {
+ try body.render(context: context)
+ }
+ return results
+ }
+}
+
+private struct ForHelper {
+ private let range: Range
+ private let reversed: Bool
+
+ private(set) var index: Int
+
+ init(range: Range, reversed: Bool) {
+ self.range = range
+ self.reversed = reversed
+ self.index = reversed ? range.endIndex - 1 : range.startIndex
+ }
+
+ var test: Bool {
+ return range.contains(index)
+ }
+
+ mutating func advance() {
+ if reversed {
+ index -= 1
+ } else {
+ index += 1
+ }
+ }
+}
+
+private class ForDrop: Drop {
+ private var currentIndex: Int = 0
+ @objc let length: Int
+ let parent: Drop?
+
+ init(length: Int, parent: Drop?) {
+ self.length = length
+ self.parent = parent
+ }
+
+ var first: Bool {
+ return currentIndex == 0
+ }
+
+ var last: Bool {
+ return currentIndex == length - 1
+ }
+
+ var index: Int {
+ return currentIndex + 1
+ }
+
+ var index0: Int {
+ return currentIndex
+ }
+
+ var rindex: Int {
+ return length - currentIndex
+ }
+
+ var rindex0: Int {
+ return length - currentIndex - 1
+ }
+
+ func value(forKey key: DropKey, encoder: Encoder) throws -> Value? {
+ switch key.rawValue {
+ case "length": return Value(length)
+ case "first": return Value(first)
+ case "last": return Value(last)
+ case "index": return Value(index)
+ case "index0": return Value(index0)
+ case "rindex": return Value(rindex)
+ case "rindex0": return Value(rindex0)
+ case "parentloop":
+ guard let parent = parent else { return Value() }
+ return Value(parent)
+ default:
+ return nil
+ }
+ }
+
+ func increment() {
+ currentIndex += 1
+ }
+}
diff --git a/Sources/Liquid/Tags/If.swift b/Sources/Liquid/Tags/If.swift
new file mode 100644
index 0000000..b2e1f89
--- /dev/null
+++ b/Sources/Liquid/Tags/If.swift
@@ -0,0 +1,210 @@
+//
+// If.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-02.
+//
+
+import Foundation
+
+final class If: Block, Tag {
+ private class Condition {
+ enum Operator {
+ case equal
+ case notEqual
+ case lessThan
+ case greaterThan
+ case lessThanEqual
+ case greaterThanEqual
+ case contains
+
+ func evaluate(lhs: Value, rhs: Value) -> Bool {
+ switch self {
+ case .equal:
+ return lhs == rhs
+ case .notEqual:
+ return lhs != rhs
+ case .lessThan:
+ return lhs < rhs
+ case .greaterThan:
+ return lhs > rhs
+ case .lessThanEqual:
+ return lhs <= rhs
+ case .greaterThanEqual:
+ return lhs >= rhs
+ case .contains:
+ if lhs.isArray {
+ return lhs.toArray().contains(rhs)
+ } else if lhs.isDictionary {
+ return lhs.toDictionary()[rhs.toString()] != nil
+ } else if lhs.isString {
+ return lhs.toString().contains(rhs.toString())
+ } else {
+ return false
+ }
+ }
+ }
+ }
+
+ enum LogicalOperator {
+ case and
+ case or
+ }
+
+ private let lhs: Expression
+ private let `operator`: Operator?
+ private let rhs: Expression?
+ var logicalCondition: (op: LogicalOperator, condition: Condition)?
+
+ init(lhs: Expression, operator: Operator, rhs: Expression) {
+ self.lhs = lhs
+ self.operator = `operator`
+ self.rhs = rhs
+ }
+
+ init(expression: Expression) {
+ self.lhs = expression
+ self.operator = nil
+ self.rhs = nil
+ }
+
+ func evaluate(context: Context) -> Bool {
+ var result: Bool
+ if let `operator` = `operator`, let rhs = rhs {
+ let lhsValue = lhs.evaluate(context: context)
+ let rhsValue = rhs.evaluate(context: context)
+ result = `operator`.evaluate(lhs: lhsValue, rhs: rhsValue)
+ } else {
+ result = lhs.evaluate(context: context).isTruthy
+ }
+
+ if let logicalCondition = logicalCondition {
+ switch logicalCondition.op {
+ case .and:
+ return result && logicalCondition.condition.evaluate(context: context)
+ case .or:
+ return result || logicalCondition.condition.evaluate(context: context)
+ }
+ }
+ return result
+ }
+ }
+
+ private struct IfBlock {
+ var condition: Condition?
+ let body: BlockBody = BlockBody()
+
+ init(condition: Condition? = nil) {
+ self.condition = condition
+ }
+
+ func evaluate(context: Context) -> Bool {
+ guard let condition = condition else { return true }
+ return condition.evaluate(context: context)
+ }
+ }
+
+ private var blocks: [IfBlock] = []
+ private let inverted: Bool
+
+ convenience init(name: String, markup: String?, context: ParseContext) throws {
+ try self.init(name: name, markup: markup, context: context, inverted: false)
+ }
+
+ private init(name: String, markup: String?, context: ParseContext, inverted: Bool) throws {
+ self.inverted = inverted
+ super.init(name: name)
+ try pushBlock(markup: markup)
+ }
+
+ static func unless(name: String, markup: String?, context: ParseContext) throws -> If {
+ return try If(name: name, markup: markup, context: context, inverted: true)
+ }
+
+ func parse(_ tokenizer: Tokenizer, context: ParseContext) throws {
+ while try parse(body: blocks.last!.body, tokenizer: tokenizer, context: context) {}
+ }
+
+ override func handleUnknown(tag: String, markup: String?) throws {
+ if tag == "elsif" {
+ try pushBlock(markup: markup)
+ } else if tag == "else" {
+ blocks.append(IfBlock())
+ } else {
+ try super.handleUnknown(tag: tag, markup: markup)
+ }
+ }
+
+ private func pushBlock(markup: String?) throws {
+ guard let markup = markup else {
+ throw SyntaxError.missingMarkup
+ }
+ let parser = try Parser(string: markup)
+ let condition = try parseLogicalCondition(parser)
+ let block = IfBlock(condition: condition)
+ blocks.append(block)
+ }
+
+ func render(context: Context) throws -> [String] {
+ if let block = blocks.first {
+ let result = block.evaluate(context: context)
+ if result && !inverted || !result && inverted {
+ return try block.body.render(context: context)
+ }
+ }
+ for block in blocks.dropFirst() {
+ if block.evaluate(context: context) {
+ return try block.body.render(context: context)
+ }
+ }
+ return []
+ }
+
+ private func parseLogicalCondition(_ parser: Parser) throws -> Condition {
+ let condition = try parseCondition(parser)
+
+ var logicalOperator: Condition.LogicalOperator? = nil
+ if parser.consumeId("and") {
+ logicalOperator = .and
+ } else if parser.consumeId("or") {
+ logicalOperator = .or
+ }
+
+ if let logicalOperator = logicalOperator {
+ condition.logicalCondition = (logicalOperator, try parseLogicalCondition(parser))
+ }
+
+ return condition
+ }
+
+ private func parseCondition(_ parser: Parser) throws -> Condition {
+ let lhs = Expression.parse(parser)
+ if parser.look(.comparison) {
+ let opString = parser.consume()
+ let op: Condition.Operator
+ switch opString {
+ case "==":
+ op = .equal
+ case "!=", "<>":
+ op = .notEqual
+ case "<":
+ op = .lessThan
+ case ">":
+ op = .greaterThan
+ case "<=":
+ op = .lessThanEqual
+ case ">=":
+ op = .greaterThanEqual
+ case "contains":
+ op = .contains
+ default:
+ throw SyntaxError.reason("Unknown operator \(opString)")
+ }
+
+ let rhs = Expression.parse(parser)
+
+ return Condition(lhs: lhs, operator: op, rhs: rhs)
+ }
+ return Condition(expression: lhs)
+ }
+}
diff --git a/Sources/Liquid/Tags/Increment.swift b/Sources/Liquid/Tags/Increment.swift
new file mode 100644
index 0000000..d027e2c
--- /dev/null
+++ b/Sources/Liquid/Tags/Increment.swift
@@ -0,0 +1,27 @@
+//
+// Increment.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-06.
+//
+
+import Foundation
+
+struct Increment: Tag {
+ private let variableName: String
+
+ init(name: String, markup: String?, context: ParseContext) throws {
+ guard let markup = markup else { throw SyntaxError.missingMarkup }
+ let parser = try Parser(string: markup)
+ self.variableName = try parser.unsafeConsume(.id)
+ _ = parser.consume(.endOfString)
+ }
+
+ func parse(_ tokenizer: Tokenizer, context: ParseContext) throws {}
+
+ func render(context: Context) throws -> [String] {
+ let value = (context[EnvironmentKey(variableName)]?.toInteger() ?? 0)
+ context[EnvironmentKey(variableName)] = Value(value + 1)
+ return ["\(value)"]
+ }
+}
diff --git a/Sources/Liquid/Template.swift b/Sources/Liquid/Template.swift
new file mode 100644
index 0000000..6594076
--- /dev/null
+++ b/Sources/Liquid/Template.swift
@@ -0,0 +1,79 @@
+//
+// File.swift
+//
+//
+// Created by Geoffrey Foster on 2019-08-31.
+//
+
+import Foundation
+
+public final class Template {
+ let source: String
+
+ private let root = BlockBody()
+ private var filters: [String: FilterFunc] = [:]
+ private var tags: [String: TagBuilder] = [:]
+
+ private let locale: Locale
+
+ var encoder: Encoder = Encoder()
+
+ /// Environments are persisted across each render of a template so we store them as part of the template itself.
+ /// The template then injects them during a render call and copies the values back upon completion.
+ private var environment: [String: Value] = [:]
+
+ public init(source: String, locale: Locale = .current) {
+ self.source = source
+ self.locale = locale
+
+ encoder.locale = locale
+
+ tags["assign"] = Assign.init
+ tags["break"] = Break.init
+ tags["capture"] = Capture.init
+ tags["case"] = Case.init
+ tags["comment"] = Comment.init
+ tags["continue"] = Continue.init
+ tags["cycle"] = Cycle.init
+ tags["decrement"] = Decrement.init
+ tags["for"] = For.init
+ tags["if"] = If.init
+ tags["increment"] = Increment.init
+ tags["unless"] = If.unless
+
+ Filters.registerFilters(template: self)
+ }
+
+ public func parse() throws {
+ let parseContext = ParseContext()
+ parseContext.tags = tags
+ try root.parse(Tokenizer(source: source), context: parseContext, step: defaultUnknownTagHandler)
+ }
+
+ public func registerFilter(name: String, filter: @escaping FilterFunc) {
+ filters[name] = filter
+ }
+
+ public func registerTag(name: String, tag: @escaping TagBuilder) {
+ tags[name] = tag
+ }
+
+ public func render(values: [String: Value] = [:]) throws -> String {
+ let context = Context(values: values, environment: environment, filters: filters, encoder: encoder)
+ defer {
+ // Merge the contexts environment back into the templates
+ environment.merge(context.environment) { (lhs, rhs) -> Value in
+ return rhs
+ }
+ }
+ return try root.render(context: context).joined()
+ }
+
+ public func render(values: [String: ValueConvertible]) throws -> String {
+ return try self.render(values: values.mapValues { try encoder.encode($0) })
+ }
+
+ public func render(values: [String: Any?]) throws -> String {
+ return try self.render(values: values.mapValues { try encoder.encode($0) })
+ }
+}
diff --git a/Sources/Liquid/Token.swift b/Sources/Liquid/Token.swift
new file mode 100644
index 0000000..f16cd8c
--- /dev/null
+++ b/Sources/Liquid/Token.swift
@@ -0,0 +1,30 @@
+//
+// Token.swift
+//
+//
+// Created by Geoffrey Foster on 2019-08-30.
+//
+
+import Foundation
+
+public enum Token: Equatable {
+ /// A token representing a piece of text.
+ case text(value: String)
+
+ /// A token representing a variable.
+ case variable(value: String)
+
+ /// A token representing a template tag.
+ case tag(value: RawTag)
+
+ public static func ==(lhs: Token, rhs: Token) -> Bool {
+ switch (lhs, rhs) {
+ case let (.text(lhsValue), .text(rhsValue)): return lhsValue == rhsValue
+ case let (.variable(lhsValue), .variable(rhsValue)): return lhsValue == rhsValue
+ case let (.tag(lhsValue), .tag(rhsValue)): return lhsValue == rhsValue
+
+ default:
+ return false
+ }
+ }
+}
diff --git a/Sources/Liquid/Tokenizer.swift b/Sources/Liquid/Tokenizer.swift
new file mode 100644
index 0000000..3abf8a4
--- /dev/null
+++ b/Sources/Liquid/Tokenizer.swift
@@ -0,0 +1,111 @@
+//
+// Lexer.swift
+//
+//
+// Created by Geoffrey Foster on 2019-08-28.
+//
+
+import Foundation
+
+public final class Tokenizer {
+ public enum Token: Equatable {
+ /// A token representing a piece of text.
+ case text(value: String)
+
+ /// A token representing a variable.
+ case variable(value: String)
+
+ /// A token representing a template tag.
+ case tag(value: RawTag)
+
+ public static func ==(lhs: Token, rhs: Token) -> Bool {
+ switch (lhs, rhs) {
+ case let (.text(lhsValue), .text(rhsValue)): return lhsValue == rhsValue
+ case let (.variable(lhsValue), .variable(rhsValue)): return lhsValue == rhsValue
+ case let (.tag(lhsValue), .tag(rhsValue)): return lhsValue == rhsValue
+
+ default:
+ return false
+ }
+ }
+ }
+
+ internal let tokens: [Token]
+ private var position: Array.Index
+
+ public init(source: String) {
+ self.tokens = tokenize(source)
+ self.position = tokens.startIndex
+ }
+
+ public func next() -> Token? {
+ guard position != tokens.endIndex else { return nil }
+ defer { position = tokens.index(after: position) }
+ return tokens[position]
+ }
+}
+
+private func tokenize(_ input: String) -> [Tokenizer.Token] {
+ var scalars = Substring(input)
+ var tokens: [Tokenizer.Token] = []
+ while let token = scalars.readToken() {
+ tokens.append(token)
+ }
+ if !scalars.isEmpty {
+ // TODO: throw an exception?
+ }
+ return tokens
+}
+
+private extension Substring {
+ mutating func readToken() -> Tokenizer.Token? {
+ guard !isEmpty else { return nil }
+ return readText() ?? readVariable() ?? readTag()
+ }
+
+ mutating func readVariable() -> Tokenizer.Token? {
+ let start = self
+ guard scan(until: ["{{"], consume: true) == "{{",
+ let variable = scan(until: ["}}"])?.trimmingCharacters(in: .whitespacesAndNewlines),
+ !variable.isEmpty,
+ scan(until: ["}}"], consume: true) == "}}"
+ else {
+ self = start
+ return nil
+ }
+ return .variable(value: variable)
+ }
+
+ mutating func readTag() -> Tokenizer.Token? {
+ let start = self
+ guard scan(until: ["{%"], consume: true) == "{%",
+ let tag = scan(until: ["%}"])?.trimmingCharacters(in: .whitespacesAndNewlines),
+ !tag.isEmpty,
+ scan(until: ["%}"], consume: true) == "%}"
+ else {
+ self = start
+ return nil
+ }
+ return .tag(value: RawTag(tag))
+ }
+
+ mutating func readText() -> Tokenizer.Token? {
+ guard let string = scan(until: ["{{", "{%"]) else { return nil }
+ return .text(value: string)
+ }
+
+ mutating func scan(until: [String], consume: Bool = false) -> String? {
+ var string = ""
+ while !isEmpty {
+ if let i = until.first(where: { hasPrefix($0) }) {
+ if consume {
+ removeFirst(i.count)
+ string += i
+ }
+ break
+ }
+ string += String(removeFirst())
+ }
+ return string.isEmpty ? nil : string
+ }
+}
diff --git a/Sources/Liquid/Value/Drop.swift b/Sources/Liquid/Value/Drop.swift
new file mode 100644
index 0000000..dd6ad50
--- /dev/null
+++ b/Sources/Liquid/Value/Drop.swift
@@ -0,0 +1,44 @@
+//
+// Drop.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-16.
+//
+
+import Foundation
+
+public struct DropKey: Hashable {
+ public let rawValue: String
+
+ public init(rawValue: String) {
+ self.rawValue = rawValue
+ }
+}
+
+public protocol Drop: class, ValueConvertible {
+ func value(forKey key: DropKey, encoder: Encoder) throws -> Value?
+}
+
+#if false
+// Disabled until I figure out best way to map NSObject, specifically NSNumber to proper types in the Encoder
+// Right now, a bool NSNumber ends up being an Int, which doesn't work for a comparison
+public func fetchValue(forKey key: DropKey, from object: NSObject, encoder: Encoder) throws -> Value? {
+ guard let value = object.value(forKey: key.rawValue) else { return nil }
+
+ return try encoder.encode(value)
+}
+#endif
+
+#if false
+// Disabled until figure out the best approach for Mirror
+public func value(forKey key: DropKey, from object: Drop, encoder: Encoder) throws -> Value? {
+ let mirror = Mirror(reflecting: object)
+ let child = mirror.children.first { (label, value) -> Bool in
+ return label == key.rawValue
+ }
+
+ child?.value
+
+ return nil
+}
+#endif
diff --git a/Sources/Liquid/Value/Encoder.swift b/Sources/Liquid/Value/Encoder.swift
new file mode 100644
index 0000000..6e01e1a
--- /dev/null
+++ b/Sources/Liquid/Value/Encoder.swift
@@ -0,0 +1,164 @@
+//
+// Encoder.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-18.
+//
+
+import Foundation
+
+public struct Encoder {
+ public enum EncodingError: Error {
+ case unencodableType(String)
+ }
+
+ public enum DateEncodingStrategy {
+ case iso8601
+ case formatted(DateFormatter)
+
+ internal func date(from string: String) -> Date? {
+ switch self {
+ case .iso8601:
+ if #available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
+ return ISO8601DateFormatter().date(from: string)
+ } else {
+ let formatter = DateFormatter()
+ formatter.locale = Locale(identifier: "en_US_POSIX")
+ formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
+ return formatter.date(from: string)
+ }
+ case .formatted(let formatter):
+ return formatter.date(from: string)
+ }
+ }
+ }
+
+ public enum KeyEncodingStrategy {
+
+ /// Use the keys specified by each type. This is the default strategy.
+ case useDefaultKeys
+
+ /// Convert from "camelCaseKeys" to "snake_case_keys" before writing a key to JSON payload.
+ ///
+ /// Capital characters are determined by testing membership in `CharacterSet.uppercaseLetters` and `CharacterSet.lowercaseLetters` (Unicode General Categories Lu and Lt).
+ /// The conversion to lower case uses `Locale.system`, also known as the ICU "root" locale. This means the result is consistent regardless of the current user's locale and language preferences.
+ ///
+ /// Converting from camel case to snake case:
+ /// 1. Splits words at the boundary of lower-case to upper-case
+ /// 2. Inserts `_` between words
+ /// 3. Lowercases the entire string
+ /// 4. Preserves starting and ending `_`.
+ ///
+ /// For example, `oneTwoThree` becomes `one_two_three`. `_oneTwoThree_` becomes `_one_two_three_`.
+ ///
+ /// - Note: Using a key encoding strategy has a nominal performance cost, as each string key has to be converted.
+ case convertToSnakeCase
+
+ func transform(key: String) -> DropKey {
+ switch self {
+ case .useDefaultKeys:
+ return DropKey(rawValue: key)
+ case .convertToSnakeCase:
+ return DropKey(rawValue: _convertToSnakeCase(key))
+ }
+ }
+
+ private func _convertToSnakeCase(_ stringKey: String) -> String {
+ guard !stringKey.isEmpty else { return stringKey }
+
+ var words : [Range] = []
+ // The general idea of this algorithm is to split words on transition from lower to upper case, then on transition of >1 upper case characters to lowercase
+ //
+ // myProperty -> my_property
+ // myURLProperty -> my_url_property
+ //
+ // We assume, per Swift naming conventions, that the first character of the key is lowercase.
+ var wordStart = stringKey.startIndex
+ var searchRange = stringKey.index(after: wordStart)..1 capital letters. Turn those into a word, stopping at the capital before the lower case character.
+ let beforeLowerIndex = stringKey.index(before: lowerCaseRange.lowerBound)
+ words.append(upperCaseRange.lowerBound.. String {
+ switch self {
+ case .full:
+ return "\(value)"
+ case .scaled(let scale):
+ return "\(value.round(scale: scale))"
+ }
+ }
+ }
+
+ public var dateEncodingStrategry: DateEncodingStrategy = .iso8601
+ public var keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys
+ public var decimalEncodingStrategy: DecimalEncodingStrategy = .scaled(8)
+ public internal(set) var locale: Locale = .current
+
+ public func encode(_ input: Any?) throws -> Value {
+ guard let input = input else {
+ return Value()
+ }
+ switch input {
+ case let value as ValueConvertible:
+ return value.toValue(encoder: self)
+ case let value as Drop:
+ return Value(value)
+ case let value as String:
+ return Value(value)
+ case let value as Int:
+ return Value(value)
+ case let value as Int8:
+ return Value(Int(value))
+ case let value as Float:
+ return Value(Double(value))
+ case let value as Double:
+ return Value(value)
+ case let value as Bool:
+ return Value(value)
+ case let value as [Any?]:
+ return try Value(value.map { try encode($0) })
+ case let value as [String: Any?]:
+ return Value(try value.mapValues { try encode($0) })
+ default:
+ throw EncodingError.unencodableType(String(describing: type(of: input)))
+ }
+ }
+}
diff --git a/Sources/Liquid/Value/Value.swift b/Sources/Liquid/Value/Value.swift
new file mode 100644
index 0000000..82268f1
--- /dev/null
+++ b/Sources/Liquid/Value/Value.swift
@@ -0,0 +1,450 @@
+//
+// File.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-03.
+//
+
+import Foundation
+
+public final class Value: Equatable, Comparable {
+ fileprivate enum Storage {
+ case `nil`
+ case bool(Bool)
+ case string(String)
+ case int(Int)
+ case decimal(Decimal)
+ case array([Value])
+ case dictionary([String: Value])
+ case drop(Drop)
+ }
+
+ fileprivate var storage: Storage
+
+ init() {
+ self.storage = .nil
+ }
+
+ fileprivate init(storage: Storage) {
+ self.storage = storage
+ }
+
+ convenience init(_ value: Bool) {
+ self.init(storage: .bool(value))
+ }
+
+ convenience init(_ value: String) {
+ self.init(storage: .string(value))
+ }
+
+ convenience init(_ value: Int) {
+ self.init(storage: .int(value))
+ }
+
+ convenience init(_ value: Double) {
+ self.init(storage: .decimal(Decimal(value)))
+ }
+
+ convenience init(_ value: Decimal) {
+ self.init(storage: .decimal(value))
+ }
+
+ convenience init(_ value: [Value]) {
+ self.init(storage: .array(value))
+ }
+
+ convenience init(_ value: [String: Value]) {
+ self.init(storage: .dictionary(value))
+ }
+
+ convenience init(_ value: Drop) {
+ self.init(storage: .drop(value))
+ }
+
+ // MARK: -
+
+ var isTruthy: Bool {
+ switch storage {
+ case .nil, .bool(false):
+ return false
+ default:
+ return true
+ }
+ }
+
+ var size: Int {
+ switch storage {
+ case .array(let value):
+ return value.count
+ case .dictionary(let value):
+ return value.count
+ case .string(let value):
+ return value.count
+ default:
+ return 0
+ }
+ }
+
+ // MARK: -
+
+ func lookup(_ key: Value, encoder: Encoder) -> Value {
+ switch (storage, key.storage) {
+ case let (.array(array), .int(index)):
+ return array[index]
+ case let (.dictionary(dictionary), .string(key)):
+ return dictionary[key] ?? Value()
+ case let (.drop(drop), .string(key)):
+ return (try? drop.value(forKey: encoder.keyEncodingStrategy.transform(key: key), encoder: encoder)) ?? Value()
+ default:
+ return Value()
+ }
+ }
+
+ // MARK: - Type check conveniences
+
+ var isNil: Bool {
+ if case .nil = storage {
+ return true
+ }
+ return false
+ }
+
+ var isInteger: Bool {
+ if case .int = storage {
+ return true
+ }
+ return false
+ }
+
+ var isDecimal: Bool {
+ if case .decimal = storage {
+ return true
+ }
+ return false
+ }
+
+ var isString: Bool {
+ if case .string = storage {
+ return true
+ }
+ return false
+ }
+
+ var isArray: Bool {
+ if case .array = storage {
+ return true
+ }
+ return false
+ }
+
+ var isDictionary: Bool {
+ if case .dictionary = storage {
+ return true
+ }
+ return false
+ }
+
+ var isDrop: Bool {
+ if case .drop = storage {
+ return true
+ }
+ return false
+ }
+
+ // MARK: - Type conversions
+
+ func toInteger() -> Int {
+ switch storage {
+ case .int(let value):
+ return value
+ case .decimal(let value):
+ return value.intValue
+ default:
+ return 0
+ }
+ }
+
+ func toDecimal() -> Decimal {
+ switch storage {
+ case .int(let value):
+ return Decimal(value)
+ case .decimal(let value):
+ return value
+ default:
+ return 0
+ }
+ }
+
+ func toArray() -> [Value] {
+ if case let .array(value) = storage {
+ return value
+ }
+ return []
+ }
+
+ func toDictionary() -> [String: Value] {
+ if case let .dictionary(value) = storage {
+ return value
+ }
+ return [:]
+ }
+
+ func toString() -> String {
+ switch storage {
+ case .bool(let value):
+ return "\(value ? "true" : "false")"
+ case .string(let value):
+ return value
+ case .int(let value):
+ return "\(value)"
+ case .decimal(let value):
+ return "\(value)"
+ default:
+ return ""
+ }
+ }
+
+ func toDrop() -> Drop? {
+ if case let .drop(drop) = storage {
+ return drop
+ }
+ return nil
+ }
+
+ // MARK: -
+
+ func liquidString(encoder: Encoder) -> String {
+ switch storage {
+ case .bool(let value):
+ return "\(value ? "true" : "false")"
+ case .string(let value):
+ return value
+ case .int(let value):
+ return "\(value)"
+ case .decimal(let value):
+ return encoder.decimalEncodingStrategy.encode(value: value)
+ default:
+ return ""
+ }
+ }
+
+ // MARK: - Array manipulation
+
+ func push(value: Value) {
+ guard case var .array(array) = storage else {
+ return
+ }
+ array.append(value)
+ self.storage = .array(array)
+ }
+
+ @discardableResult
+ func pop() -> Value? {
+ guard case var .array(array) = storage else {
+ return nil
+ }
+ let last = array.popLast()
+ self.storage = .array(array)
+ return last
+ }
+
+ // MARK: - Dictionary manipulation
+
+ subscript(at: String) -> Value? {
+ get {
+ guard case let .dictionary(dictionary) = storage else {
+ return nil
+ }
+ return dictionary[at]
+ }
+ set {
+ guard case var .dictionary(dictionary) = storage else {
+ return
+ }
+ dictionary[at] = newValue
+ self.storage = .dictionary(dictionary)
+ }
+ }
+
+ // MARK: - Equatable
+
+ public static func ==(lhs: Value, rhs: Value) -> Bool {
+ return lhs.storage == rhs.storage
+ }
+
+ // MARK: - Comparable
+
+ public static func <(lhs: Value, rhs: Value) -> Bool {
+ return lhs.storage < rhs.storage
+ }
+
+ public static func <=(lhs: Value, rhs: Value) -> Bool {
+ return lhs.storage <= rhs.storage
+ }
+
+ public static func >(lhs: Value, rhs: Value) -> Bool {
+ return lhs.storage > rhs.storage
+ }
+
+ public static func >=(lhs: Value, rhs: Value) -> Bool {
+ return lhs.storage >= rhs.storage
+ }
+}
+
+extension Value: Hashable {
+ public func hash(into hasher: inout Hasher) {
+ hasher.combine(storage)
+ }
+}
+
+extension Value.Storage: Equatable {
+ static func ==(lhs: Value.Storage, rhs: Value.Storage) -> Bool {
+ switch (lhs, rhs) {
+ case (.nil, .nil):
+ return true
+ case let (.bool(l), .bool(r)):
+ return l == r
+ case let (.string(l), .string(r)):
+ return l == r
+ case let (.int(l), .int(r)):
+ return l == r
+ case let (.decimal(l), .decimal(r)):
+ return l == r
+// case let (.int(l), .float(r)):
+// case let (.float(l), .int(r)):
+ case let (.array(l), .array(r)):
+ return l == r
+ case let (.dictionary(l), .dictionary(r)):
+ return l == r
+ case let (.drop(l), .drop(r)):
+ return l === r
+ default:
+ return false
+ }
+ }
+}
+
+extension Value.Storage: Hashable {
+ func hash(into hasher: inout Hasher) {
+ switch self {
+ case .nil:
+ break
+ case .bool(let value):
+ hasher.combine(value)
+ case .string(let value):
+ hasher.combine(value)
+ case .int(let value):
+ hasher.combine(value)
+ case .decimal(let value):
+ hasher.combine(value)
+ case .array(let value):
+ hasher.combine(value)
+ case .dictionary(let value):
+ hasher.combine(value)
+ case .drop(let value):
+ hasher.combine(ObjectIdentifier(value))
+ }
+ }
+}
+
+extension Value.Storage: Comparable {
+ static func <(lhs: Value.Storage, rhs: Value.Storage) -> Bool {
+ switch (lhs, rhs) {
+ case let (.int(l), .int(r)):
+ return l < r
+ case let (.decimal(l), .decimal(r)):
+ return l < r
+// case let (.int(l), .float(r)):
+// return Float(l) < r
+// case let (.float(l), .int(r)):
+// return l < Float(r)
+ case let (.string(l), .string(r)):
+ return l < r
+ default:
+ return false
+ }
+ }
+
+ static func <=(lhs: Value.Storage, rhs: Value.Storage) -> Bool {
+ switch (lhs, rhs) {
+ case let (.int(l), .int(r)):
+ return l <= r
+ case let (.decimal(l), .decimal(r)):
+ return l <= r
+// case let (.int(l), .float(r)):
+// case let (.float(l), .int(r)):
+ case let (.string(l), .string(r)):
+ return l <= r
+ default:
+ return lhs == rhs
+ }
+ }
+
+ static func >(lhs: Value.Storage, rhs: Value.Storage) -> Bool {
+ // TODO: combos of int and float could be allowed
+ switch (lhs, rhs) {
+ case let (.int(l), .int(r)):
+ return l > r
+ case let (.decimal(l), .decimal(r)):
+ return l > r
+// case let (.int(l), .float(r)):
+// case let (.float(l), .int(r)):
+ case let (.string(l), .string(r)):
+ return l > r
+ default:
+ return false
+ }
+ }
+
+ static func >=(lhs: Value.Storage, rhs: Value.Storage) -> Bool {
+ switch (lhs, rhs) {
+ case let (.int(l), .int(r)):
+ return l >= r
+ case let (.decimal(l), .decimal(r)):
+ return l >= r
+// case let (.int(l), .float(r)):
+// case let (.float(l), .int(r)):
+ case let (.string(l), .string(r)):
+ return l >= r
+ default:
+ return lhs == rhs
+ }
+ }
+}
+
+extension Value.Storage: CustomStringConvertible, CustomDebugStringConvertible {
+ public var description: String {
+ switch self {
+ case .nil:
+ return ""
+ case let .bool(value):
+ return "bool: <\(value)>"
+ case let .string(value):
+ return "string: <\(value)>"
+ case let .int(value):
+ return "int: <\(value)>"
+ case let .decimal(value):
+ return "float: <\(value)>"
+ case let .array(value):
+ return "array: <\(value)>"
+ case let .dictionary(value):
+ return "dictionary: <\(value)>"
+ case let .drop(value):
+ return "drop: <\(value)>"
+ }
+ }
+
+ public var debugDescription: String {
+ return description
+ }
+}
+
+extension Value: CustomStringConvertible, CustomDebugStringConvertible {
+ public var description: String {
+ return storage.description
+ }
+
+ public var debugDescription: String {
+ return storage.debugDescription
+ }
+}
diff --git a/Sources/Liquid/Value/ValueConvertible.swift b/Sources/Liquid/Value/ValueConvertible.swift
new file mode 100644
index 0000000..7428ce9
--- /dev/null
+++ b/Sources/Liquid/Value/ValueConvertible.swift
@@ -0,0 +1,71 @@
+//
+// ValueConvertible.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-16.
+//
+
+import Foundation
+
+public protocol ValueConvertible {
+ func toValue(encoder: Encoder) -> Value
+}
+
+extension Bool: ValueConvertible {
+ public func toValue(encoder: Encoder) -> Value {
+ return Value(self)
+ }
+}
+
+extension String: ValueConvertible {
+ public func toValue(encoder: Encoder) -> Value {
+ return Value(self)
+ }
+}
+
+extension Int: ValueConvertible {
+ public func toValue(encoder: Encoder) -> Value {
+ return Value(self)
+ }
+}
+
+extension Double: ValueConvertible {
+ public func toValue(encoder: Encoder) -> Value {
+ return Value(self)
+ }
+}
+
+extension Decimal: ValueConvertible {
+ public func toValue(encoder: Encoder) -> Value {
+ return Value(self)
+ }
+}
+
+extension Array: ValueConvertible where Element: ValueConvertible {
+ public func toValue(encoder: Encoder) -> Value {
+ return Value(self.map { $0.toValue(encoder: encoder)} )
+ }
+}
+
+extension Dictionary: ValueConvertible where Key == String, Value: ValueConvertible {
+ public func toValue(encoder: Encoder) -> Liquid.Value {
+ return Liquid.Value(self.mapValues { $0.toValue(encoder: encoder)} )
+ }
+}
+
+extension Optional where Wrapped: ValueConvertible {
+ public func toValue(encoder: Encoder) -> Liquid.Value {
+ switch self {
+ case .none:
+ return Value()
+ case .some(let value):
+ return value.toValue(encoder: encoder)
+ }
+ }
+}
+
+extension Drop {
+ public func toValue(encoder: Encoder) -> Value {
+ return Value(self)
+ }
+}
diff --git a/Sources/Liquid/Variable.swift b/Sources/Liquid/Variable.swift
new file mode 100644
index 0000000..7456199
--- /dev/null
+++ b/Sources/Liquid/Variable.swift
@@ -0,0 +1,61 @@
+//
+// Variable.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-03.
+//
+
+import Foundation
+
+struct Variable {
+ private let expression: Expression
+ private var filters: [Filter] = []
+
+ init(string: String) throws {
+ self.init(parser: try Parser(string: string))
+ }
+
+ init(parser: Parser) {
+ self.expression = Expression.parse(parser)
+ parseFilters(parser)
+ }
+
+ private mutating func parseFilters(_ parser: Parser) {
+ while parser.consume(.pipe) != nil {
+ guard let name = parser.consume(.id) else {
+ break // TODO: throw an error ?
+ }
+ var args: [Expression]?
+ if parser.consume(.colon) != nil {
+ args = parseFilterArgs(parser)
+ }
+ filters.append(Filter(name: name, args: args ?? []))
+ }
+ parser.consume(.endOfString)
+ }
+
+ private func parseFilterArgs(_ parser: Parser) -> [Expression] {
+ var args: [Expression] = []
+
+ while !parser.look(.endOfString) {
+ args.append(Expression.parse(parser))
+ if !parser.look(.comma) {
+ break
+ }
+ parser.consume(.comma)
+ }
+
+ return args
+ }
+
+ func evaluate(context: Context) throws -> Value {
+ var value = expression.evaluate(context: context)
+ for filter in filters {
+ guard let filterFunc = context.filter(named: filter.name) else {
+ throw RuntimeError.unknownFilter(filter.name)
+ }
+ value = try filterFunc(value, filter.args.map { $0.evaluate(context: context) }, context.encoder)
+ }
+ return value
+ }
+}
diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift
new file mode 100644
index 0000000..036766c
--- /dev/null
+++ b/Tests/LinuxMain.swift
@@ -0,0 +1,8 @@
+import XCTest
+
+import LiquidTests
+
+var tests = [XCTestCaseEntry]()
+tests += LiquidTests.__allTests()
+
+XCTMain(tests)
diff --git a/Tests/LiquidTests/CaptureTests.swift b/Tests/LiquidTests/CaptureTests.swift
new file mode 100644
index 0000000..f20abaa
--- /dev/null
+++ b/Tests/LiquidTests/CaptureTests.swift
@@ -0,0 +1,22 @@
+//
+// CaptureTests.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-14.
+//
+
+import Foundation
+
+import XCTest
+@testable import Liquid
+
+final class CaptureTests: XCTestCase {
+ func test_capture() {
+ XCTAssertTemplate("{{ var2 }}{% capture var2 %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}", "content foo content foo ", ["var": "content"])
+ }
+
+ func test_capture_detects_bad_syntax() throws {
+ let template = Template(source: "{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}")
+ XCTAssertThrowsError(try template.parse())
+ }
+}
diff --git a/Tests/LiquidTests/CaseTests.swift b/Tests/LiquidTests/CaseTests.swift
new file mode 100644
index 0000000..3fbbbcf
--- /dev/null
+++ b/Tests/LiquidTests/CaseTests.swift
@@ -0,0 +1,92 @@
+//
+// CaseTests.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-12.
+//
+
+import Foundation
+
+import XCTest
+@testable import Liquid
+
+final class CaseTests: XCTestCase {
+
+ func test_case() {
+ XCTAssertTemplate("{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}", " its 2 ", ["condition": 2])
+ XCTAssertTemplate("{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}", " its 1 ", ["condition": 1])
+ XCTAssertTemplate("{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}", "", ["condition": 3])
+ XCTAssertTemplate(#"{% case condition %}{% when "string here" %} hit {% endcase %}"#, " hit ", ["condition": "string here"])
+ XCTAssertTemplate(#"{% case condition %}{% when "string here" %} hit {% endcase %}"#, "", ["condition": "bad string here"])
+ }
+
+ func test_case_with_else() {
+ XCTAssertTemplate("{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}", " hit ", ["condition": 5])
+ XCTAssertTemplate("{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}", " else ", ["condition": 6])
+ XCTAssertTemplate("{% case condition %} {% when 5 %} hit {% else %} else {% endcase %}", " else ", ["condition": 6])
+ }
+
+ func test_case_on_size() {
+ XCTAssertTemplate("{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}", "", ["a": []])
+ XCTAssertTemplate("{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}", "1", ["a": [1]])
+ XCTAssertTemplate("{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}", "2", ["a": [1, 1]])
+ XCTAssertTemplate("{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}", "", ["a": [1, 1, 1]])
+ XCTAssertTemplate("{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}", "", ["a": [1, 1, 1, 1]])
+ XCTAssertTemplate("{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}", "", ["a": [1, 1, 1, 1, 1]])
+ }
+
+ func test_case_on_size_with_else() {
+ XCTAssertTemplate("{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}", "else", ["a": []])
+ XCTAssertTemplate("{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}", "1", ["a": [1]])
+ XCTAssertTemplate("{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}", "2", ["a": [1, 1]])
+ XCTAssertTemplate("{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}", "else", ["a": [1, 1, 1]])
+ XCTAssertTemplate("{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}", "else", ["a": [1, 1, 1, 1]])
+ XCTAssertTemplate("{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}", "else", ["a": [1, 1, 1, 1, 1]])
+ }
+
+ func test_case_on_length_with_else() {
+ XCTAssertTemplate("{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}", "else")
+ XCTAssertTemplate("{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}", "false")
+ XCTAssertTemplate("{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}", "true")
+ XCTAssertTemplate("{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}", "else")
+ }
+
+ func test_assign_from_case() {
+ let code = "{% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}"
+ XCTAssertTemplate(code, "menswear", ["collection": ["handle": "menswear-jackets"]])
+ XCTAssertTemplate(code, "menswear", ["collection": ["handle": "menswear-t-shirts"]])
+ XCTAssertTemplate(code, "womenswear", ["collection": ["handle": "x"]])
+ XCTAssertTemplate(code, "womenswear", ["collection": ["handle": "y"]])
+ XCTAssertTemplate(code, "womenswear", ["collection": ["handle": "z"]])
+ }
+
+ func test_case_when_or() {
+ let code1 = "{% case condition %}{% when 1 or 2 or 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}"
+ XCTAssertTemplate(code1, " its 1 or 2 or 3 ", ["condition": 1])
+ XCTAssertTemplate(code1, " its 1 or 2 or 3 ", ["condition": 2])
+ XCTAssertTemplate(code1, " its 1 or 2 or 3 ", ["condition": 3])
+ XCTAssertTemplate(code1, " its 4 ", ["condition": 4])
+ XCTAssertTemplate(code1, "", ["condition": 5])
+
+ let code2 = #"{% case condition %}{% when 1 or "string" or null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}"#
+ XCTAssertTemplate(code2, " its 1 or 2 or 3 ", ["condition": 1])
+ XCTAssertTemplate(code2, " its 1 or 2 or 3 ", ["condition": "string"])
+ XCTAssertTemplate(code2, " its 1 or 2 or 3 ", ["condition": nil])
+ XCTAssertTemplate(code2, "", ["condition": "something else"])
+ }
+
+ func test_case_when_comma() {
+ let code1 = "{% case condition %}{% when 1, 2, 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}"
+ XCTAssertTemplate(code1, " its 1 or 2 or 3 ", ["condition": 1])
+ XCTAssertTemplate(code1, " its 1 or 2 or 3 ", ["condition": 2])
+ XCTAssertTemplate(code1, " its 1 or 2 or 3 ", ["condition": 3])
+ XCTAssertTemplate(code1, " its 4 ", ["condition": 4])
+ XCTAssertTemplate(code1, "", ["condition": 5])
+
+ let code2 = #"{% case condition %}{% when 1, "string", null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}"#
+ XCTAssertTemplate(code2, " its 1 or 2 or 3 ", ["condition": 1])
+ XCTAssertTemplate(code2, " its 1 or 2 or 3 ", ["condition": "string"])
+ XCTAssertTemplate(code2, " its 1 or 2 or 3 ", ["condition": nil])
+ XCTAssertTemplate(code2, "", ["condition": "something else"])
+ }
+}
diff --git a/Tests/LiquidTests/CycleTests.swift b/Tests/LiquidTests/CycleTests.swift
new file mode 100644
index 0000000..7767c0f
--- /dev/null
+++ b/Tests/LiquidTests/CycleTests.swift
@@ -0,0 +1,34 @@
+//
+// CycleTests.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-12.
+//
+
+import Foundation
+
+import XCTest
+@testable import Liquid
+
+final class CycleTests: XCTestCase {
+
+ func test_cycle() {
+ XCTAssertTemplate(#"{%cycle "one", "two"%}"#, "one")
+ XCTAssertTemplate(#"{%cycle "one", "two"%} {%cycle "one", "two"%}"#, "one two")
+ XCTAssertTemplate(#"{%cycle "", "two"%} {%cycle "", "two"%}"#, " two")
+ XCTAssertTemplate(#"{%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}"#, "one two one")
+ XCTAssertTemplate(#"{%cycle "text-align: left", "text-align: right" %} {%cycle "text-align: left", "text-align: right"%}"#, "text-align: left text-align: right")
+ }
+
+ func test_multiple_cycles() {
+ XCTAssertTemplate("{%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%}", "1 2 1 1 2 3 1")
+ }
+
+ func test_multiple_named_cycles() {
+ XCTAssertTemplate(#"{%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %}"#, "one one two two one one")
+ }
+
+ func test_multiple_named_cycles_with_names_from_context() {
+ XCTAssertTemplate(#"{%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %}"#, "one one two two one one", ["var1": 1, "var2": 2])
+ }
+}
diff --git a/Tests/LiquidTests/FilterTests.swift b/Tests/LiquidTests/FilterTests.swift
new file mode 100644
index 0000000..bbef9ff
--- /dev/null
+++ b/Tests/LiquidTests/FilterTests.swift
@@ -0,0 +1,273 @@
+//
+// FilterTests.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-12.
+//
+
+import Foundation
+
+import XCTest
+@testable import Liquid
+
+final class FilterTests: XCTestCase {
+ func testAppendToString() {
+ XCTAssertTemplate("{{ \"hello \" | append: \"world\" }}", "hello world")
+ XCTAssertTemplate("{{ 32 | append: \"world\" }}", "32world")
+ XCTAssertTemplate("{{ 32.94 | append: \"world\" }}", "32.94world")
+ }
+
+ func testPrepend() {
+ XCTAssertTemplate("{{ \" world\" | prepend: \"hello\" }}", "hello world")
+ }
+
+ func testDowncase() {
+ XCTAssertTemplate("{{ \"HELLO\" | downcase }}", "hello")
+ }
+
+ func testUpcase() {
+ XCTAssertTemplate("{{ \"hello\" | upcase }}", "HELLO")
+ }
+
+ func testCapitalize() {
+ XCTAssertTemplate("{{ \"hello world\" | capitalize }}", "Hello world")
+ }
+
+ func testStrip() {
+ XCTAssertTemplate("{{ \" \r\n\thello\t\n\r \" | strip }}", "hello")
+ }
+
+ func testRstrip() {
+ XCTAssertTemplate("{{ \" \r\n\thello\t\n\r \" | rstrip }}", " \r\n\thello")
+ }
+
+ func testLstrip() {
+ XCTAssertTemplate("{{ \" \r\n\thello\t\n\r \" | lstrip }}", "hello\t\n\r ")
+ }
+
+ func testStripNewLines() {
+ XCTAssertTemplate("{{ \"\r\nhe\nll\ro\r\" | strip_newlines }}", "hello")
+ }
+
+ func testNewlineToBr() {
+ XCTAssertTemplate("{{ \"hello\nand\ngoodbye\n\" | newline_to_br }}", "hello
\nand
\ngoodbye
\n")
+ }
+
+ func testEscape() {
+ XCTAssertEqual(try Filters.escapeFilter(value: Value(""), args: [], encoder: Encoder()).toString(), "<strong>")
+ }
+
+ func testEscapeOnce() throws {
+ XCTAssertEqual(try Filters.escapeOnceFilter(value: Value("<strong>Hulk"), args: [], encoder: Encoder()).toString(), "<strong>Hulk</strong>")
+ }
+
+ func testUrlEncode() {
+ XCTAssertEqual(try Filters.urlEncodeFilter(value: Value("foo+1@example.com"), args: [], encoder: Encoder()).toString(), "foo%2B1%40example.com")
+ }
+
+ func testUrlDecode() {
+ XCTAssertEqual(try Filters.urlDecodeFilter(value: Value("foo+bar"), args: [], encoder: Encoder()).toString(), "foo bar")
+ XCTAssertEqual(try Filters.urlDecodeFilter(value: Value("foo%20bar"), args: [], encoder: Encoder()).toString(), "foo bar")
+ XCTAssertEqual(try Filters.urlDecodeFilter(value: Value("foo%2B1%40example.com"), args: [], encoder: Encoder()).toString(), "foo+1@example.com")
+
+ XCTAssertEqual(try Filters.urlDecodeFilter(value: Value("%20"), args: [], encoder: Encoder()).toString(), " ")
+ XCTAssertEqual(try Filters.urlDecodeFilter(value: Value("%2"), args: [], encoder: Encoder()).toString(), "%2")
+ XCTAssertEqual(try Filters.urlDecodeFilter(value: Value("%"), args: [], encoder: Encoder()).toString(), "%")
+ }
+
+ func testStripHtml() {
+ XCTAssertTemplate("{{ \"hello world
\" | strip_html }}", "hello world")
+ }
+
+ func testTruncateWords() {
+ XCTAssertTemplate("{{ 'Ground control to Major Tom.' | truncatewords: 3 }}", "Ground control to…")
+ XCTAssertTemplate("{{ 'Ground control to Major Tom.' | truncatewords: 3, '--' }}", "Ground control to--")
+ XCTAssertTemplate("{{ 'Ground control to Major Tom.' | truncatewords: 3, '' }}", "Ground control to")
+ XCTAssertTemplate("{{ 'one two three' | truncatewords: 2, 1 }}", "one two1")
+ }
+
+ func testTruncate() {
+ XCTAssertTemplate("{{ \"Ground control to Major Tom.\" | truncate: 20 }}", "Ground control to M…")
+ XCTAssertTemplate("{{ \"Ground control to Major Tom.\" | truncate: 25, \", and so on\" }}", "Ground control, and so on")
+ XCTAssertTemplate("{{ \"Ground control to Major Tom.\" | truncate: 20, \"\" }}", "Ground control to Ma")
+ XCTAssertTemplate("{{ '1234567890' | truncate: 5, 1 }}", "12341")
+ }
+
+ func testPlus() {
+ XCTAssertTemplate("{{ 4 | plus: 2 }}", "6")
+ XCTAssertTemplate("{{ 16 | plus: 4 }}", "20")
+ XCTAssertTemplate("{{ 183.357 | plus: 12 }}", "195.357")
+ }
+
+ func testMinus() {
+ XCTAssertTemplate("{{ 4 | minus: 2 }}", "2")
+ XCTAssertTemplate("{{ 16 | minus: 4 }}", "12")
+ XCTAssertTemplate("{{ 183.357 | minus: 12 }}", "171.357")
+ }
+
+ func testTimes() {
+ XCTAssertTemplate("{{ 4 | times: 2 }}", "8")
+ XCTAssertTemplate("{{ 16 | times: 4 }}", "64")
+ XCTAssertTemplate("{{ 183.357 | times: 12 }}", "2200.284")
+ }
+
+ func testDividedBy() {
+ XCTAssertTemplate("{{ 16 | divided_by: 4 }}", "4")
+ XCTAssertTemplate("{{ 5 | divided_by: 3 }}", "1")
+ XCTAssertTemplate("{{ 20 | divided_by: 7 }}", "2")
+ XCTAssertTemplate("{{ 20 | divided_by: 7.0 }}", "2.85714286")
+ }
+
+ func testAbs() {
+ XCTAssertTemplate("{{ -17 | abs }}", "17")
+ XCTAssertTemplate("{{ 4 | abs }}", "4")
+ XCTAssertTemplate("{{ -19.86 | abs }}", "19.86")
+ }
+
+ func testCeil() {
+ XCTAssertTemplate("{{ 1.2 | ceil }}", "2")
+ XCTAssertTemplate("{{ 2.0 | ceil }}", "2")
+ XCTAssertTemplate("{{ 183.357 | ceil }}", "184")
+ }
+
+ func testFloor() {
+ XCTAssertTemplate("{{ 1.2 | floor }}", "1")
+ XCTAssertTemplate("{{ 2.0 | floor }}", "2")
+ XCTAssertTemplate("{{ 183.357 | floor }}", "183")
+ }
+
+ func testRound() {
+ XCTAssertTemplate("{{ 1.2 | round }}", "1")
+ XCTAssertTemplate("{{ 2.7 | round }}", "3")
+ XCTAssertTemplate("{{ 183.357 | round: 1}}", "183.4")
+ XCTAssertTemplate("{{ 183.357 | round: 2}}", "183.36")
+ }
+
+ func testModulo() {
+ XCTAssertTemplate("{{ 3 | modulo: 2 }}", "1")
+ XCTAssertTemplate("{{ 24 | modulo: 7 }}", "3")
+ XCTAssertTemplate("{{ 183.357 | modulo: 12 }}", "3.357")
+ }
+
+ func testSplit() {
+ XCTAssertTemplate("{{ 'John, Paul, George, Ringo' | split: ', ' | size }}", "4")
+ XCTAssertTemplate("{{ 'A1Z' | split: '1' | join: '' }}", "AZ")
+ XCTAssertTemplate("{{ 'A1Z' | split: 1 | join: '' }}", "AZ")
+ }
+
+ func testJoin() {
+ XCTAssertTemplate("{{ 'John, Paul, George, Ringo' | split: ', ' | join: '.' }}", "John.Paul.George.Ringo")
+ }
+
+ func testUnique() {
+ XCTAssertTemplate("{{ 'ants, bugs, bees, bugs, ants' | split: ', ' | uniq | join: '.' }}", "ants.bugs.bees")
+ }
+
+ func testSize() {
+ XCTAssertTemplate("{{ 'Ground control to Major Tom.' | size }}", "28")
+ XCTAssertTemplate("{{ what.size }}", "28", ["what": "Ground control to Major Tom."])
+ }
+
+ func testFirst() {
+ XCTAssertTemplate("{{ 'John, Paul, George, Ringo' | split: ', ' | first }}", "John")
+ XCTAssertTemplate("{{ names.first }}", "John", ["names": ["John", "Paul", "George", "Ringo"]])
+ }
+
+ func testLast() {
+ XCTAssertTemplate("{{ 'John, Paul, George, Ringo' | split: ', ' | last }}", "Ringo")
+ XCTAssertTemplate("{{ names.last }}", "Ringo", ["names": ["John", "Paul", "George", "Ringo"]])
+ }
+
+ func testDefault() {
+ XCTAssertTemplate("{{ nil | default: 'test' }}", "test")
+ XCTAssertTemplate("{{ product_price | default: 2.99 }}", "2.99")
+ XCTAssertTemplate("{{ product_price | default: 2.99 }}", "4.99", ["product_price": 4.99]) // TODO: Double needs to be parsable
+ XCTAssertTemplate("{{ product_price | default: 2.99 }}", "2.99", ["product_price": nil])
+ }
+
+ func testReplace() {
+ XCTAssertTemplate("{{ 'Take my protein pills and put my helmet on' | replace: 'my', 'your' }}", "Take your protein pills and put your helmet on")
+ }
+
+ func testReplaceFirst() {
+ XCTAssertTemplate("{{ 'Take my protein pills and put my helmet on' | replace_first: 'my', 'your' }}", "Take your protein pills and put my helmet on")
+ }
+
+ func testRemove() {
+ XCTAssertTemplate("{{ 'I strained to see the train through the rain' | remove: 'rain' }}", "I sted to see the t through the ")
+ }
+
+ func testRemoveFirst() {
+ XCTAssertTemplate("{{ 'I strained to see the train through the rain' | remove_first: 'rain' }}", "I sted to see the train through the rain")
+ }
+
+ func testSlice() {
+ XCTAssertTemplate("{{ 'Liquid' | slice: 0 }}", "L")
+ XCTAssertTemplate("{{ 'Liquid' | slice: 2 }}", "q")
+ XCTAssertTemplate("{{ 'Liquid' | slice: -3, 2 }}", "ui")
+ }
+
+ func testReverse() {
+ XCTAssertTemplate("{{ 'apples,oranges,peaches' | split: ',' | reverse | join: ',' }}", "peaches,oranges,apples")
+ }
+
+ func testCompact() {
+ let data = [
+ "array": [nil, "hello", nil, nil, "world", nil]
+ ]
+ XCTAssertTemplate("{{ array | size }}", "6", data)
+ XCTAssertTemplate("{{ array | compact | size }}", "2", data)
+ XCTAssertTemplate("{{ array | compact | join: ' ' }}", "hello world", data)
+ }
+
+ func testMap() {
+ let data = [
+ "pages": [
+ ["category": "business"],
+ ["category": "celebrities"],
+ ["category": "lifestyle"],
+ ["category": "sports"],
+ ["category": "technology"]
+ ]
+ ]
+ XCTAssertTemplate("{{ pages | size }}", "5", data)
+ XCTAssertTemplate("{{ pages | map: 'category' | size }}", "5", data)
+ XCTAssertTemplate("{{ pages | map: 'category' | join: ' ' }}", "business celebrities lifestyle sports technology", data)
+ }
+
+ func testConcat() {
+ XCTAssertTemplate("{{ names1 | concat: names2 | join: ',' }}", "bill,steve,larry,michael", ["names1": ["bill", "steve"], "names2": ["larry", "michael"]])
+ }
+
+ func testSort() {
+ XCTAssertTemplate("{{ array | sort | join: ', ' }}", "Sally Snake, giraffe, octopus, zebra", ["array": ["zebra", "octopus", "giraffe", "Sally Snake"]])
+ XCTAssertTemplate("{{ names | sort: 'name' | map: 'name' | join: ', ' }}", "Jane, Sally, bob, george", ["names": [["name": "bob"], ["name": "Sally"], ["name": "george"], ["name": "Jane"]]])
+ }
+
+ func testSortNatural() {
+ XCTAssertTemplate("{{ array | sort_natural | join: ', ' }}", "giraffe, octopus, Sally Snake, zebra", ["array": ["zebra", "octopus", "giraffe", "Sally Snake"]])
+ XCTAssertTemplate("{{ names | sort_natural: 'name' | map: 'name' | join: ', ' }}", "bob, george, Jane, Sally", ["names": [["name": "bob"], ["name": "Sally"], ["name": "george"], ["name": "Jane"]]])
+ }
+
+ func testDate() throws {
+ NSTimeZone.default = TimeZone(abbreviation: "GMT")!
+ var encoder = Encoder()
+ encoder.locale = Locale(identifier: "en_US")
+
+ XCTAssertEqual(try Filters.dateFilter(value: Value("2006-05-05T10:00:00Z"), args: [Value("%B")], encoder: encoder), Value("May"))
+ XCTAssertEqual(try Filters.dateFilter(value: Value("2006-06-05T10:00:00Z"), args: [Value("%B")], encoder: encoder), Value("June"))
+ XCTAssertEqual(try Filters.dateFilter(value: Value("2006-07-05T10:00:00Z"), args: [Value("%B")], encoder: encoder), Value("July"))
+
+ XCTAssertEqual(try Filters.dateFilter(value: Value("2006-07-05T10:00:00Z"), args: [Value("")], encoder: encoder), Value("7/5/06, 10:00:00 AM"))
+ XCTAssertEqual(try Filters.dateFilter(value: Value("2006-07-05T10:00:00Z"), args: [Value()], encoder: encoder), Value("7/5/06, 10:00:00 AM"))
+
+ let yearString = "\(Calendar.autoupdatingCurrent.component(.year, from: Date()))"
+ XCTAssertEqual(try Filters.dateFilter(value: Value("2004-07-16T01:00:00Z"), args: [Value("%m/%d/%Y")], encoder: encoder), Value("07/16/2004"))
+ XCTAssertEqual(try Filters.dateFilter(value: Value("now"), args: [Value("%Y")], encoder: encoder), Value(yearString))
+ XCTAssertEqual(try Filters.dateFilter(value: Value("today"), args: [Value("%Y")], encoder: encoder), Value(yearString))
+ XCTAssertEqual(try Filters.dateFilter(value: Value("Today"), args: [Value("%Y")], encoder: encoder), Value(yearString))
+
+ XCTAssertEqual(try Filters.dateFilter(value: Value(1152098955), args: [Value("%m/%d/%Y")], encoder: encoder), Value("07/05/2006"))
+ XCTAssertEqual(try Filters.dateFilter(value: Value("1152098955"), args: [Value("%m/%d/%Y")], encoder: encoder), Value("07/05/2006"))
+ }
+}
diff --git a/Tests/LiquidTests/ForTests.swift b/Tests/LiquidTests/ForTests.swift
new file mode 100644
index 0000000..a9539d5
--- /dev/null
+++ b/Tests/LiquidTests/ForTests.swift
@@ -0,0 +1,97 @@
+//
+// ForTests.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-11.
+//
+
+import Foundation
+
+import XCTest
+@testable import Liquid
+
+final class ForTests: XCTestCase {
+ func test_for() {
+ XCTAssertTemplate("{%for item in array%} yo {%endfor%}", " yo yo yo yo ", ["array": [1, 2, 3, 4]])
+ XCTAssertTemplate("{%for item in array%}yo{%endfor%}", "yoyo", ["array": [1, 2]])
+ XCTAssertTemplate("{%for item in array%} yo {%endfor%}", " yo ", ["array": [1]])
+ XCTAssertTemplate("{%for item in array%}{%endfor%}", "", ["array": [] as [Int]])
+ }
+
+ func test_for_reversed() {
+ XCTAssertTemplate("{%for item in array reversed %}{{item}}{%endfor%}", "321", ["array": [1, 2, 3]])
+ }
+
+ func test_for_with_range() {
+ XCTAssertTemplate("{%for item in (1..3) %} {{item}} {%endfor%}", " 1 2 3 ")
+ XCTAssertTemplate("{% for i in (a..2) %}{% endfor %}", "", ["a": [1, 2]])
+ XCTAssertTemplate("{% for item in (a..3) %} {{item}} {% endfor %}", " 0 1 2 3 ", ["a": "invalid integer"])
+ }
+
+ func test_for_with_variable_range() {
+ XCTAssertTemplate("{%for item in (1..foobar.value) %} {{item}} {%endfor%}", " 1 2 3 ", ["foobar": ["value": 3]])
+ }
+
+ func test_for_helpers() {
+ let assigns = ["array": [1, 2, 3]]
+ XCTAssertTemplate("{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}",
+ " 1/3 2/3 3/3 ",
+ assigns)
+ XCTAssertTemplate("{%for item in array%} {{forloop.index}} {%endfor%}", " 1 2 3 ", assigns)
+ XCTAssertTemplate("{%for item in array%} {{forloop.index0}} {%endfor%}", " 0 1 2 ", assigns)
+ XCTAssertTemplate("{%for item in array%} {{forloop.rindex0}} {%endfor%}", " 2 1 0 ", assigns)
+ XCTAssertTemplate("{%for item in array%} {{forloop.rindex}} {%endfor%}", " 3 2 1 ", assigns)
+ XCTAssertTemplate("{%for item in array%} {{forloop.first}} {%endfor%}", " true false false ", assigns)
+ XCTAssertTemplate("{%for item in array%} {{forloop.last}} {%endfor%}", " false false true ", assigns)
+ }
+
+ func test_for_and_if() {
+ let assigns = ["array": [1, 2, 3]]
+ XCTAssertTemplate("{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}", "+--", assigns)
+ }
+
+ func test_for_else() {
+ XCTAssertTemplate("{%for item in array%}+{%else%}-{%endfor%}", "+++", ["array": [1, 2, 3]])
+ XCTAssertTemplate("{%for item in array%}+{%else%}-{%endfor%}", "-", ["array": [] as [Int]])
+ XCTAssertTemplate("{%for item in array%}+{%else%}-{%endfor%}", "-", ["array": nil])
+ }
+
+ func test_limiting() {
+ let assigns = ["array": [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]]
+ XCTAssertTemplate("{%for i in array limit:2 %}{{ i }}{%endfor%}", "12", assigns)
+ XCTAssertTemplate("{%for i in array limit:4 %}{{ i }}{%endfor%}", "1234", assigns)
+ XCTAssertTemplate("{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}", "3456", assigns)
+ XCTAssertTemplate("{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}", "3456", assigns)
+ }
+
+ func test_for_with_break() {
+ let assigns = ["array": ["items": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]]
+
+// XCTAssertTemplate("{% for i in array.items %}{% break %}{% endfor %}", "", assigns)
+// XCTAssertTemplate("{% for i in array.items %}{{ i }}{% break %}{% endfor %}", "1", assigns)
+// XCTAssertTemplate("{% for i in array.items %}{% break %}{{ i }}{% endfor %}", "", assigns)
+ XCTAssertTemplate("{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}", "1234", assigns)
+
+ // tests to ensure it only breaks out of the local for loop and not all of them.
+// XCTAssertTemplate("{% for item in array %}{% for i in item %}{% if i == 1 %}{% break %}{% endif %}{{ i }}{% endfor %}{% endfor %}", "3456", ["array": [[1, 2], [3, 4], [5, 6]]])
+
+ // test break does nothing when unreached
+// XCTAssertTemplate("{% for i in array.items %}{% if i == 9999 %}{% break %}{% endif %}{{ i }}{% endfor %}", "12345", ["array": ["items": [1, 2, 3, 4, 5]]])
+ }
+
+ func test_for_with_continue() {
+ let assigns = ["array": ["items": [1, 2, 3, 4, 5]]]
+
+ XCTAssertTemplate("{% for i in array.items %}{% continue %}{% endfor %}", "", assigns)
+ XCTAssertTemplate("{% for i in array.items %}{{ i }}{% continue %}{% endfor %}", "12345", assigns)
+ XCTAssertTemplate("{% for i in array.items %}{% continue %}{{ i }}{% endfor %}", "", assigns)
+ XCTAssertTemplate("{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}", "123", assigns)
+ XCTAssertTemplate("{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}", "1245", assigns)
+
+ // tests to ensure it only continues the local for loop and not all of them.
+ XCTAssertTemplate("{% for item in array %}{% for i in item %}{% if i == 1 %}{% continue %}{% endif %}{{ i }}{% endfor %}{% endfor %}", "23456", ["array": [[1, 2], [3, 4], [5, 6]]])
+
+ // test continue does nothing when unreached
+ XCTAssertTemplate("{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}", "12345", ["array": ["items": [1, 2, 3, 4, 5]]])
+ }
+}
diff --git a/Tests/LiquidTests/IfElseTests.swift b/Tests/LiquidTests/IfElseTests.swift
new file mode 100644
index 0000000..eb68416
--- /dev/null
+++ b/Tests/LiquidTests/IfElseTests.swift
@@ -0,0 +1,137 @@
+//
+// IfElseTests.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-12.
+//
+
+import Foundation
+
+import XCTest
+@testable import Liquid
+
+final class IfElseTests: XCTestCase {
+ func test_if() {
+ XCTAssertTemplate(" {% if false %} this text should not go into the output {% endif %} ", " ")
+ XCTAssertTemplate(" {% if true %} this text should go into the output {% endif %} ",
+ " this text should go into the output ")
+ XCTAssertTemplate("{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?", " you rock ?")
+ }
+
+ func test_literal_comparisons() {
+ XCTAssertTemplate("{% assign v = false %}{% if v %} YES {% else %} NO {% endif %}", " NO ")
+ XCTAssertTemplate("{% assign v = nil %}{% if v == nil %} YES {% else %} NO {% endif %}", " YES ")
+ }
+
+ func test_if_else() {
+ XCTAssertTemplate("{% if false %} NO {% else %} YES {% endif %}", " YES ")
+ XCTAssertTemplate("{% if true %} YES {% else %} NO {% endif %}", " YES ")
+ XCTAssertTemplate(#"{% if "foo" %} YES {% else %} NO {% endif %}"#, " YES ")
+ }
+
+ func test_if_boolean() {
+ XCTAssertTemplate("{% if var %} YES {% endif %}", " YES ", ["var": true])
+ }
+
+ func test_if_or() {
+ XCTAssertTemplate("{% if a or b %} YES {% endif %}", " YES ", ["a": true, "b": true])
+ XCTAssertTemplate("{% if a or b %} YES {% endif %}", " YES ", ["a": true, "b": false])
+ XCTAssertTemplate("{% if a or b %} YES {% endif %}", " YES ", ["a": false, "b": true])
+ XCTAssertTemplate("{% if a or b %} YES {% endif %}", "", ["a": false, "b": false])
+
+ XCTAssertTemplate("{% if a or b or c %} YES {% endif %}", " YES ", ["a": false, "b": false, "c": true])
+ XCTAssertTemplate("{% if a or b or c %} YES {% endif %}", "", ["a": false, "b": false, "c": false])
+ }
+
+ func test_if_or_with_operators() {
+ XCTAssertTemplate("{% if a == true or b == true %} YES {% endif %}", " YES ", ["a": true, "b": true])
+ XCTAssertTemplate("{% if a == true or b == false %} YES {% endif %}", " YES ", ["a": true, "b": true])
+ XCTAssertTemplate("{% if a == false or b == false %} YES {% endif %}", "", ["a": true, "b": true])
+ }
+
+ func test_comparison_of_strings_containing_and_or_or() {
+ let awfulMarkup = "a == 'and' and b == 'or' and c == 'foo and bar' and d == 'bar or baz' and e == 'foo' and foo and bar"
+ let assigns: [String: Any] = [ "a": "and", "b": "or", "c": "foo and bar", "d": "bar or baz", "e": "foo", "foo": true, "bar": true ]
+ XCTAssertTemplate("{% if \(awfulMarkup) %} YES {% endif %}", " YES ", assigns)
+ }
+
+ func test_if_and() {
+ XCTAssertTemplate("{% if true and true %} YES {% endif %}", " YES ")
+ XCTAssertTemplate("{% if false and true %} YES {% endif %}", "")
+ XCTAssertTemplate("{% if false and true %} YES {% endif %}", "")
+ }
+
+ func test_hash_miss_generates_false() {
+ XCTAssertTemplate("{% if foo.bar %} NO {% endif %}", "", ["foo": [] as [Int]])
+ }
+
+ func test_if_from_variable() {
+ XCTAssertTemplate("{% if var %} NO {% endif %}", "", ["var": false])
+ XCTAssertTemplate("{% if var %} NO {% endif %}", "", ["var": nil])
+ XCTAssertTemplate("{% if foo.bar %} NO {% endif %}", "", ["foo": ["bar": false]])
+ XCTAssertTemplate("{% if foo.bar %} NO {% endif %}", "", ["foo": []])
+ XCTAssertTemplate("{% if foo.bar %} NO {% endif %}", "", ["foo": nil])
+ XCTAssertTemplate("{% if foo.bar %} NO {% endif %}", "", ["foo": true])
+
+ XCTAssertTemplate("{% if var %} YES {% endif %}", " YES ", ["var": "text"])
+ XCTAssertTemplate("{% if var %} YES {% endif %}", " YES ", ["var": true])
+ XCTAssertTemplate("{% if var %} YES {% endif %}", " YES ", ["var": 1])
+ XCTAssertTemplate("{% if var %} YES {% endif %}", " YES ", ["var": []])
+ XCTAssertTemplate("{% if var %} YES {% endif %}", " YES ", ["var": []])
+ XCTAssertTemplate(#"{% if "foo" %} YES {% endif %}"#, " YES ")
+ XCTAssertTemplate("{% if foo.bar %} YES {% endif %}", " YES ", ["foo": ["bar": true]])
+ XCTAssertTemplate("{% if foo.bar %} YES {% endif %}", " YES ", ["foo": ["bar": "text"]])
+ XCTAssertTemplate("{% if foo.bar %} YES {% endif %}", " YES ", ["foo": ["bar": 1]])
+ XCTAssertTemplate("{% if foo.bar %} YES {% endif %}", " YES ", ["foo": ["bar": []]])
+ XCTAssertTemplate("{% if foo.bar %} YES {% endif %}", " YES ", ["foo": ["bar": []]])
+
+ XCTAssertTemplate("{% if var %} NO {% else %} YES {% endif %}", " YES ", ["var": false])
+ XCTAssertTemplate("{% if var %} NO {% else %} YES {% endif %}", " YES ", ["var": nil])
+ XCTAssertTemplate("{% if var %} YES {% else %} NO {% endif %}", " YES ", ["var": true])
+ XCTAssertTemplate(#"{% if "foo" %} YES {% else %} NO {% endif %}"#, " YES ", ["var": "text"])
+
+ XCTAssertTemplate("{% if foo.bar %} NO {% else %} YES {% endif %}", " YES ", ["foo": ["bar": false]])
+ XCTAssertTemplate("{% if foo.bar %} YES {% else %} NO {% endif %}", " YES ", ["foo": ["bar": true]])
+ XCTAssertTemplate("{% if foo.bar %} YES {% else %} NO {% endif %}", " YES ", ["foo": ["bar": "text"]])
+ XCTAssertTemplate("{% if foo.bar %} NO {% else %} YES {% endif %}", " YES ", ["foo": ["notbar": true]])
+ XCTAssertTemplate("{% if foo.bar %} NO {% else %} YES {% endif %}", " YES ", ["foo": []])
+ XCTAssertTemplate("{% if foo.bar %} NO {% else %} YES {% endif %}", " YES ", ["notfoo": ["bar": true]])
+ }
+
+ func test_nested_if() {
+ XCTAssertTemplate("{% if false %}{% if false %} NO {% endif %}{% endif %}", "")
+ XCTAssertTemplate("{% if false %}{% if true %} NO {% endif %}{% endif %}", "")
+ XCTAssertTemplate("{% if true %}{% if false %} NO {% endif %}{% endif %}", "")
+ XCTAssertTemplate("{% if true %}{% if true %} YES {% endif %}{% endif %}", " YES ")
+
+ XCTAssertTemplate("{% if true %}{% if true %} YES {% else %} NO {% endif %}{% else %} NO {% endif %}", " YES ")
+ XCTAssertTemplate("{% if true %}{% if false %} NO {% else %} YES {% endif %}{% else %} NO {% endif %}", " YES ")
+ XCTAssertTemplate("{% if false %}{% if true %} NO {% else %} NONO {% endif %}{% else %} YES {% endif %}", " YES ")
+ }
+
+ func test_comparisons_on_null() {
+// XCTAssertTemplate("{% if null < 10 %} NO {% endif %}", "")
+ XCTAssertTemplate("{% if null <= 10 %} NO {% endif %}", "")
+// XCTAssertTemplate("{% if null >= 10 %} NO {% endif %}", "")
+// XCTAssertTemplate("{% if null > 10 %} NO {% endif %}", "")
+//
+// XCTAssertTemplate("{% if 10 < null %} NO {% endif %}", "")
+// XCTAssertTemplate("{% if 10 <= null %} NO {% endif %}", "")
+// XCTAssertTemplate("{% if 10 >= null %} NO {% endif %}", "")
+// XCTAssertTemplate("{% if 10 > null %} NO {% endif %}", "")
+ }
+
+ func test_else_if() {
+ XCTAssertTemplate("{% if 0 == 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}", "0")
+ XCTAssertTemplate("{% if 0 != 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}", "1")
+ XCTAssertTemplate("{% if 0 != 0 %}0{% elsif 1 != 1%}1{% else %}2{% endif %}", "2")
+
+ XCTAssertTemplate("{% if false %}if{% elsif true %}elsif{% endif %}", "elsif")
+ }
+
+ func test_unended_if() throws {
+ let template = Template(source: "{% if true %}hello")
+ XCTAssertThrowsError(try template.parse())
+ }
+}
+
diff --git a/Tests/LiquidTests/LexerTests.swift b/Tests/LiquidTests/LexerTests.swift
new file mode 100644
index 0000000..db6782f
--- /dev/null
+++ b/Tests/LiquidTests/LexerTests.swift
@@ -0,0 +1,17 @@
+//
+// LexerTests.swift
+//
+//
+// Created by Geoffrey Foster on 2019-08-29.
+//
+
+import XCTest
+@testable import Liquid
+
+final class LexerTests: XCTestCase {
+ func testTextToken() {
+ let tokenizer = Tokenizer(source: "hello {{ name }}, how are you?")
+ XCTAssertEqual(tokenizer.tokens, [.text(value: "hello "), .variable(value: "name"), .text(value: ", how are you?")])
+ }
+}
+
diff --git a/Tests/LiquidTests/LiquidTests.swift b/Tests/LiquidTests/LiquidTests.swift
new file mode 100644
index 0000000..1708563
--- /dev/null
+++ b/Tests/LiquidTests/LiquidTests.swift
@@ -0,0 +1,11 @@
+import XCTest
+@testable import Liquid
+
+final class LiquidTests: XCTestCase {
+ func testExample() {
+ }
+
+ static var allTests = [
+ ("testExample", testExample),
+ ]
+}
diff --git a/Tests/LiquidTests/ValueTests.swift b/Tests/LiquidTests/ValueTests.swift
new file mode 100644
index 0000000..f0d77f5
--- /dev/null
+++ b/Tests/LiquidTests/ValueTests.swift
@@ -0,0 +1,21 @@
+//
+// ValueTests.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-12.
+//
+
+import Foundation
+
+import XCTest
+@testable import Liquid
+
+final class ValueTests: XCTestCase {
+ func testComparable() {
+ XCTAssertFalse(Value() < Value(1))
+ XCTAssertFalse(Value() <= Value(1))
+ XCTAssertFalse(Value() > Value(1))
+ XCTAssertFalse(Value() >= Value(1))
+ XCTAssertFalse(Value() == Value(1))
+ }
+}
diff --git a/Tests/LiquidTests/XCTestCase+Extensions.swift b/Tests/LiquidTests/XCTestCase+Extensions.swift
new file mode 100644
index 0000000..11bad00
--- /dev/null
+++ b/Tests/LiquidTests/XCTestCase+Extensions.swift
@@ -0,0 +1,20 @@
+//
+// XCTestCase+Extensions.swift
+//
+//
+// Created by Geoffrey Foster on 2019-09-09.
+//
+
+import XCTest
+@testable import Liquid
+
+func XCTAssertTemplate(_ templateString: String, _ expression2: String, _ values: [String: Any?] = [:], _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) {
+ let template = Template(source: templateString)
+ XCTAssertNoThrow(try template.parse())
+ do {
+ let result = try template.render(values: values)
+ XCTAssertEqual(result, expression2, message(), file: file, line: line)
+ } catch {
+ XCTFail(error.localizedDescription)
+ }
+}
diff --git a/Tests/LiquidTests/XCTestManifests.swift b/Tests/LiquidTests/XCTestManifests.swift
new file mode 100644
index 0000000..50761bc
--- /dev/null
+++ b/Tests/LiquidTests/XCTestManifests.swift
@@ -0,0 +1,175 @@
+#if !canImport(ObjectiveC)
+import XCTest
+
+extension CaptureTests {
+ // DO NOT MODIFY: This is autogenerated, use:
+ // `swift test --generate-linuxmain`
+ // to regenerate.
+ static let __allTests__CaptureTests = [
+ ("test_capture", test_capture),
+ ("test_capture_detects_bad_syntax", test_capture_detects_bad_syntax),
+ ]
+}
+
+extension CaseTests {
+ // DO NOT MODIFY: This is autogenerated, use:
+ // `swift test --generate-linuxmain`
+ // to regenerate.
+ static let __allTests__CaseTests = [
+ ("test_assign_from_case", test_assign_from_case),
+ ("test_case", test_case),
+ ("test_case_on_length_with_else", test_case_on_length_with_else),
+ ("test_case_on_size", test_case_on_size),
+ ("test_case_on_size_with_else", test_case_on_size_with_else),
+ ("test_case_when_comma", test_case_when_comma),
+ ("test_case_when_or", test_case_when_or),
+ ("test_case_with_else", test_case_with_else),
+ ]
+}
+
+extension CycleTests {
+ // DO NOT MODIFY: This is autogenerated, use:
+ // `swift test --generate-linuxmain`
+ // to regenerate.
+ static let __allTests__CycleTests = [
+ ("test_cycle", test_cycle),
+ ("test_multiple_cycles", test_multiple_cycles),
+ ("test_multiple_named_cycles", test_multiple_named_cycles),
+ ("test_multiple_named_cycles_with_names_from_context", test_multiple_named_cycles_with_names_from_context),
+ ]
+}
+
+extension FilterTests {
+ // DO NOT MODIFY: This is autogenerated, use:
+ // `swift test --generate-linuxmain`
+ // to regenerate.
+ static let __allTests__FilterTests = [
+ ("testAbs", testAbs),
+ ("testAppendToString", testAppendToString),
+ ("testCapitalize", testCapitalize),
+ ("testCeil", testCeil),
+ ("testCompact", testCompact),
+ ("testConcat", testConcat),
+ ("testDate", testDate),
+ ("testDefault", testDefault),
+ ("testDividedBy", testDividedBy),
+ ("testDowncase", testDowncase),
+ ("testEscape", testEscape),
+ ("testEscapeOnce", testEscapeOnce),
+ ("testFirst", testFirst),
+ ("testFloor", testFloor),
+ ("testJoin", testJoin),
+ ("testLast", testLast),
+ ("testLstrip", testLstrip),
+ ("testMap", testMap),
+ ("testMinus", testMinus),
+ ("testModulo", testModulo),
+ ("testNewlineToBr", testNewlineToBr),
+ ("testPlus", testPlus),
+ ("testPrepend", testPrepend),
+ ("testRemove", testRemove),
+ ("testRemoveFirst", testRemoveFirst),
+ ("testReplace", testReplace),
+ ("testReplaceFirst", testReplaceFirst),
+ ("testReverse", testReverse),
+ ("testRound", testRound),
+ ("testRstrip", testRstrip),
+ ("testSize", testSize),
+ ("testSlice", testSlice),
+ ("testSort", testSort),
+ ("testSortNatural", testSortNatural),
+ ("testSplit", testSplit),
+ ("testStrip", testStrip),
+ ("testStripHtml", testStripHtml),
+ ("testStripNewLines", testStripNewLines),
+ ("testTimes", testTimes),
+ ("testTruncate", testTruncate),
+ ("testTruncateWords", testTruncateWords),
+ ("testUnique", testUnique),
+ ("testUpcase", testUpcase),
+ ("testUrlDecode", testUrlDecode),
+ ("testUrlEncode", testUrlEncode),
+ ]
+}
+
+extension ForTests {
+ // DO NOT MODIFY: This is autogenerated, use:
+ // `swift test --generate-linuxmain`
+ // to regenerate.
+ static let __allTests__ForTests = [
+ ("test_for", test_for),
+ ("test_for_and_if", test_for_and_if),
+ ("test_for_else", test_for_else),
+ ("test_for_helpers", test_for_helpers),
+ ("test_for_reversed", test_for_reversed),
+ ("test_for_with_break", test_for_with_break),
+ ("test_for_with_continue", test_for_with_continue),
+ ("test_for_with_range", test_for_with_range),
+ ("test_for_with_variable_range", test_for_with_variable_range),
+ ("test_limiting", test_limiting),
+ ]
+}
+
+extension IfElseTests {
+ // DO NOT MODIFY: This is autogenerated, use:
+ // `swift test --generate-linuxmain`
+ // to regenerate.
+ static let __allTests__IfElseTests = [
+ ("test_comparison_of_strings_containing_and_or_or", test_comparison_of_strings_containing_and_or_or),
+ ("test_comparisons_on_null", test_comparisons_on_null),
+ ("test_else_if", test_else_if),
+ ("test_hash_miss_generates_false", test_hash_miss_generates_false),
+ ("test_if", test_if),
+ ("test_if_and", test_if_and),
+ ("test_if_boolean", test_if_boolean),
+ ("test_if_else", test_if_else),
+ ("test_if_from_variable", test_if_from_variable),
+ ("test_if_or", test_if_or),
+ ("test_if_or_with_operators", test_if_or_with_operators),
+ ("test_literal_comparisons", test_literal_comparisons),
+ ("test_nested_if", test_nested_if),
+ ("test_unended_if", test_unended_if),
+ ]
+}
+
+extension LexerTests {
+ // DO NOT MODIFY: This is autogenerated, use:
+ // `swift test --generate-linuxmain`
+ // to regenerate.
+ static let __allTests__LexerTests = [
+ ("testTextToken", testTextToken),
+ ]
+}
+
+extension LiquidTests {
+ // DO NOT MODIFY: This is autogenerated, use:
+ // `swift test --generate-linuxmain`
+ // to regenerate.
+ static let __allTests__LiquidTests = [
+ ("testExample", testExample),
+ ]
+}
+
+extension ValueTests {
+ // DO NOT MODIFY: This is autogenerated, use:
+ // `swift test --generate-linuxmain`
+ // to regenerate.
+ static let __allTests__ValueTests = [
+ ("testComparable", testComparable),
+ ]
+}
+
+public func __allTests() -> [XCTestCaseEntry] {
+ return [
+ testCase(CaptureTests.__allTests__CaptureTests),
+ testCase(CaseTests.__allTests__CaseTests),
+ testCase(CycleTests.__allTests__CycleTests),
+ testCase(FilterTests.__allTests__FilterTests),
+ testCase(ForTests.__allTests__ForTests),
+ testCase(IfElseTests.__allTests__IfElseTests),
+ testCase(LexerTests.__allTests__LexerTests),
+ testCase(LiquidTests.__allTests__LiquidTests),
+ testCase(ValueTests.__allTests__ValueTests),
+ ]
+}
+#endif