krz/orgstar

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

Sources/OrgCore/Parser/Settings.swift

10c33661279a1fb0e8d3dd51509db9637778769e
orgstar/Sources/OrgCore/Parser/Settings.swift history · blame · raw

129 lines · 5215 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    public func isDone(_ name: String) -> Bool {
 62        todoSequences.contains { $0.done.contains { $0.name == name } }
 63    }
 64}
 65
 66enum SettingsScanner {
 67    /// Reads `#+TODO`, `#+SEQ_TODO`, `#+TYP_TODO` and `#+PRIORITIES` outside blocks. Any TODO
 68    /// line replaces the default sequences, as in org.
 69    static func scan(lines: [RawLine], info: [ClassifiedLine], blockEnds: [Int: Int], defaults: OrgSettings) -> OrgSettings {
 70        var sequences: [TodoSequence] = []
 71        var priorities = defaults.priorities
 72        var k = 0
 73        while k < lines.count {
 74            switch info[k].cls {
 75            case .blockBegin, .dynamicBegin:
 76                if let end = blockEnds[k] { k = end }
 77            case .keyword(let key):
 78                let value = keywordValue(lines[k].content)
 79                switch key {
 80                case "TODO", "SEQ_TODO":
 81                    if let s = todoSequence(value, kind: .sequence) { sequences.append(s) }
 82                case "TYP_TODO":
 83                    if let s = todoSequence(value, kind: .type) { sequences.append(s) }
 84                case "PRIORITIES":
 85                    let words = value.split(whereSeparator: \.isWhitespace)
 86                    if words.count == 3 {
 87                        priorities = Priorities(highest: String(words[0]), lowest: String(words[1]), default: String(words[2]))
 88                    }
 89                default:
 90                    break
 91                }
 92            default:
 93                break
 94            }
 95            k += 1
 96        }
 97        return OrgSettings(todoSequences: sequences.isEmpty ? defaults.todoSequences : sequences, priorities: priorities)
 98    }
 99
100    static func keywordValue(_ line: Substring) -> Substring {
101        guard let colon = line.firstIndex(of: ":") else { return "" }
102        return line[line.index(after: colon)...]
103    }
104
105    static func todoSequence(_ value: Substring, kind: TodoSequence.Kind) -> TodoSequence? {
106        let words = value.split(whereSeparator: \.isWhitespace)
107        guard !words.isEmpty else { return nil }
108        if let bar = words.firstIndex(of: "|") {
109            return TodoSequence(kind: kind, active: words[..<bar].map(todoKeyword), done: words[(bar + 1)...].map(todoKeyword))
110        }
111        return TodoSequence(kind: kind, active: words.dropLast().map(todoKeyword), done: [todoKeyword(words.last!)])
112    }
113
114    /// `NAME`, or `NAME(spec)` where spec is an optional fast key followed by `enter/leave`
115    /// logging flags.
116    static func todoKeyword(_ word: Substring) -> TodoKeyword {
117        guard let open = word.firstIndex(of: "("), word.last == ")" else { return TodoKeyword(name: String(word)) }
118        var spec = word[word.index(after: open)..<word.index(before: word.endIndex)]
119        var fastKey: Character?
120        if let first = spec.first, first != "!", first != "@", first != "/" {
121            fastKey = first
122            spec = spec.dropFirst()
123        }
124        let parts = spec.split(separator: "/", omittingEmptySubsequences: false)
125        let enter = parts.first.flatMap { $0.isEmpty ? nil : String($0) }
126        let leave = parts.count > 1 && !parts[1].isEmpty ? String(parts[1]) : nil
127        return TodoKeyword(name: String(word[..<open]), fastKey: fastKey, logOnEnter: enter, logOnLeave: leave)
128    }
129}