Sources/OrgCore/Keymap/TOML.swift
123 lines · 5764 bytes
1import Foundation
2
3/// The part of TOML keymaps use: `[table]` and `[[array-of-tables]]` headers, and
4/// `key = value` lines with basic or literal strings, booleans and integers. Comments start
5/// with `#` outside strings.
6public enum TOML {
7 public enum Value: Sendable, Equatable {
8 case string(String)
9 case bool(Bool)
10 case integer(Int)
11
12 public var string: String? {
13 if case .string(let value) = self { return value }
14 return nil
15 }
16
17 public var bool: Bool? {
18 if case .bool(let value) = self { return value }
19 return nil
20 }
21
22 public var integer: Int? {
23 if case .integer(let value) = self { return value }
24 return nil
25 }
26 }
27
28 public struct Table: Sendable, Equatable {
29 /// Empty for keys before the first header.
30 public var name: String
31 /// Declared with `[[name]]`.
32 public var isArrayElement: Bool
33 public var values: [String: Value]
34 /// 1-based line of the header.
35 public var line: Int
36 }
37
38 public struct ParseError: Error, Equatable, CustomStringConvertible {
39 public let line: Int
40 public let message: String
41 public var description: String { "line \(line): \(message)" }
42 }
43
44 /// Tables in file order.
45 public static func parse(_ text: String) throws -> [Table] {
46 var tables = [Table(name: "", isArrayElement: false, values: [:], line: 0)]
47 for (index, rawLine) in text.split(separator: "\n", omittingEmptySubsequences: false).enumerated() {
48 let number = index + 1
49 let line = rawLine.trimmingCharacters(in: [" ", "\t", "\r"])
50 if line.isEmpty || line.hasPrefix("#") { continue }
51 if line.hasPrefix("[") {
52 let isArray = line.hasPrefix("[[")
53 let close = isArray ? "]]" : "]"
54 let body = line.dropFirst(isArray ? 2 : 1)
55 guard let end = body.range(of: close) else { throw ParseError(line: number, message: "unclosed table header") }
56 let rest = body[end.upperBound...].trimmingCharacters(in: [" ", "\t"])
57 guard rest.isEmpty || rest.hasPrefix("#") else { throw ParseError(line: number, message: "text after table header") }
58 let name = body[..<end.lowerBound].trimmingCharacters(in: [" ", "\t"])
59 guard !name.isEmpty else { throw ParseError(line: number, message: "empty table name") }
60 tables.append(Table(name: name, isArrayElement: isArray, values: [:], line: number))
61 continue
62 }
63 guard let equals = line.firstIndex(of: "=") else { throw ParseError(line: number, message: "expected key = value") }
64 let key = line[..<equals].trimmingCharacters(in: [" ", "\t"])
65 guard !key.isEmpty, key.allSatisfy({ $0.isLetter || $0.isNumber || $0 == "_" || $0 == "-" }) else {
66 throw ParseError(line: number, message: "bad key")
67 }
68 var rest = Substring(line[line.index(after: equals)...]).drop { $0 == " " || $0 == "\t" }
69 let value = try parseValue(&rest, line: number)
70 rest = rest.drop { $0 == " " || $0 == "\t" }
71 guard rest.isEmpty || rest.first == "#" else { throw ParseError(line: number, message: "text after value") }
72 guard tables[tables.count - 1].values[key] == nil else { throw ParseError(line: number, message: "duplicate key \(key)") }
73 tables[tables.count - 1].values[key] = value
74 }
75 return tables
76 }
77
78 private static func parseValue(_ rest: inout Substring, line: Int) throws -> Value {
79 switch rest.first {
80 case "\"":
81 rest = rest.dropFirst()
82 var value = ""
83 while let c = rest.first {
84 rest = rest.dropFirst()
85 if c == "\"" { return .string(value) }
86 guard c == "\\" else {
87 value.append(c)
88 continue
89 }
90 guard let escaped = rest.first else { break }
91 rest = rest.dropFirst()
92 switch escaped {
93 case "\"": value.append("\"")
94 case "\\": value.append("\\")
95 case "n": value.append("\n")
96 case "t": value.append("\t")
97 case "u", "U":
98 let digits = escaped == "u" ? 4 : 8
99 guard let code = UInt32(rest.prefix(digits), radix: 16), let scalar = Unicode.Scalar(code), rest.count >= digits else {
100 throw ParseError(line: line, message: "bad unicode escape")
101 }
102 value.unicodeScalars.append(scalar)
103 rest = rest.dropFirst(digits)
104 default: throw ParseError(line: line, message: "bad escape \\\(escaped)")
105 }
106 }
107 throw ParseError(line: line, message: "unclosed string")
108 case "'":
109 rest = rest.dropFirst()
110 guard let end = rest.firstIndex(of: "'") else { throw ParseError(line: line, message: "unclosed string") }
111 let value = String(rest[..<end])
112 rest = rest[rest.index(after: end)...]
113 return .string(value)
114 default:
115 let word = rest.prefix { !$0.isWhitespace && $0 != "#" }
116 rest = rest.dropFirst(word.count)
117 if word == "true" { return .bool(true) }
118 if word == "false" { return .bool(false) }
119 if let number = Int(word.replacingOccurrences(of: "_", with: "")) { return .integer(number) }
120 throw ParseError(line: line, message: "unsupported value \(word)")
121 }
122 }
123}