krz/orgstar

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

Sources/OrgCore/Semantic/HeadingInfo.swift

3a3dedba062790e1580c66d38606eda4b02cb501
orgstar/Sources/OrgCore/Semantic/HeadingInfo.swift history · blame · raw

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}