krz/orgstar

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

Sources/OrgCore/Capture/Datetree.swift

c1295824f55c7bba681fca8338bf08167e110105
orgstar/Sources/OrgCore/Capture/Datetree.swift history · blame · raw

155 lines · 8066 bytes

  1import Foundation
  2
  3// Date trees (org-datetree.el, Org 9.8.7): `org-datetree-find-create-entry` with the groupings
  4// capture uses, and `org-datetree-add-timestamp` nil. Each level is found or created inside
  5// the one above it, as org does while narrowed to it.
  6enum Datetree {
  7    static let weekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
  8    static let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
  9
 10    /// Each level's heading title, and the pattern whose first group orders siblings.
 11    static func levels(_ tree: CaptureTemplate.TreeType, date: DateComponents) -> [(title: String, pattern: String)] {
 12        var gregorian = Calendar(identifier: .gregorian)
 13        gregorian.timeZone = TimeZone(identifier: "UTC")!
 14        let day = gregorian.date(from: DateComponents(year: date.year, month: date.month, day: date.day))!
 15        let weekday = weekdays[gregorian.component(.weekday, from: day) - 1]
 16        var iso = Calendar(identifier: .iso8601)
 17        iso.timeZone = gregorian.timeZone
 18        let isoYear = iso.component(.yearForWeekOfYear, from: day)
 19        let week = iso.component(.weekOfYear, from: day)
 20        let year = (title: String(date.year!), pattern: "([12][0-9]{3})")
 21        let dayLevel = (title: String(format: "%04d-%02d-%02d ", date.year!, date.month!, date.day!) + weekday,
 22                        pattern: "([12][0-9]{3}-[01][0-9]-[0123][0-9]) \\w+")
 23        switch tree {
 24        case .day, .month:
 25            let month = (title: String(format: "%04d-%02d ", date.year!, date.month!) + months[date.month! - 1],
 26                         pattern: "([12][0-9]{3}-[01][0-9]) \\w+")
 27            return tree == .day ? [year, month, dayLevel] : [year, month]
 28        case .week:
 29            return [(String(isoYear), year.pattern), (String(format: "%04d-W%02d", isoYear, week), "([12][0-9]{3}-W[0-5][0-9])"), dayLevel]
 30        }
 31    }
 32
 33    /// Point at the date's entry, created where missing, under the heading at `parent` or at
 34    /// the top of the file. Returns the entry's start.
 35    static func findCreate(_ buffer: EmacsBuffer, under parent: Int?, tree: CaptureTemplate.TreeType, date: DateComponents) -> Int {
 36        let low = buffer.marker(parent ?? 0)
 37        let high = buffer.marker(parent.map { narrowEnd(buffer, $0, limit: buffer.length) } ?? buffer.length, advances: true)
 38        defer {
 39            buffer.release(low)
 40            buffer.release(high)
 41        }
 42        var level = parent.map { Capture.headingLevel(buffer, at: $0) + 1 } ?? 1
 43        for (title, pattern) in levels(tree, date: date) {
 44            subheading(buffer, title: title, pattern: pattern, level: level, low: low, high: high)
 45            level += 1
 46        }
 47        return low.position
 48    }
 49
 50    /// `org-datetree--find-create-subheading`: leaves the region narrowed to the subheading.
 51    static func subheading(_ buffer: EmacsBuffer, title: String, pattern: String, level: Int, low: EmacsBuffer.Marker, high: EmacsBuffer.Marker) {
 52        func compare(_ sibling: String) -> Int? {
 53            guard let a = firstGroup(pattern, sibling), let b = firstGroup(pattern, title) else { return nil }
 54            return a < b ? -1 : a > b ? 1 : 0
 55        }
 56        var sibling: (start: Int, order: Int)?
 57        buffer.point = low.position
 58        while let match = buffer.searchForward("^(\\*+) ", bound: high.position, caseInsensitive: false) {
 59            let start = match[0]!.lowerBound
 60            buffer.point = buffer.lineEnd(start)
 61            guard match[1]!.count == level else { continue }
 62            if let order = compare(rawTitle(buffer, start)), order >= 0 {
 63                sibling = (start, order)
 64                break
 65            }
 66        }
 67        if let sibling {
 68            buffer.point = sibling.start
 69        } else {
 70            buffer.point = high.position
 71            if !buffer.bolp { buffer.insert("\n") }
 72        }
 73        if let sibling, sibling.order == 0 {
 74            narrow(buffer, to: sibling.start, low: low, high: high)
 75            return
 76        }
 77        let end = buffer.point
 78        while buffer.point > low.position, let c = buffer.character(at: buffer.point - 1), c == 32 || c == 9 || c == 10 { buffer.point -= 1 }
 79        buffer.deleteRegion(buffer.point, end)
 80        if blankBeforeHeading(buffer, low: low.position, high: high.position) { buffer.insert("\n") }
 81        buffer.insert("\n" + String(repeating: "*", count: level) + " " + title + "\n")
 82        buffer.forwardLine(-1)
 83        narrow(buffer, to: buffer.point, low: low, high: high)
 84    }
 85
 86    static func firstGroup(_ pattern: String, _ s: String) -> String? {
 87        let regex = try! NSRegularExpression(pattern: pattern)
 88        let ns = s as NSString
 89        guard let m = regex.firstMatch(in: s, range: NSRange(location: 0, length: ns.length)) else { return nil }
 90        return ns.substring(with: m.range(at: 1))
 91    }
 92
 93    /// A heading's `:raw-value`: its title without keyword, priority or tags.
 94    static func rawTitle(_ buffer: EmacsBuffer, _ start: Int) -> String {
 95        buffer.saveExcursion {
 96            buffer.point = start
 97            guard let match = buffer.lookingAt(buffer.complexHeadingPattern), let title = match[4] else { return "" }
 98            return buffer.substring(title.lowerBound, title.upperBound)
 99        }
100    }
101
102    /// `org-narrow-to-subtree` inside the current region.
103    static func narrow(_ buffer: EmacsBuffer, to heading: Int, low: EmacsBuffer.Marker, high: EmacsBuffer.Marker) {
104        low.position = heading
105        high.position = narrowEnd(buffer, heading, limit: high.position)
106        buffer.point = heading
107    }
108
109    /// The subtree's end, before the newline that precedes the next heading, within `limit`.
110    static func narrowEnd(_ buffer: EmacsBuffer, _ heading: Int, limit: Int) -> Int {
111        let end = buffer.saveExcursion { () -> Int in
112            Capture.endOfSubtree(buffer, heading: heading)
113            return buffer.point
114        }
115        return min(limit, end == buffer.length ? end : end - 1)
116    }
117
118    /// `org--blank-before-heading-p` with `org-blank-before-new-entry` `(heading . auto)`,
119    /// in the region `low..<high`.
120    static func blankBeforeHeading(_ buffer: EmacsBuffer, low: Int, high: Int) -> Bool {
121        buffer.saveExcursion {
122            func headingStart(before limit: Int) -> Int? {
123                buffer.saveExcursion {
124                    buffer.point = buffer.lineEnd(limit)
125                    return buffer.searchBackward("^\\*+ ", bound: low, caseInsensitive: false)?[0]?.lowerBound
126                }
127            }
128            /// `outline-next-heading` bounded by `high`.
129            func nextHeading() -> Bool {
130                if let match = buffer.searchForward("\\n\\*+ ", bound: high, caseInsensitive: false) {
131                    buffer.point = match[0]!.lowerBound + 1
132                    return true
133                }
134                buffer.point = high
135                return false
136            }
137            func previousLineEmpty() -> Bool {
138                guard buffer.point > low else { return false }
139                let start = buffer.lineStart(buffer.point)
140                guard start > low else { return false }
141                let previous = max(low, buffer.lineStart(start - 1))
142                return buffer.substring(previous, start - 1).allSatisfy { $0 == " " || $0 == "\t" }
143            }
144            if headingStart(before: buffer.point) == nil {
145                guard nextHeading() else { return false }
146            }
147            guard let heading = headingStart(before: buffer.point) else { return false }
148            buffer.point = heading
149            if buffer.point != low { return previousLineEmpty() }
150            if nextHeading() { return previousLineEmpty() }
151            while buffer.point > low, let c = buffer.character(at: buffer.point - 1), c == 32 || c == 9 { buffer.point -= 1 }
152            return buffer.bolp ? previousLineEmpty() : false
153        }
154    }
155}