krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/OrgCore/Config/Lisp.swift

778058aafc7ddacee4a613b1da87c60aa0c8e113
orgstar/Sources/OrgCore/Config/Lisp.swift history · blame · raw

224 lines · 8482 bytes

15 symbols in this file
  1import Foundation
  2
  3/// Emacs Lisp read without evaluating: for importing settings from an Emacs configuration.
  4public indirect enum Sexp: Equatable, Sendable {
  5    case symbol(String)
  6    case string(String)
  7    case integer(Int)
  8    case float(Double)
  9    /// `?a`, as its code point.
 10    case character(Int)
 11    case list([Sexp])
 12    /// `(a b . c)`: the elements, then the final cdr.
 13    case dotted([Sexp], Sexp)
 14    case vector([Sexp])
 15
 16    public static let `nil` = Sexp.symbol("nil")
 17
 18    public var symbol: String? { if case .symbol(let s) = self { return s } else { return nil } }
 19    public var string: String? { if case .string(let s) = self { return s } else { return nil } }
 20    public var integer: Int? { if case .integer(let i) = self { return i } else { return nil } }
 21
 22    /// A list's elements; `nil` is the empty list.
 23    public var elements: [Sexp]? {
 24        switch self {
 25        case .list(let items): items
 26        case .symbol("nil"): []
 27        default: nil
 28        }
 29    }
 30
 31    /// The symbol a list starts with: `setq` for `(setq ...)`.
 32    public var head: String? { elements?.first?.symbol }
 33
 34    /// Written back as Lisp, for reports.
 35    public var description: String {
 36        switch self {
 37        case .symbol(let s): return s
 38        case .string(let s): return "\"" + s.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"").replacingOccurrences(of: "\n", with: "\\n") + "\""
 39        case .integer(let i): return String(i)
 40        case .float(let d): return String(d)
 41        case .character(let c): return "?" + (Unicode.Scalar(c).map { String(Character($0)) } ?? "")
 42        case .list(let items):
 43            if items.count == 2, let head = items[0].symbol {
 44                switch head {
 45                case "quote": return "'" + items[1].description
 46                case "function": return "#'" + items[1].description
 47                case "\\`": return "`" + items[1].description
 48                case ",": return "," + items[1].description
 49                case ",@": return ",@" + items[1].description
 50                default: break
 51                }
 52            }
 53            return "(" + items.map(\.description).joined(separator: " ") + ")"
 54        case .dotted(let items, let last): return "(" + items.map(\.description).joined(separator: " ") + " . " + last.description + ")"
 55        case .vector(let items): return "[" + items.map(\.description).joined(separator: " ") + "]"
 56        }
 57    }
 58}
 59
 60public enum LispReader {
 61    public struct Form: Sendable {
 62        public let sexp: Sexp
 63        /// 1-based line where the form starts.
 64        public let line: Int
 65    }
 66
 67    public struct ReadError: Error, Equatable, CustomStringConvertible {
 68        public let line: Int
 69        public let message: String
 70        public var description: String { "line \(line): \(message)" }
 71    }
 72
 73    /// The top-level forms of `text`.
 74    public static func read(_ text: String) throws -> [Form] {
 75        var reader = Reader(Array(text.unicodeScalars))
 76        var forms: [Form] = []
 77        while true {
 78            reader.skipSpace()
 79            guard !reader.atEnd else { break }
 80            let line = reader.line
 81            forms.append(Form(sexp: try reader.form(), line: line))
 82        }
 83        return forms
 84    }
 85
 86    struct Reader {
 87        let chars: [Unicode.Scalar]
 88        var index = 0
 89        var line = 1
 90
 91        init(_ chars: [Unicode.Scalar]) { self.chars = chars }
 92
 93        var atEnd: Bool { index >= chars.count }
 94        var peek: Unicode.Scalar? { index < chars.count ? chars[index] : nil }
 95
 96        mutating func next() -> Unicode.Scalar? {
 97            guard index < chars.count else { return nil }
 98            let c = chars[index]
 99            index += 1
100            if c == "\n" { line += 1 }
101            return c
102        }
103
104        mutating func skipSpace() {
105            while let c = peek {
106                if c == ";" {
107                    while let d = peek, d != "\n" { _ = next() }
108                } else if c.properties.isWhitespace {
109                    _ = next()
110                } else {
111                    return
112                }
113            }
114        }
115
116        func error(_ message: String) -> ReadError { ReadError(line: line, message: message) }
117
118        static let delimiters: Set<Unicode.Scalar> = ["(", ")", "[", "]", "\"", "'", "`", ",", ";"]
119
120        mutating func form() throws -> Sexp {
121            skipSpace()
122            guard let c = next() else { throw error("unexpected end of file") }
123            switch c {
124            case "(": return try list(close: ")")
125            case "[":
126                guard case .list(let items) = try list(close: "]") else { throw error("bad vector") }
127                return .vector(items)
128            case ")", "]": throw error("unexpected \(c)")
129            case "\"": return .string(try string())
130            case "'": return .list([.symbol("quote"), try form()])
131            case "`": return .list([.symbol("\\`"), try form()])
132            case ",":
133                if peek == "@" {
134                    _ = next()
135                    return .list([.symbol(",@"), try form()])
136                }
137                return .list([.symbol(","), try form()])
138            case "?": return .character(try character())
139            case "#":
140                if peek == "'" {
141                    _ = next()
142                    return .list([.symbol("function"), try form()])
143                }
144                return .symbol("#" + atom())
145            default:
146                index -= 1
147                if c == "\n" { line -= 1 }
148                return number(atom())
149            }
150        }
151
152        mutating func list(close: Unicode.Scalar) throws -> Sexp {
153            var items: [Sexp] = []
154            while true {
155                skipSpace()
156                guard let c = peek else { throw error("unclosed list") }
157                if c == close {
158                    _ = next()
159                    return .list(items)
160                }
161                if c == ".", index + 1 < chars.count, chars[index + 1].properties.isWhitespace || Self.delimiters.contains(chars[index + 1]) {
162                    _ = next()
163                    let last = try form()
164                    skipSpace()
165                    guard next() == close else { throw error("bad dotted list") }
166                    return .dotted(items, last)
167                }
168                items.append(try form())
169            }
170        }
171
172        mutating func string() throws -> String {
173            var out = ""
174            while let c = next() {
175                switch c {
176                case "\"": return out
177                case "\\":
178                    guard let e = next() else { break }
179                    switch e {
180                    case "n": out += "\n"
181                    case "t": out += "\t"
182                    case "\n": break
183                    default: out.unicodeScalars.append(e)
184                    }
185                default:
186                    out.unicodeScalars.append(c)
187                }
188            }
189            throw error("unclosed string")
190        }
191
192        mutating func character() throws -> Int {
193            guard let c = next() else { throw error("bad character") }
194            guard c == "\\" else { return Int(c.value) }
195            guard let e = next() else { throw error("bad character") }
196            switch e {
197            case "n": return 10
198            case "t": return 9
199            case "s": return 32
200            case "e": return 27
201            case "C", "M", "S", "H", "A":
202                if peek == "-" { _ = next(); return try character() }
203                return Int(e.value)
204            default: return Int(e.value)
205            }
206        }
207
208        mutating func atom() -> String {
209            var out = ""
210            while let c = peek, !c.properties.isWhitespace, !Self.delimiters.contains(c) {
211                _ = next()
212                if c == "\\", let escaped = next() { out.unicodeScalars.append(escaped) } else { out.unicodeScalars.append(c) }
213            }
214            return out
215        }
216
217        func number(_ text: String) -> Sexp {
218            if let i = Int(text.hasPrefix("+") ? String(text.dropFirst()) : text) { return .integer(i) }
219            if text.contains(where: { $0 == "." || $0 == "e" }), text.first.map({ $0.isNumber || $0 == "-" || $0 == "+" || $0 == "." }) == true,
220               let d = Double(text) { return .float(d) }
221            return .symbol(text)
222        }
223    }
224}