Sources/OrgCore/Semantic/HeadingInfo.swift
70 lines · 2912 bytes
1import Foundation
2
3public struct Property: Sendable, Equatable {
4 public let key: String
5 public let value: String
6 /// `:KEY+:` appends to the value instead of replacing it.
7 public let additive: Bool
8
9 public init(key: String, value: String, additive: Bool = false) {
10 self.key = key
11 self.value = value
12 self.additive = additive
13 }
14
15 /// A `:KEY: value` or `:KEY+: value` drawer line.
16 init?(line: String) {
17 let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
18 guard trimmed.hasPrefix(":") else { return nil }
19 let rest = trimmed.dropFirst()
20 // `org-property-re`: the key ends at the first colon followed by a blank or the end,
21 // so `:header-args:sh:` is one key.
22 guard let colon = rest.indices.first(where: { rest[$0] == ":" && (rest.index(after: $0) == rest.endIndex || rest[rest.index(after: $0)].isWhitespace) })
23 else { return nil }
24 var key = rest[..<colon]
25 guard !key.isEmpty else { return nil }
26 let additive = key.hasSuffix("+")
27 if additive { key = key.dropLast() }
28 let value = rest[rest.index(after: colon)...].trimmingCharacters(in: .whitespaces)
29 self.init(key: String(key), value: value, additive: additive)
30 }
31}
32
33public struct Clock: Sendable, Equatable {
34 public let start: Timestamp.Point
35 /// Nil while the clock is running.
36 public let end: Timestamp.Point?
37 /// From `=> H:MM`, when present.
38 public let minutes: Int?
39}
40
41/// One heading and the facts that belong to it directly, not to its children.
42public struct HeadingInfo: Sendable, Equatable {
43 public internal(set) var sectionRange: Range<Int>
44 public internal(set) var headingRange: Range<Int>
45 public internal(set) var level: Int
46 /// Index of the parent heading in `DocumentModel.headings`.
47 public internal(set) var parent: Int?
48 public internal(set) var todo: String?
49 public internal(set) var isDone: Bool
50 /// The cookie's value (`A`, `10`), when the heading has one.
51 public internal(set) var priority: String?
52 /// The title as written, markup included.
53 public internal(set) var title: String
54 public internal(set) var localTags: [String]
55 /// Property drawer lines in order.
56 public internal(set) var properties: [Property]
57 public internal(set) var scheduled: Timestamp?
58 public internal(set) var deadline: Timestamp?
59 public internal(set) var closed: Timestamp?
60 public internal(set) var clocks: [Clock]
61 /// Timestamps in the title and body, excluding planning and clock lines.
62 public internal(set) var timestamps: [Timestamp]
63 /// Link targets in the title and body.
64 public internal(set) var links: [String]
65
66 /// The heading's own `ID`. Never inherited.
67 public var id: String? {
68 properties.last { $0.key.uppercased() == "ID" && !$0.additive }?.value
69 }
70}