Sources/OrgCore/Capture/Datetree.swift
155 lines · 8066 bytes
13 symbols in this file
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}