Sources/OrgCore/Parser/Settings.swift
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}