Sources/OrgCore/Links/Backlinks.swift
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}