krz/orgstar

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

Sources/OrgCore/Parser/Settings.swift

176fc78498d9ba962fa963bbeca0dd433d9e0bab
orgstar/Sources/OrgCore/Parser/Settings.swift history · blame · raw

138 lines · 5614 bytes

  1public struct TodoKeyword: Sendable, Hashable {
  2    public var name: String
  3    public var fastKey: Character?
  4    /// Logging flag when entering the state (`!` or `@`), from `NAME(k!/@)`.
  5    public var logOnEnter: String?
  6    /// Logging flag when leaving the state.
  7    public var logOnLeave: String?
  8
  9    public init(name: String, fastKey: Character? = nil, logOnEnter: String? = nil, logOnLeave: String? = nil) {
 10        self.name = name
 11        self.fastKey = fastKey
 12        self.logOnEnter = logOnEnter
 13        self.logOnLeave = logOnLeave
 14    }
 15}
 16
 17public struct TodoSequence: Sendable, Equatable {
 18    public enum Kind: Sendable, Equatable { case sequence, type }
 19
 20    public var kind: Kind
 21    public var active: [TodoKeyword]
 22    public var done: [TodoKeyword]
 23
 24    public init(kind: Kind, active: [TodoKeyword], done: [TodoKeyword]) {
 25        self.kind = kind
 26        self.active = active
 27        self.done = done
 28    }
 29}
 30
 31public struct Priorities: Sendable, Equatable {
 32    public var highest: String
 33    public var lowest: String
 34    public var `default`: String
 35
 36    public init(highest: String, lowest: String, default: String) {
 37        self.highest = highest
 38        self.lowest = lowest
 39        self.default = `default`
 40    }
 41}
 42
 43public struct OrgSettings: Sendable, Equatable {
 44    public var todoSequences: [TodoSequence]
 45    public var priorities: Priorities
 46
 47    public init(todoSequences: [TodoSequence], priorities: Priorities) {
 48        self.todoSequences = todoSequences
 49        self.priorities = priorities
 50    }
 51
 52    public static let `default` = OrgSettings(
 53        todoSequences: [TodoSequence(kind: .sequence, active: [TodoKeyword(name: "TODO")], done: [TodoKeyword(name: "DONE")])],
 54        priorities: Priorities(highest: "A", lowest: "C", default: "B")
 55    )
 56
 57    public var todoKeywordNames: Set<String> {
 58        Set(todoSequences.flatMap { ($0.active + $0.done).map(\.name) })
 59    }
 60
 61    /// These settings with TODO keywords from `#+TODO:` syntax (`TODO NEXT | DONE`); nil when
 62    /// the line has no keywords.
 63    public func withTodoKeywords(_ line: String) -> OrgSettings? {
 64        guard let sequence = SettingsScanner.todoSequence(Substring(line), kind: .sequence) else { return nil }
 65        var copy = self
 66        copy.todoSequences = [sequence]
 67        return copy
 68    }
 69
 70    public func isDone(_ name: String) -> Bool {
 71        todoSequences.contains { $0.done.contains { $0.name == name } }
 72    }
 73}
 74
 75enum SettingsScanner {
 76    /// Reads `#+TODO`, `#+SEQ_TODO`, `#+TYP_TODO` and `#+PRIORITIES` outside blocks. Any TODO
 77    /// line replaces the default sequences, as in org.
 78    static func scan(lines: [RawLine], info: [ClassifiedLine], blockEnds: [Int: Int], defaults: OrgSettings) -> OrgSettings {
 79        var sequences: [TodoSequence] = []
 80        var priorities = defaults.priorities
 81        var k = 0
 82        while k < lines.count {
 83            switch info[k].cls {
 84            case .blockBegin, .dynamicBegin:
 85                if let end = blockEnds[k] { k = end }
 86            case .keyword(let key):
 87                let value = keywordValue(lines[k].content)
 88                switch key {
 89                case "TODO", "SEQ_TODO":
 90                    if let s = todoSequence(value, kind: .sequence) { sequences.append(s) }
 91                case "TYP_TODO":
 92                    if let s = todoSequence(value, kind: .type) { sequences.append(s) }
 93                case "PRIORITIES":
 94                    let words = value.split(whereSeparator: \.isWhitespace)
 95                    if words.count == 3 {
 96                        priorities = Priorities(highest: String(words[0]), lowest: String(words[1]), default: String(words[2]))
 97                    }
 98                default:
 99                    break
100                }
101            default:
102                break
103            }
104            k += 1
105        }
106        return OrgSettings(todoSequences: sequences.isEmpty ? defaults.todoSequences : sequences, priorities: priorities)
107    }
108
109    static func keywordValue(_ line: Substring) -> Substring {
110        guard let colon = line.firstIndex(of: ":") else { return "" }
111        return line[line.index(after: colon)...]
112    }
113
114    static func todoSequence(_ value: Substring, kind: TodoSequence.Kind) -> TodoSequence? {
115        let words = value.split(whereSeparator: \.isWhitespace)
116        guard !words.isEmpty else { return nil }
117        if let bar = words.firstIndex(of: "|") {
118            return TodoSequence(kind: kind, active: words[..<bar].map(todoKeyword), done: words[(bar + 1)...].map(todoKeyword))
119        }
120        return TodoSequence(kind: kind, active: words.dropLast().map(todoKeyword), done: [todoKeyword(words.last!)])
121    }
122
123    /// `NAME`, or `NAME(spec)` where spec is an optional fast key followed by `enter/leave`
124    /// logging flags.
125    static func todoKeyword(_ word: Substring) -> TodoKeyword {
126        guard let open = word.firstIndex(of: "("), word.last == ")" else { return TodoKeyword(name: String(word)) }
127        var spec = word[word.index(after: open)..<word.index(before: word.endIndex)]
128        var fastKey: Character?
129        if let first = spec.first, first != "!", first != "@", first != "/" {
130            fastKey = first
131            spec = spec.dropFirst()
132        }
133        let parts = spec.split(separator: "/", omittingEmptySubsequences: false)
134        let enter = parts.first.flatMap { $0.isEmpty ? nil : String($0) }
135        let leave = parts.count > 1 && !parts[1].isEmpty ? String(parts[1]) : nil
136        return TodoKeyword(name: String(word[..<open]), fastKey: fastKey, logOnEnter: enter, logOnLeave: leave)
137    }
138}