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