Sources/OrgCore/Parser/SetupFile.swift
57 lines · 3463 bytes
6 symbols in this file
1import Foundation
2
3/// `#+SETUPFILE` (`org--collect-keywords-1`): each setup file's keywords count as if they were
4/// in the file that names it, setup files named there included. Paths are relative to the
5/// naming file's folder; URLs aren't fetched.
6public enum SetupFile {
7 /// The keyword lines the setup files `text` names bring, in order, as `#+KEY: value` lines.
8 /// `read` gives a file's text, or nil when it can't be read. `path`, the naming file's,
9 /// counts as read already, as `buffer-file-name` does in Org.
10 public static func keywordLines(in text: String, directory: String, path: String? = nil, read: (String) -> String?) -> String {
11 lines(in: text, directory: directory, read: read, own: false, seen: path.map { [($0 as NSString).standardizingPath] } ?? [])
12 }
13
14 private static func lines(in text: String, directory: String, read: (String) -> String?, own: Bool, seen: Set<String>) -> String {
15 guard own || text.range(of: "#+setupfile:", options: .caseInsensitive) != nil else { return "" }
16 var out = ""
17 for node in OrgParser.parse(text).root.descendants() where node.kind == .keyword {
18 let line = node.text.trimmingCharacters(in: .whitespacesAndNewlines)
19 guard let colon = line.firstIndex(of: ":") else { continue }
20 let key = line[line.index(line.startIndex, offsetBy: 2)..<colon].uppercased()
21 guard key == "SETUPFILE" else {
22 if own { out += line + "\n" }
23 continue
24 }
25 var value = line[line.index(after: colon)...].trimmingCharacters(in: .whitespaces)
26 if value.count >= 2, value.hasPrefix("\""), value.hasSuffix("\"") { value = String(value.dropFirst().dropLast()) }
27 guard !value.isEmpty, value.range(of: "^[a-zA-Z][a-zA-Z0-9+.-]*://", options: .regularExpression) == nil else { continue }
28 let expanded = (value as NSString).expandingTildeInPath
29 let path = ((expanded.hasPrefix("/") ? expanded : (directory as NSString).appendingPathComponent(expanded)) as NSString).standardizingPath
30 guard !seen.contains(path), let contents = read(path) else { continue }
31 out += lines(in: contents, directory: (path as NSString).deletingLastPathComponent, read: read, own: true, seen: seen.union([path]))
32 }
33 return out
34 }
35
36 /// `(key, value)` for each `#+KEY: value` line.
37 static func keywords(_ lines: String) -> [(key: String, value: Substring)] {
38 lines.split(separator: "\n").compactMap { line in
39 guard line.hasPrefix("#+"), let colon = line.firstIndex(of: ":") else { return nil }
40 return (line[line.index(line.startIndex, offsetBy: 2)..<colon].uppercased(), line[line.index(after: colon)...])
41 }
42 }
43}
44
45extension OrgSettings {
46 /// These settings with the keywords of the setup files `text` names, for a file at `path`.
47 public func resolvingSetupFiles(in text: String, path: String, read: (String) -> String?) -> OrgSettings {
48 var copy = self
49 copy.setupKeywords = SetupFile.keywordLines(in: text, directory: (path as NSString).deletingLastPathComponent, path: path, read: read)
50 return copy
51 }
52
53 /// A file's text readable as UTF-8, for `resolvingSetupFiles`.
54 public static func readFile(_ path: String) -> String? {
55 FileManager.default.contents(atPath: path).flatMap { String(data: $0, encoding: .utf8) }
56 }
57}