krz/orgstar

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

Sources/OrgCore/Links/Backlinks.swift

16086b4cf2caff5328774b2cd5ae3ffe1ab65ca4
orgstar/Sources/OrgCore/Links/Backlinks.swift history · blame · raw

58 lines · 2665 bytes

 1import Foundation
 2
 3/// Which links point at a file or one of its headings, read as `org-open-at-point` would
 4/// follow them: `id:`, `file:` with `::*Title` or `::#custom-id`, plain paths, and `*Title`,
 5/// `#custom-id` or a heading's exact title within the same file.
 6public enum Backlinks {
 7    public struct Target: Sendable, Equatable {
 8        public var path: String
 9        /// The heading, or nil for the file itself.
10        public var title: String?
11        public var id: String?
12        public var customID: String?
13
14        public init(path: String, title: String? = nil, id: String? = nil, customID: String? = nil) {
15            self.path = path
16            self.title = title
17            self.id = id
18            self.customID = customID
19        }
20    }
21
22    /// Whether `link`, written in the file at `source`, leads to `target`.
23    public static func links(_ link: String, from source: String, to target: Target, home: String = NSHomeDirectory()) -> Bool {
24        let link = Links.unescape(link)
25        if link.lowercased().hasPrefix("id:") {
26            let id = link.dropFirst(3).components(separatedBy: "::")[0]
27            return target.id.map { $0 == id } ?? false
28        }
29        var path: String
30        var search: String
31        if link.lowercased().hasPrefix("file:") {
32            let parts = link.dropFirst(5).components(separatedBy: "::")
33            path = parts[0]
34            search = parts.dropFirst().joined(separator: "::")
35        } else if ["/", "./", "../", "~/"].contains(where: link.hasPrefix) {
36            let parts = link.components(separatedBy: "::")
37            path = parts[0]
38            search = parts.dropFirst().joined(separator: "::")
39        } else if link.firstMatch(of: /^[a-zA-Z][-a-zA-Z0-9+]*:/) != nil {
40            return false
41        } else {
42            // Within the file.
43            guard source == target.path else { return false }
44            path = ""
45            search = link
46        }
47        if !path.isEmpty {
48            let directory = (source as NSString).deletingLastPathComponent
49            let expanded = Links.expand(path, directory: directory, home: home)
50            guard (expanded as NSString).standardizingPath == (target.path as NSString).standardizingPath else { return false }
51            if search.isEmpty { return target.title == nil }
52        }
53        guard let title = target.title else { return false }
54        if search.hasPrefix("#") { return target.customID.map { "#" + $0 == search } ?? false }
55        let wanted = search.hasPrefix("*") ? String(search.dropFirst()) : search
56        return Links.normalizedTitle(wanted) == Links.normalizedTitle(title)
57    }
58}